diff --git a/.eslintignore b/.eslintignore index 58fd3b1955a..069b1736366 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 14940ee979d..f930ed530a1 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 7ed9ff2a0d7..e56c2b32a83 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 46cbb12c17c..7e00b45e95a 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 38c49293a46..e7190706c4b 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 a7db35001bb..38bf1eb21d4 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 = []; - - for (let i = 0; i < section.metadatas.length; i++) { - const md = section.metadatas[i]; + // const settingComps = []; + + 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} +    + +    + [{_('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 = ; + 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 00000000000..e4bd2cf09b2 --- /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( + + ); + } + + if (props.cancelButtonShow !== false) { + buttonComps.push( + + ); + } + + return
{buttonComps}
; +} + +module.exports = DialogButtonRow; diff --git a/ElectronClient/app/gui/DropboxLoginScreen.jsx b/ElectronClient/app/gui/DropboxLoginScreen.jsx index 18b8cf76157..88be59f61b5 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 51ac9dd9d09..0ae093f5692 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 e495d9c3255..24bd1d3e2a0 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 6bd2a8a4c9e..6a46c0f4d2f 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 297ada29430..80671ee29db 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 f8fcd5aa5e7..6274220743d 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 abc3b468135..d87fccaa7c6 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 0c2fe70b1c3..ff736d27202 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( - - ); - buttonComps.push( - - ); - 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 76fd96bbcc7..7d484347901 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 63559d30cc9..d0c01d6abc5 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 112b5ddb817..9d2127c9e0d 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 00000000000..e70ba8338e4 --- /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 : ( + + ); + + 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)} + +
{statusMessage(sharesState)}
+ +
+
+ ); +} diff --git a/ElectronClient/app/gui/StatusScreen.jsx b/ElectronClient/app/gui/StatusScreen.jsx index 645a35890a3..006c4ed3bbe 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 03fa83ac32d..949e4c4e9c7 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 d999c603de7..b9d15fec306 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 8acf455c640..0659ad60499 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 be2cd01c97f..c1234466443 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 00000000000..cb244a86302 --- /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 f63811593d3..2e01b474143 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 d738f20a2db..3e33f892397 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 e10151cf4c3..20ec3411ebc 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 bb7a5998abb..d637ab3d1fc 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 ca60f7b0ff5..33e830da99c 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 0a81843e0c0..288f0e9dbfb 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 498e557f85f..133feaac18d 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 ce9419f9677..bc467a9e365 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": { + "@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": { + "@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.2", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz", - "integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==", + "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.0.0" + "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 8a72c31299f..5b6ebac2ba6 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 00000000000..715d2e70155 --- /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 54fe85c86b7..209e96160dc 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",