From b0f1665f11120183fb21bdaf31231d077ad96cbb Mon Sep 17 00:00:00 2001 From: Nicolas Gotchac Date: Sat, 10 Dec 2016 23:55:36 +0100 Subject: [PATCH] Notify user on transaction received (#3782) * Notify user on new transaction #2556 * Add routing to account on notification click * Timeout of notif set to 20s --- js/package.json | 1 + js/src/index.js | 2 +- js/src/redux/middleware.js | 6 +- js/src/redux/providers/balancesActions.js | 95 +++++++++++++++++++---- js/src/redux/providers/balancesReducer.js | 35 +-------- js/src/redux/providers/index.js | 6 +- js/src/redux/reducers.js | 7 +- js/src/redux/store.js | 4 +- js/src/util/notifications.js | 45 +++++++++++ 9 files changed, 142 insertions(+), 59 deletions(-) create mode 100644 js/src/util/notifications.js diff --git a/js/package.json b/js/package.json index 0618f9d2d..011ecc897 100644 --- a/js/package.json +++ b/js/package.json @@ -146,6 +146,7 @@ "mobx-react-devtools": "4.2.10", "moment": "2.17.0", "phoneformat.js": "1.0.3", + "push.js": "0.0.11", "qs": "6.3.0", "react": "15.4.1", "react-ace": "4.1.0", diff --git a/js/src/index.js b/js/src/index.js index 6938a46f8..46d6c9c74 100644 --- a/js/src/index.js +++ b/js/src/index.js @@ -67,7 +67,7 @@ if (window.location.hash && window.location.hash.indexOf(AUTH_HASH) === 0) { const api = new SecureApi(`ws://${parityUrl}`, token); ContractInstances.create(api); -const store = initStore(api); +const store = initStore(api, hashHistory); store.dispatch({ type: 'initAll', api }); store.dispatch(setApi(api)); diff --git a/js/src/redux/middleware.js b/js/src/redux/middleware.js index bb11cf32f..14bc9b0a6 100644 --- a/js/src/redux/middleware.js +++ b/js/src/redux/middleware.js @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Parity. If not, see . import thunk from 'redux-thunk'; +import { routerMiddleware } from 'react-router-redux'; import ErrorsMiddleware from '~/ui/Errors/middleware'; import SettingsMiddleware from '~/views/Settings/middleware'; @@ -22,12 +23,13 @@ import SignerMiddleware from './providers/signerMiddleware'; import statusMiddleware from '~/views/Status/middleware'; import CertificationsMiddleware from './providers/certifications/middleware'; -export default function (api) { +export default function (api, browserHistory) { const errors = new ErrorsMiddleware(); const signer = new SignerMiddleware(api); const settings = new SettingsMiddleware(); const status = statusMiddleware(); const certifications = new CertificationsMiddleware(); + const routeMiddleware = routerMiddleware(browserHistory); const middleware = [ settings.toMiddleware(), @@ -36,5 +38,5 @@ export default function (api) { certifications.toMiddleware() ]; - return middleware.concat(status, thunk); + return middleware.concat(status, routeMiddleware, thunk); } diff --git a/js/src/redux/providers/balancesActions.js b/js/src/redux/providers/balancesActions.js index 65f831008..f8cfb2c1e 100644 --- a/js/src/redux/providers/balancesActions.js +++ b/js/src/redux/providers/balancesActions.js @@ -15,11 +15,14 @@ // along with Parity. If not, see . import { range, uniq, isEqual } from 'lodash'; +import BigNumber from 'bignumber.js'; +import { push } from 'react-router-redux'; import { hashToImageUrl } from './imagesReducer'; import { setAddressImage } from './imagesActions'; import * as ABIS from '~/contracts/abi'; +import { notifyTransaction } from '~/util/notifications'; import imagesEthereum from '../../../assets/images/contracts/ethereum-black-64x64.png'; const ETH = { @@ -28,7 +31,64 @@ const ETH = { image: imagesEthereum }; -export function setBalances (balances) { +function setBalances (_balances) { + return (dispatch, getState) => { + const state = getState(); + + const accounts = state.personal.accounts; + const nextBalances = _balances; + const prevBalances = state.balances.balances; + const balances = { ...prevBalances }; + + Object.keys(nextBalances).forEach((address) => { + if (!balances[address]) { + balances[address] = Object.assign({}, nextBalances[address]); + return; + } + + const balance = Object.assign({}, balances[address]); + const { tokens, txCount = balance.txCount } = nextBalances[address]; + const nextTokens = [].concat(balance.tokens); + + tokens.forEach((t) => { + const { token, value } = t; + const { tag } = token; + + const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); + + if (tokenIndex === -1) { + nextTokens.push({ + token, + value + }); + } else { + const oldValue = nextTokens[tokenIndex].value; + + // If received a token/eth (old value < new value), notify + if (oldValue.lt(value) && accounts[address]) { + const account = accounts[address]; + const txValue = value.minus(oldValue); + + const redirectToAccount = () => { + const route = `/account/${account.address}`; + dispatch(push(route)); + }; + + notifyTransaction(account, token, txValue, redirectToAccount); + } + + nextTokens[tokenIndex] = { token, value }; + } + }); + + balances[address] = { txCount: txCount || new BigNumber(0), tokens: nextTokens }; + }); + + dispatch(_setBalances(balances)); + }; +} + +function _setBalances (balances) { return { type: 'setBalances', balances @@ -123,14 +183,14 @@ export function fetchBalances (_addresses) { const fullFetch = addresses.length === 1; - const fetchedAddresses = uniq(addresses.concat(Object.keys(accounts))); + const addressesToFetch = uniq(addresses.concat(Object.keys(accounts))); return Promise - .all(fetchedAddresses.map((addr) => fetchAccount(addr, api, fullFetch))) + .all(addressesToFetch.map((addr) => fetchAccount(addr, api, fullFetch))) .then((accountsBalances) => { const balances = {}; - fetchedAddresses.forEach((addr, idx) => { + addressesToFetch.forEach((addr, idx) => { balances[addr] = accountsBalances[idx]; }); @@ -146,10 +206,12 @@ export function fetchBalances (_addresses) { export function updateTokensFilter (_addresses, _tokens) { return (dispatch, getState) => { const { api, balances, personal } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; const { tokensFilter } = balances; - const addresses = uniq(_addresses || visibleAccounts || []).sort(); + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = uniq(_addresses || addressesToFetch || []).sort(); + const tokens = _tokens || Object.values(balances.tokens) || []; const tokenAddresses = tokens.map((t) => t.address).sort(); @@ -221,8 +283,10 @@ export function updateTokensFilter (_addresses, _tokens) { export function queryTokensFilter (tokensFilter) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; + const visibleAddresses = visibleAccounts.map((a) => a.toLowerCase()); + const addressesToFetch = uniq(visibleAddresses.concat(Object.keys(accounts))); Promise .all([ @@ -237,18 +301,16 @@ export function queryTokensFilter (tokensFilter) { .concat(logsTo) .forEach((log) => { const tokenAddress = log.address; + const fromAddress = '0x' + log.topics[1].slice(-40); const toAddress = '0x' + log.topics[2].slice(-40); - const fromIdx = visibleAddresses.indexOf(fromAddress); - const toIdx = visibleAddresses.indexOf(toAddress); - - if (fromIdx > -1) { - addresses.push(visibleAccounts[fromIdx]); + if (addressesToFetch.includes(fromAddress)) { + addresses.push(fromAddress); } - if (toIdx > -1) { - addresses.push(visibleAccounts[toIdx]); + if (addressesToFetch.includes(toAddress)) { + addresses.push(toAddress); } tokenAddresses.push(tokenAddress); @@ -269,9 +331,10 @@ export function queryTokensFilter (tokensFilter) { export function fetchTokensBalances (_addresses = null, _tokens = null) { return (dispatch, getState) => { const { api, personal, balances } = getState(); - const { visibleAccounts } = personal; + const { visibleAccounts, accounts } = personal; - const addresses = _addresses || visibleAccounts; + const addressesToFetch = uniq(visibleAccounts.concat(Object.keys(accounts))); + const addresses = _addresses || addressesToFetch; const tokens = _tokens || Object.values(balances.tokens); if (addresses.length === 0) { diff --git a/js/src/redux/providers/balancesReducer.js b/js/src/redux/providers/balancesReducer.js index 01923a4f4..4b6950498 100644 --- a/js/src/redux/providers/balancesReducer.js +++ b/js/src/redux/providers/balancesReducer.js @@ -15,7 +15,6 @@ // along with Parity. If not, see . import { handleActions } from 'redux-actions'; -import BigNumber from 'bignumber.js'; const initialState = { balances: {}, @@ -26,39 +25,7 @@ const initialState = { export default handleActions({ setBalances (state, action) { - const nextBalances = action.balances; - const prevBalances = state.balances; - const balances = { ...prevBalances }; - - Object.keys(nextBalances).forEach((address) => { - if (!balances[address]) { - balances[address] = Object.assign({}, nextBalances[address]); - return; - } - - const balance = Object.assign({}, balances[address]); - const { tokens, txCount = balance.txCount } = nextBalances[address]; - const nextTokens = [].concat(balance.tokens); - - tokens.forEach((t) => { - const { token, value } = t; - const { tag } = token; - - const tokenIndex = nextTokens.findIndex((tok) => tok.token.tag === tag); - - if (tokenIndex === -1) { - nextTokens.push({ - token, - value - }); - } else { - nextTokens[tokenIndex] = { token, value }; - } - }); - - balances[address] = Object.assign({}, { txCount: txCount || new BigNumber(0), tokens: nextTokens }); - }); - + const { balances } = action; return Object.assign({}, state, { balances }); }, diff --git a/js/src/redux/providers/index.js b/js/src/redux/providers/index.js index 563378caa..a90d8b62c 100644 --- a/js/src/redux/providers/index.js +++ b/js/src/redux/providers/index.js @@ -21,11 +21,11 @@ export Status from './status'; export apiReducer from './apiReducer'; export balancesReducer from './balancesReducer'; +export blockchainReducer from './blockchainReducer'; +export compilerReducer from './compilerReducer'; export imagesReducer from './imagesReducer'; export personalReducer from './personalReducer'; export signerReducer from './signerReducer'; -export statusReducer from './statusReducer'; -export blockchainReducer from './blockchainReducer'; -export compilerReducer from './compilerReducer'; export snackbarReducer from './snackbarReducer'; +export statusReducer from './statusReducer'; export walletReducer from './walletReducer'; diff --git a/js/src/redux/reducers.js b/js/src/redux/reducers.js index 92388df65..642dfe403 100644 --- a/js/src/redux/reducers.js +++ b/js/src/redux/reducers.js @@ -17,7 +17,12 @@ import { combineReducers } from 'redux'; import { routerReducer } from 'react-router-redux'; -import { apiReducer, balancesReducer, blockchainReducer, compilerReducer, imagesReducer, personalReducer, signerReducer, statusReducer as nodeStatusReducer, snackbarReducer, walletReducer } from './providers'; +import { + apiReducer, balancesReducer, blockchainReducer, + compilerReducer, imagesReducer, personalReducer, + signerReducer, statusReducer as nodeStatusReducer, + snackbarReducer, walletReducer +} from './providers'; import certificationsReducer from './providers/certifications/reducer'; import errorReducer from '~/ui/Errors/reducers'; diff --git a/js/src/redux/store.js b/js/src/redux/store.js index 2ff50ea53..1d62f9ea5 100644 --- a/js/src/redux/store.js +++ b/js/src/redux/store.js @@ -32,9 +32,9 @@ const storeCreation = window.devToolsExtension ? window.devToolsExtension()(createStore) : createStore; -export default function (api) { +export default function (api, browserHistory) { const reducers = initReducers(); - const middleware = initMiddleware(api); + const middleware = initMiddleware(api, browserHistory); const store = applyMiddleware(...middleware)(storeCreation)(reducers); new BalancesProvider(store, api).start(); diff --git a/js/src/util/notifications.js b/js/src/util/notifications.js new file mode 100644 index 000000000..479448234 --- /dev/null +++ b/js/src/util/notifications.js @@ -0,0 +1,45 @@ +// Copyright 2015, 2016 Ethcore (UK) Ltd. +// This file is part of Parity. + +// Parity is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// Parity is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with Parity. If not, see . + +import Push from 'push.js'; +import BigNumber from 'bignumber.js'; +import { noop } from 'lodash'; + +import { fromWei } from '~/api/util/wei'; + +import ethereumIcon from '~/../assets/images/contracts/ethereum-black-64x64.png'; +import unkownIcon from '~/../assets/images/contracts/unknown-64x64.png'; + +export function notifyTransaction (account, token, _value, onClick) { + const name = account.name || account.address; + const value = token.tag.toLowerCase() === 'eth' + ? fromWei(_value) + : _value.div(new BigNumber(token.format || 1)); + + const icon = token.tag.toLowerCase() === 'eth' + ? ethereumIcon + : (token.image || unkownIcon); + + Push.create(`${name}`, { + body: `You just received ${value.toFormat()} ${token.tag.toUpperCase()}`, + icon: { + x16: icon, + x32: icon + }, + timeout: 20000, + onClick: onClick || noop + }); +}