From 8dd41dca9655493bfb8a6d441b1ac718543bd0dd Mon Sep 17 00:00:00 2001 From: Laurent Cozic Date: Mon, 6 Nov 2017 18:35:04 +0000 Subject: [PATCH] Added navigator and handle OneDrive auth --- ElectronClient/app/app.js | 103 +++++++++++++----- ElectronClient/app/bridge.js | 5 + ElectronClient/app/gui/MainScreen.jsx | 50 +++++++++ ElectronClient/app/gui/Navigator.jsx | 34 ++++++ ElectronClient/app/gui/OneDriveAuthScreen.jsx | 103 ++++++++++++++++++ ElectronClient/app/gui/Root.jsx | 42 ++----- ElectronClient/app/gui/SideBar.jsx | 9 +- ReactNativeClient/lib/BaseApplication.js | 6 +- .../lib/components/screens/onedrive-login.js | 2 +- ReactNativeClient/lib/onedrive-api.js | 4 + ReactNativeClient/lib/reducer.js | 13 --- ReactNativeClient/root.js | 11 +- 12 files changed, 305 insertions(+), 77 deletions(-) create mode 100644 ElectronClient/app/gui/MainScreen.jsx create mode 100644 ElectronClient/app/gui/Navigator.jsx create mode 100644 ElectronClient/app/gui/OneDriveAuthScreen.jsx diff --git a/ElectronClient/app/app.js b/ElectronClient/app/app.js index 21845859f..356c660a8 100644 --- a/ElectronClient/app/app.js +++ b/ElectronClient/app/app.js @@ -14,50 +14,96 @@ const { sprintf } = require('sprintf-js'); const { JoplinDatabase } = require('lib/joplin-database.js'); const { DatabaseDriverNode } = require('lib/database-driver-node.js'); const { ElectronAppWrapper } = require('./ElectronAppWrapper'); +const { defaultState } = require('lib/reducer.js'); + +const appDefaultState = Object.assign({}, defaultState, { + route: { + type: 'NAV_GO', + routeName: 'Main', + params: {}, + }, + navHistory: [], +}); class Application extends BaseApplication { - constructor() { - super(); - } - hasGui() { return true; } + reducer(state = appDefaultState, action) { + let newState = state; + + try { + switch (action.type) { + + case 'NAV_BACK': + case 'NAV_GO': + + const goingBack = action.type === 'NAV_BACK'; + + if (goingBack && !state.navHistory.length) break; + + const currentRoute = state.route; + + newState = Object.assign({}, state); + let newNavHistory = state.navHistory.slice(); + + if (goingBack) { + let newAction = null; + while (newNavHistory.length) { + newAction = newNavHistory.pop(); + if (newAction.routeName !== state.route.routeName) break; + } + + if (!newAction) break; + + action = newAction; + } + + if (!goingBack) newNavHistory.push(currentRoute); + newState.navHistory = newNavHistory + newState.route = action; + break; + + case 'WINDOW_CONTENT_SIZE_SET': + + newState = Object.assign({}, state); + newState.windowContentSize = action.size; + break; + + } + } catch (error) { + error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action); + throw error; + } + + return super.reducer(newState, action); + } + async start(argv) { argv = await super.start(argv); this.initRedux(); - //this.gui_ = new ElectronAppWrapper(this.electronApp_, this, this.store()); + // Since the settings need to be loaded before the store is created, it will never + // receive the SETTINGS_UPDATE_ALL even, which mean state.settings will not be + // initialised. So we manually call dispatchUpdateAll() to force an update. + Setting.dispatchUpdateAll(); - try { - // this.gui_.setLogger(this.logger()); - // await this.gui().start(); + await FoldersScreenUtils.refreshFolders(); - // Since the settings need to be loaded before the store is created, it will never - // receive the SETTINGS_UPDATE_ALL even, which mean state.settings will not be - // initialised. So we manually call dispatchUpdateAll() to force an update. - Setting.dispatchUpdateAll(); + const tags = await Tag.allWithNotes(); - await FoldersScreenUtils.refreshFolders(); + this.dispatch({ + type: 'TAGS_UPDATE_ALL', + tags: tags, + }); - const tags = await Tag.allWithNotes(); - - this.dispatch({ - type: 'TAGS_UPDATE_ALL', - tags: tags, - }); - - this.store().dispatch({ - type: 'FOLDERS_SELECT', - id: Setting.value('activeFolderId'), - }); - } catch (error) { - //await this.gui_.exit(); - throw error; - } + this.store().dispatch({ + type: 'FOLDERS_SELECT', + id: Setting.value('activeFolderId'), + }); } } @@ -65,7 +111,6 @@ class Application extends BaseApplication { let application_ = null; function app() { - //if (!application_) throw new Error('Application has not been initialized'); if (!application_) application_ = new Application(); return application_; } diff --git a/ElectronClient/app/bridge.js b/ElectronClient/app/bridge.js index 19c3d69d1..1bdb4ed47 100644 --- a/ElectronClient/app/bridge.js +++ b/ElectronClient/app/bridge.js @@ -18,6 +18,11 @@ class Bridge { return { width: s[0], height: s[1] }; } + showMessageBox(options) { + const {dialog} = require('electron'); + return dialog.showMessageBox(options); + } + } let bridge_ = null; diff --git a/ElectronClient/app/gui/MainScreen.jsx b/ElectronClient/app/gui/MainScreen.jsx new file mode 100644 index 000000000..bc6bfdbaf --- /dev/null +++ b/ElectronClient/app/gui/MainScreen.jsx @@ -0,0 +1,50 @@ +const React = require('react'); +const { connect } = require('react-redux'); +const { SideBar } = require('./SideBar.min.js'); +const { NoteList } = require('./NoteList.min.js'); +const { NoteText } = require('./NoteText.min.js'); + +class MainScreenComponent extends React.Component { + + render() { + const style = this.props.style; + + const noteListStyle = { + width: Math.floor(style.width / 3), + height: style.height, + display: 'inline-block', + verticalAlign: 'top', + }; + + const noteTextStyle = { + width: noteListStyle.width, + height: style.height, + display: 'inline-block', + verticalAlign: 'top', + }; + + const sideBarStyle = { + width: style.width - (noteTextStyle.width + noteListStyle.width), + height: style.height, + display: 'inline-block', + verticalAlign: 'top', + }; + + return ( +
+ + + +
+ ); + } + +} + +const mapStateToProps = (state) => { + return {}; +}; + +const MainScreen = connect(mapStateToProps)(MainScreenComponent); + +module.exports = { MainScreen }; \ No newline at end of file diff --git a/ElectronClient/app/gui/Navigator.jsx b/ElectronClient/app/gui/Navigator.jsx new file mode 100644 index 000000000..1fb1adc4c --- /dev/null +++ b/ElectronClient/app/gui/Navigator.jsx @@ -0,0 +1,34 @@ +const React = require('react'); const Component = React.Component; +const { connect } = require('react-redux'); + +class NavigatorComponent extends Component { + + render() { + if (!this.props.route) throw new Error('Route must not be null'); + + const route = this.props.route; + const Screen = this.props.screens[route.routeName].screen; + + const screenStyle = { + width: this.props.style.width, + height: this.props.style.height, + }; + + return ( +
+ +
+ ); + } + +} + +const Navigator = connect( + (state) => { + return { + route: state.route, + }; + } +)(NavigatorComponent) + +module.exports = { Navigator }; \ No newline at end of file diff --git a/ElectronClient/app/gui/OneDriveAuthScreen.jsx b/ElectronClient/app/gui/OneDriveAuthScreen.jsx new file mode 100644 index 000000000..0c10291da --- /dev/null +++ b/ElectronClient/app/gui/OneDriveAuthScreen.jsx @@ -0,0 +1,103 @@ +const React = require('react'); +const { connect } = require('react-redux'); +const shared = require('lib/components/shared/side-menu-shared.js'); +const { Synchronizer } = require('lib/synchronizer.js'); +const { reg } = require('lib/registry.js'); +const { bridge } = require('electron').remote.require('./bridge'); + +class OneDriveAuthScreenComponent extends React.Component { + + constructor() { + super(); + this.webview_ = null; + this.authCode_ = null; + } + + back_click() { + this.props.dispatch({ + type: 'NAV_BACK', + }); + } + + refresh_click() { + if (!this.webview_) return; + this.webview_.src = this.startUrl(); + } + + componentWillMount() { + this.setState({ + webviewUrl: this.startUrl(), + webviewReady: false, + }); + } + + componentDidMount() { + this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this)); + } + + componentWillUnmount() { + this.webview_.addEventListener('dom-ready', this.webview_domReady.bind(this)); + } + + webview_domReady() { + this.setState({ + webviewReady: true, + }); + + this.webview_.addEventListener('did-navigate', async (event) => { + const url = event.url; + + if (this.authCode_) return; + if (url.indexOf(this.redirectUrl() + '?code=') !== 0) return; + + let code = url.split('?code='); + this.authCode_ = code[1]; + + try { + await reg.oneDriveApi().execTokenRequest(this.authCode_, this.redirectUrl(), true); + this.props.dispatch({ type: 'NAV_BACK' }); + reg.scheduleSync(0); + } catch (error) { + bridge().showMessageBox({ + type: 'error', + message: error.message, + }); + } + + this.authCode_ = null; + }); + } + + startUrl() { + return reg.oneDriveApi().authCodeUrl(this.redirectUrl()); + } + + redirectUrl() { + return reg.oneDriveApi().nativeClientRedirectUrl(); + } + + render() { + const webviewStyle = { + width: this.props.style.width, + height: this.props.style.height, + overflow: 'hidden', + }; + + return ( +
+ {this.back_click()}}>BACK + {this.refresh_click()}}>REFRESH + this.webview_ = elem} /> +
+ ); + } + +} + +const mapStateToProps = (state) => { + return {}; +}; + +const OneDriveAuthScreen = connect(mapStateToProps)(OneDriveAuthScreenComponent); + +module.exports = { OneDriveAuthScreen }; \ No newline at end of file diff --git a/ElectronClient/app/gui/Root.jsx b/ElectronClient/app/gui/Root.jsx index d4be5eddb..d4abff66d 100644 --- a/ElectronClient/app/gui/Root.jsx +++ b/ElectronClient/app/gui/Root.jsx @@ -3,9 +3,9 @@ const { render } = require('react-dom'); const { createStore } = require('redux'); const { connect, Provider } = require('react-redux'); -const { SideBar } = require('./SideBar.min.js'); -const { NoteList } = require('./NoteList.min.js'); -const { NoteText } = require('./NoteText.min.js'); +const { MainScreen } = require('./MainScreen.min.js'); +const { OneDriveAuthScreen } = require('./OneDriveAuthScreen.min.js'); +const { Navigator } = require('./Navigator.min.js'); const { app } = require('../app'); @@ -25,7 +25,7 @@ async function initialize(dispatch) { }); } -class ReactRootComponent extends React.Component { +class RootComponent extends React.Component { async componentDidMount() { if (this.props.appState == 'starting') { @@ -44,38 +44,18 @@ class ReactRootComponent extends React.Component { } render() { - const style = { + const navigatorStyle = { width: this.props.size.width, height: this.props.size.height, }; - const noteListStyle = { - width: Math.floor(this.props.size.width / 3), - height: this.props.size.height, - display: 'inline-block', - verticalAlign: 'top', - }; - - const noteTextStyle = { - width: noteListStyle.width, - height: this.props.size.height, - display: 'inline-block', - verticalAlign: 'top', - }; - - const sideBarStyle = { - width: this.props.size.width - (noteTextStyle.width + noteListStyle.width), - height: this.props.size.height, - display: 'inline-block', - verticalAlign: 'top', + const screens = { + Main: { screen: MainScreen }, + OneDriveAuth: { screen: OneDriveAuthScreen }, }; return ( -
- - - -
+ ); } @@ -88,13 +68,13 @@ const mapStateToProps = (state) => { }; }; -const ReactRoot = connect(mapStateToProps)(ReactRootComponent); +const Root = connect(mapStateToProps)(RootComponent); const store = app().store(); render( - + , document.getElementById('react-root') ) \ No newline at end of file diff --git a/ElectronClient/app/gui/SideBar.jsx b/ElectronClient/app/gui/SideBar.jsx index 654805011..87ede1f1b 100644 --- a/ElectronClient/app/gui/SideBar.jsx +++ b/ElectronClient/app/gui/SideBar.jsx @@ -19,6 +19,13 @@ class SideBarComponent extends React.Component { }); } + sync_click() { + this.props.dispatch({ + type: 'NAV_GO', + routeName: 'OneDriveAuth', + }); + } + folderItem(folder, selected) { let classes = []; if (selected) classes.push('selected'); @@ -36,7 +43,7 @@ class SideBarComponent extends React.Component { } synchronizeButton(label) { - return
{label}
+ return {this.sync_click()}}>{label} } render() { diff --git a/ReactNativeClient/lib/BaseApplication.js b/ReactNativeClient/lib/BaseApplication.js index fc223ab72..6bd543a00 100644 --- a/ReactNativeClient/lib/BaseApplication.js +++ b/ReactNativeClient/lib/BaseApplication.js @@ -231,8 +231,12 @@ class BaseApplication { if (this.store()) return this.store().dispatch(action); } + reducer(state = defaultState, action) { + return reducer(state, action); + } + initRedux() { - this.store_ = createStore(reducer, applyMiddleware(this.generalMiddleware())); + this.store_ = createStore(this.reducer, applyMiddleware(this.generalMiddleware())); BaseModel.dispatch = this.store().dispatch; FoldersScreenUtils.dispatch = this.store().dispatch; } diff --git a/ReactNativeClient/lib/components/screens/onedrive-login.js b/ReactNativeClient/lib/components/screens/onedrive-login.js index bd476502c..30653bf9b 100644 --- a/ReactNativeClient/lib/components/screens/onedrive-login.js +++ b/ReactNativeClient/lib/components/screens/onedrive-login.js @@ -32,7 +32,7 @@ class OneDriveLoginScreenComponent extends BaseScreenComponent { } redirectUrl() { - return 'https://login.microsoftonline.com/common/oauth2/nativeclient'; + return reg.oneDriveApi().nativeClientRedirectUrl(); } async webview_load(noIdeaWhatThisIs) { diff --git a/ReactNativeClient/lib/onedrive-api.js b/ReactNativeClient/lib/onedrive-api.js index 0d9f86ed9..c3c5894db 100644 --- a/ReactNativeClient/lib/onedrive-api.js +++ b/ReactNativeClient/lib/onedrive-api.js @@ -47,6 +47,10 @@ class OneDriveApi { return 'https://login.microsoftonline.com/common/oauth2/v2.0/token'; } + nativeClientRedirectUrl() { + return 'https://login.microsoftonline.com/common/oauth2/nativeclient'; + } + auth() { return this.auth_; } diff --git a/ReactNativeClient/lib/reducer.js b/ReactNativeClient/lib/reducer.js index f62f701fe..f59b60562 100644 --- a/ReactNativeClient/lib/reducer.js +++ b/ReactNativeClient/lib/reducer.js @@ -23,12 +23,6 @@ const defaultState = { searchQuery: '', settings: {}, appState: 'starting', - sideMenuOpenPercent: 0, - route: { - type: 'NAV_GO', - routeName: 'Welcome', - params: {}, - }, windowContentSize: { width: 0, height: 0 }, }; @@ -107,7 +101,6 @@ function defaultNotesParentType(state, exclusion) { const reducer = (state = defaultState, action) => { let newState = state; - let historyGoingBack = false; try { switch (action.type) { @@ -305,12 +298,6 @@ const reducer = (state = defaultState, action) => { newState.appState = action.state; break; - case 'WINDOW_CONTENT_SIZE_SET': - - newState = Object.assign({}, state); - newState.windowContentSize = action.size; - break; - } } catch (error) { error.message = 'In reducer: ' + error.message + ' Action: ' + JSON.stringify(action); diff --git a/ReactNativeClient/root.js b/ReactNativeClient/root.js index 7fce22215..0af0de6e9 100644 --- a/ReactNativeClient/root.js +++ b/ReactNativeClient/root.js @@ -74,7 +74,16 @@ function historyCanGoBackTo(route) { return true; } -const appReducer = (state = defaultState, action) => { +const appDefaultState = Object.assign({}, defaultState, { + sideMenuOpenPercent: 0, + route: { + type: 'NAV_GO', + routeName: 'Welcome', + params: {}, + }, +}); + +const appReducer = (state = appDefaultState, action) => { let newState = state; let historyGoingBack = false;