Skip to content

Commit

Permalink
Desktop: Add ability to share a note publicly using Nextcloud (#2173)
Browse files Browse the repository at this point in the history
* Moved button row to separate component file and started Sharing dialog

* Adding Sharing dialog

* Applied "npx react-codemod rename-unsafe-lifecycles"

* More UI

* Tools: Improved TypeScript integration

* Improved share dialog

* Tools Added support for translation validation in CI, and added support for plural translations

* Improved UI and sharing workflow

* Share workflow

* Cleaned up and improved sharing config error handling

* Fixed build scripts and doc for TypeScript

* Run linter
  • Loading branch information
laurent22 committed Dec 13, 2019
1 parent 611be7c commit 34f0a29
Show file tree
Hide file tree
Showing 39 changed files with 988 additions and 159 deletions.
6 changes: 5 additions & 1 deletion .eslintignore
Expand Up @@ -45,4 +45,8 @@ Server/docs/
Server/dist/
Server/bin/
Server/node_modules/
ElectronClient/app/packageInfo.js
ElectronClient/app/packageInfo.js

# Ignore files generated from TypeScript files
ElectronClient/app/gui/ShareNoteDialog.js
ReactNativeClient/lib/JoplinServerApi.js
5 changes: 5 additions & 0 deletions .gitignore
Expand Up @@ -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
19 changes: 18 additions & 1 deletion .travis.yml
Expand Up @@ -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 ..
Expand Down Expand Up @@ -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.
Expand All @@ -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
31 changes: 23 additions & 8 deletions BUILD.md
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
```
Expand All @@ -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
```
Expand All @@ -67,15 +75,22 @@ 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

```
cd CliClient
npm install
./build.sh
rsync --delete -aP ../ReactNativeClient/locales/ build/locales/
```

Run `run.sh` to start the application for testing.
5 changes: 5 additions & 0 deletions CliClient/build.sh
Expand Up @@ -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"
95 changes: 86 additions & 9 deletions ElectronClient/app/gui/ConfigScreen.jsx
Expand Up @@ -24,14 +24,27 @@ 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,
};

this.configMenuBar_selectionChange = this.configMenuBar_selectionChange.bind(this);
}

componentWillMount() {
UNSAFE_componentWillMount() {
this.setState({ settings: this.props.settings });
}

Expand Down Expand Up @@ -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,
Expand All @@ -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 : (
<div style={statusStyle}>
{messages[0]}
Expand All @@ -137,12 +157,69 @@ class ConfigScreenComponent extends React.Component {
</div>
);
}

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 : (
<div style={statusStyle}>
<p style={theme.textStyle}>{_('The Joplin Nextcloud App is either not installed or misconfigured. Please see the full error message below:')}</p>
<pre>{errorMessage}</pre>
</div>
);

const showLogButton = !errorMessage || this.state.showNextcloudAppLog ? null : (
<a style={theme.urlStyle} href="#" onClick={this.showLogButton_click}>[{_('Show Log')}]</a>
);

const appStatusStyle = Object.assign({}, theme.textStyle, { fontWeight: 'bold' });

settingComps.push(
<div key="nextcloud_app_check" style={this.rowStyle_}>
<span style={theme.textStyle}>Beta: {_('Joplin Nextcloud App status:')} </span><span style={appStatusStyle}>{status}</span>
&nbsp;&nbsp;
{showLogButton}
&nbsp;&nbsp;
<button disabled={this.state.checkNextcloudAppResult === 'checking'} style={theme.buttonStyle} onClick={this.checkNextcloudAppButton_click}>
{_('Check Status')}
</button>
&nbsp;&nbsp;
<a style={theme.urlStyle} href="#" onClick={this.nextcloudAppHelpLink_click}>[{_('Help')}]</a>
{statusComp}
</div>
);
}
}

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 = <button onClick={() => shared.advancedSettingsButton_click(this)} style={advancedSettingsButtonStyle}><i style={{fontSize: 14}} className={iconName}></i> {_('Show Advanced Settings')}</button>;
advancedSettingsSectionStyle.display = this.state.showAdvancedSettings ? 'block' : 'none';
}

return (
<div key={key} style={sectionStyle}>
{noteComp}
<div>{settingComps}</div>
{advancedSettingsButton}
<div style={advancedSettingsSectionStyle}>{advancedSettingComps}</div>
</div>
);
}
Expand Down
45 changes: 45 additions & 0 deletions 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(
<button key="ok" style={theme.buttonStyle} onClick={okButton_click} ref={props.okButtonRef} onKeyDown={onKeyDown}>
{_('OK')}
</button>
);
}

if (props.cancelButtonShow !== false) {
buttonComps.push(
<button key="cancel" style={Object.assign({}, theme.buttonStyle, { marginLeft: 10 })} onClick={cancelButton_click}>
{props.cancelButtonLabel ? props.cancelButtonLabel : _('Cancel')}
</button>
);
}

return <div style={{ textAlign: 'right', marginTop: 10 }}>{buttonComps}</div>;
}

module.exports = DialogButtonRow;
2 changes: 1 addition & 1 deletion ElectronClient/app/gui/DropboxLoginScreen.jsx
Expand Up @@ -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();
}

Expand Down
4 changes: 2 additions & 2 deletions ElectronClient/app/gui/EncryptionConfigScreen.jsx
Expand Up @@ -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);
}

Expand Down
2 changes: 1 addition & 1 deletion ElectronClient/app/gui/Header.jsx
Expand Up @@ -71,7 +71,7 @@ class HeaderComponent extends React.Component {
};
}

async componentWillReceiveProps(nextProps) {
async UNSAFE_componentWillReceiveProps(nextProps) {
if (nextProps.windowCommand) {
this.doCommand(nextProps.windowCommand);
}
Expand Down
4 changes: 2 additions & 2 deletions ElectronClient/app/gui/ImportScreen.jsx
Expand Up @@ -8,15 +8,15 @@ 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,
messages: [],
});
}

componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
if (newProps.filePath) {
this.setState(
{
Expand Down
4 changes: 2 additions & 2 deletions ElectronClient/app/gui/ItemList.jsx
Expand Up @@ -32,11 +32,11 @@ class ItemList extends React.Component {
});
}

componentWillMount() {
UNSAFE_componentWillMount() {
this.updateStateItemIndexes();
}

componentWillReceiveProps(newProps) {
UNSAFE_componentWillReceiveProps(newProps) {
this.updateStateItemIndexes(newProps);
}

Expand Down

0 comments on commit 34f0a29

Please sign in to comment.