diff --git a/app/assets/icons/app_icon.png b/app/assets/icons/app_icon.png new file mode 100644 index 000000000..f52c1a7aa Binary files /dev/null and b/app/assets/icons/app_icon.png differ diff --git a/app/components/localNode/LeftPane.js b/app/components/localNode/LeftPane.js index 1dc616582..f4b608403 100644 --- a/app/components/localNode/LeftPane.js +++ b/app/components/localNode/LeftPane.js @@ -52,9 +52,10 @@ class LeftPane extends Component { componentDidMount() { const { getLocalNodeSetupProgress } = this.props; this.checkInitStatus(); + getLocalNodeSetupProgress(); this.timer = setInterval(() => { getLocalNodeSetupProgress(); - }, 10000); + }, 30000); } componentDidUpdate() { diff --git a/app/components/localNode/LocalNodeLog.js b/app/components/localNode/LocalNodeLog.js index 0d157d985..e5c3e6540 100644 --- a/app/components/localNode/LocalNodeLog.js +++ b/app/components/localNode/LocalNodeLog.js @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import styled from 'styled-components'; import { smColors } from '/vars'; -// TODO: remove stab +// TODO: remove stub const getRandomNumber = (maxValue: number) => Math.floor(Math.random() * Math.floor(maxValue)); const getTimestamp = () => new Date().toUTCString(); const generateLogEntry = (i: number) => { @@ -56,6 +56,7 @@ const LogRow = styled.div` align-items: center; `; +// $FlowStyledIssue const LogEntry = styled.div` font-size: 16px; line-height: 30px; @@ -87,12 +88,12 @@ class LocalNodeLog extends Component<{}, { log: LogRecord[] }> { ); } - // TODO: remove stab + // TODO: remove stub componentDidMount() { this.generateLog(); } - // TODO: remove stab + // TODO: remove stub componentWillUnmount() { this.timer && clearInterval(this.timer); } @@ -104,7 +105,7 @@ class LocalNodeLog extends Component<{}, { log: LogRecord[] }> { )); - // TODO: remove stab + // TODO: remove stub generateLog = () => { let i = 0; this.timer = setInterval(() => { diff --git a/app/infra/httpService/httpService.js b/app/infra/httpService/httpService.js index e0fd2ff9e..89997cf94 100644 --- a/app/infra/httpService/httpService.js +++ b/app/infra/httpService/httpService.js @@ -29,7 +29,10 @@ class HttpService { ipcRenderer.send(ipcConsts.GET_INIT_PROGRESS); return new Promise((resolve: Function, reject: Function) => { ipcRenderer.once(ipcConsts.GET_INIT_PROGRESS_SUCCESS, (event, response) => { - resolve(response); + const timer = setTimeout(() => { + resolve(response); + clearTimeout(timer); + }, 10000); }); ipcRenderer.once(ipcConsts.GET_INIT_PROGRESS_FAILURE, (event, args) => { reject(args); diff --git a/app/infra/notificationsService/index.js b/app/infra/notificationsService/index.js new file mode 100644 index 000000000..61f3156db --- /dev/null +++ b/app/infra/notificationsService/index.js @@ -0,0 +1 @@ +export { default as notificationsService } from './notificationsService'; // eslint-disable-line import/prefer-default-export diff --git a/app/infra/notificationsService/notificationsService.js b/app/infra/notificationsService/notificationsService.js new file mode 100644 index 000000000..a09faba6c --- /dev/null +++ b/app/infra/notificationsService/notificationsService.js @@ -0,0 +1,34 @@ +import path from 'path'; +import { ipcRenderer } from 'electron'; +import { ipcConsts } from '/vars'; + +// @flow +class NotificationsService { + static notify = ({ title, notification, callback }: { title: string, notification: string, callback: () => void }) => { + NotificationsService.getNotificationAllowedStatus().then(({ isNotificationAllowed }: { isNotificationAllowed: boolean }) => { + if (isNotificationAllowed) { + const notificationOptions: any = { + body: notification, + icon: path.join(__dirname, '..', 'app', 'assets', 'icons', 'app_icon.png') + }; + const desktopNotification = new Notification(title || 'Alert', notificationOptions); + desktopNotification.onclick = () => { + ipcRenderer.send(ipcConsts.NOTIFICATION_CLICK); + callback && callback(); + }; + } + }); + }; + + static getNotificationAllowedStatus = async () => { + const isPermitted = await Notification.requestPermission(); + ipcRenderer.send(ipcConsts.CAN_NOTIFY); + return new Promise((resolve) => { + ipcRenderer.once(ipcConsts.CAN_NOTIFY_SUCCESS, (event, isInFocus) => { + resolve({ isNotificationAllowed: isPermitted && !isInFocus }); + }); + }); + }; +} + +export default NotificationsService; diff --git a/app/redux/localNode/reducer.js b/app/redux/localNode/reducer.js index 93d07d0de..df5edae0b 100644 --- a/app/redux/localNode/reducer.js +++ b/app/redux/localNode/reducer.js @@ -21,7 +21,8 @@ const initialState = { progress: null, totalEarnings: null, upcomingEarnings: null, - awardsAddress: null + awardsAddress: null, + pathToLocalNode: null }; const reducer = (state: any = initialState, action: Action) => { diff --git a/app/screens/main/Main.js b/app/screens/main/Main.js index a1e99737c..9cbad7da8 100644 --- a/app/screens/main/Main.js +++ b/app/screens/main/Main.js @@ -10,6 +10,9 @@ import type { SideMenuItem } from '/basicComponents'; import { menu1, menu2, menu3, menu4, menu5, menu6, menu7 } from '/assets/images'; import routes from '/routes'; import type { Account, Action } from '/types'; +import { notificationsService } from '/infra/notificationsService'; + +const completeValue = 80; // TODO: change to actual complete value const sideMenuItems: SideMenuItem[] = [ { @@ -73,7 +76,9 @@ type Props = { location: { pathname: string, hash: string }, accounts: Account[], resetNodeSettings: Action, - logout: Action + logout: Action, + // pathToLocalNode: string, + progress: number }; type State = { @@ -109,6 +114,17 @@ class Main extends Component { ); } + componentDidUpdate(prevProps: Props) { + const { progress, history } = this.props; + if (prevProps.progress !== progress && progress === completeValue) { + notificationsService.notify({ + title: 'Local Node', + notification: 'Your full node setup is complete! You are now participating in the Spacemesh network!', + callback: () => this.handleSideMenuPress({ index: 0 }) + }); + } + } + handleSideMenuPress = ({ index }: { index: number }) => { const { history, accounts, location } = this.props; const newPath: ?string = sideMenuItems[index].path; @@ -133,7 +149,8 @@ class Main extends Component { } const mapStateToProps = (state) => ({ - accounts: state.wallet.accounts + accounts: state.wallet.accounts, + progress: state.localNode.progress }); const mapDispatchToProps = { diff --git a/app/vars/ipcConsts.js b/app/vars/ipcConsts.js index 71e1491bc..39bb40258 100644 --- a/app/vars/ipcConsts.js +++ b/app/vars/ipcConsts.js @@ -28,6 +28,9 @@ const ipcConsts = { SET_NODE_IP: 'SET_NODE_IP', SET_NODE_IP_SUCCESS: 'SET_NODE_IP_SUCCESS', SET_NODE_IP_FAILURE: 'SET_NODE_IP_FAILURE', + CAN_NOTIFY: 'CAN_NOTIFY', + CAN_NOTIFY_SUCCESS: 'CAN_NOTIFY_SUCCESS', + NOTIFICATION_CLICK: 'NOTIFICATION_CLICK', // gRPC calls GET_BALANCE: 'GET_BALANCE', GET_BALANCE_SUCCESS: 'GET_BALANCE_SUCCESS', diff --git a/desktop/eventListners.js b/desktop/eventListners.js index 2840bbf39..bb169024a 100644 --- a/desktop/eventListners.js +++ b/desktop/eventListners.js @@ -84,6 +84,16 @@ const subscribeToEventListeners = ({ mainWindow }) => { ipcMain.on(ipcConsts.SET_NODE_IP, async (event, request) => { netService.setNodeIpAddress({ event, ...request }); }); + + ipcMain.on(ipcConsts.NOTIFICATION_CLICK, () => { + mainWindow.show(); + mainWindow.focus(); + }); + + ipcMain.on(ipcConsts.CAN_NOTIFY, (event) => { + const isInFocus = mainWindow.isFocused(); + event.sender.send(ipcConsts.CAN_NOTIFY_SUCCESS, isInFocus); + }); }; export { subscribeToEventListeners }; // eslint-disable-line import/prefer-default-export diff --git a/desktop/main.dev.js b/desktop/main.dev.js index 7e7a58770..af92de4fd 100644 --- a/desktop/main.dev.js +++ b/desktop/main.dev.js @@ -8,7 +8,7 @@ * When running `npm run build` or `npm run build-main`, this file is compiled to * `./desktop/main.prod.js` using webpack. This gives us some performance wins. */ -import { app, BrowserWindow } from 'electron'; +import { app, BrowserWindow, dialog } from 'electron'; import { autoUpdater } from 'electron-updater'; import log from 'electron-log'; import MenuBuilder from './menu'; @@ -41,9 +41,6 @@ const installExtensions = async () => { return Promise.all(extensions.map((name) => installer.default(installer[name], forceDownload))).catch(console.error); // eslint-disable-line no-console }; -// Add event listeners. -subscribeToEventListeners({ mainWindow }); - app.on('window-all-closed', () => { // Respect the OSX convention of having the application in memory even // after all windows have been closed @@ -64,6 +61,8 @@ const createWindow = () => { nodeIntegration: true } }); + // Add event listeners. + subscribeToEventListeners({ mainWindow }); }; app.on('ready', async () => { @@ -83,8 +82,26 @@ app.on('ready', async () => { mainWindow.focus(); }); - mainWindow.on('closed', () => { - mainWindow = null; + mainWindow.on('close', (event) => { + event.preventDefault(); + const options = { + title: 'Spacemesh', + message: 'Quit app or keep in background?', + buttons: ['Keep running in background', 'Quit'] + }; + dialog.showMessageBox(mainWindow, options, (response) => { + if (response === 0) { + mainWindow.hide(); + } + if (response === 1) { + mainWindow.destroy(); + mainWindow = null; + app.quit(); + } + if (process.platform !== 'darwin') { + app.dock.hide(); + } + }); }); const menuBuilder = new MenuBuilder(mainWindow); @@ -98,5 +115,8 @@ app.on('ready', async () => { app.on('activate', () => { if (mainWindow === null) { createWindow(); + } else if (mainWindow) { + mainWindow.show(); + mainWindow.focus(); } });