diff --git a/.eslintignore b/.eslintignore
index 58fd3b1955..069b173636 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 14940ee979..f930ed530a 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 25185c118e..e56c2b32a8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -50,12 +50,17 @@ before_install:
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update || true
sudo apt-get install -y yarn
+ sudo apt-get install -y gettext
fi
script:
- |
+ # Copy lib
+ rsync -aP --delete ReactNativeClient/lib/ ElectronClient/app/lib/
+
# Install tools
npm install
+ npm run typescript-compile
cd Tools
npm install
cd ..
@@ -84,6 +89,19 @@ script:
fi
fi
+ # Validate translations - this is needed as some users manually
+ # edit .po files (and often make mistakes) instead of using a proper
+ # tool like poedit. Doing it for Linux only is sufficient.
+ if [ "$TRAVIS_PULL_REQUEST" != "false" ]; then
+ if [ "$TRAVIS_OS_NAME" != "osx" ]; then
+ node Tools/validate-translation.js
+ testResult=$?
+ if [ $testResult -ne 0 ]; then
+ exit $testResult
+ fi
+ fi
+ fi
+
# Find out if we should run the build or not. Electron-builder gets stuck when
# builing PRs so we disable it in this case. The Linux build should provide
# enough info if the app builds or not.
@@ -96,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 46cbb12c17..7e00b45e95 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 38c49293a4..e7190706c4 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 a7db35001b..38bf1eb21d 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 0000000000..e4bd2cf09b
--- /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 18b8cf7615..88be59f61b 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 51ac9dd9d0..0ae093f569 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 e495d9c325..24bd1d3e2a 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 6bd2a8a4c9..6a46c0f4d2 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 297ada2943..80671ee29d 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 f8fcd5aa5e..6274220743 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 abc3b46813..d87fccaa7c 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 0c2fe70b1c..ff736d2720 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 76fd96bbcc..7d48434790 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 63559d30cc..d0c01d6abc 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 112b5ddb81..9d2127c9e0 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 0000000000..e70ba8338e
--- /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 645a35890a..006c4ed3bb 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 03fa83ac32..949e4c4e9c 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 d999c603de..b9d15fec30 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 8acf455c64..0659ad6049 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 be2cd01c97..c123446644 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 0000000000..cb244a8630
--- /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 f63811593d..2e01b47414 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 d738f20a2d..3e33f89239 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/locale.js b/ReactNativeClient/lib/locale.js
index a34afe5332..7696eb5b1b 100644
--- a/ReactNativeClient/lib/locale.js
+++ b/ReactNativeClient/lib/locale.js
@@ -317,4 +317,9 @@ function _(s, ...args) {
}
}
-module.exports = { _, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
+function _n(singular, plural, n, ...args) {
+ if (n > 1) return _(plural, ...args);
+ return _(singular, ...args);
+}
+
+module.exports = { _, _n, supportedLocales, countryDisplayName, localeStrings, setLocale, supportedLocalesToLanguages, defaultLocale, closestSupportedLocale, languageCode, countryCodeOnly };
diff --git a/ReactNativeClient/lib/models/Setting.js b/ReactNativeClient/lib/models/Setting.js
index e10151cf4c..20ec3411eb 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 bb7a5998ab..d637ab3d1f 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 ca60f7b0ff..33e830da99 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/Tools/build-translation.js b/Tools/build-translation.js
index 17226cfa41..e02fcd1811 100644
--- a/Tools/build-translation.js
+++ b/Tools/build-translation.js
@@ -88,6 +88,7 @@ async function createPotFile(potFilePath, sources) {
baseArgs.push('--package-name=Joplin-CLI');
baseArgs.push('--package-version=1.0.0');
baseArgs.push('--no-location');
+ baseArgs.push('--keyword=_n:1,2');
for (let i = 0; i < sources.length; i++) {
let args = baseArgs.slice();
diff --git a/Tools/validate-translation.js b/Tools/validate-translation.js
new file mode 100644
index 0000000000..091b786ea9
--- /dev/null
+++ b/Tools/validate-translation.js
@@ -0,0 +1,35 @@
+'use strict';
+
+// Dependencies:
+//
+// sudo apt install gettext
+
+require('app-module-path').addPath(`${__dirname}/../ReactNativeClient`);
+
+const rootDir = `${__dirname}/..`;
+const fs = require('fs-extra');
+const cliLocalesDir = `${rootDir}/CliClient/locales`;
+const { execCommand } = require('./tool-utils.js');
+
+async function main() {
+ const files = fs.readdirSync(cliLocalesDir);
+ let hasErrors = false;
+ for (const file of files) {
+ if (!file.endsWith('.po')) continue;
+ const fullPath = `${cliLocalesDir}/${file}`;
+
+ try {
+ await execCommand(`msgfmt -v "${fullPath}"`);
+ } catch (error) {
+ hasErrors = true;
+ console.error(error);
+ }
+ }
+
+ if (hasErrors) throw new Error('Some .po files could not be validated');
+}
+
+main().catch((error) => {
+ console.error(error);
+ process.exit(1);
+});
diff --git a/appveyor.yml b/appveyor.yml
index 0a81843e0c..288f0e9dbf 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 498e557f85..133feaac18 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 ce9419f967..bc467a9e36 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 8a72c31299..5b6ebac2ba 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 0000000000..715d2e7015
--- /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 54fe85c86b..209e96160d 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",