diff --git a/.eslintrc b/.eslintrc index 635c47a95..e64b3e5be 100644 --- a/.eslintrc +++ b/.eslintrc @@ -42,7 +42,7 @@ "constructor-super": 2, "curly": [2, "multi-line"], "dot-location": [2, "property"], - "eol-last": 2, + "eol-last": 1, "eqeqeq": [2, "allow-null"], "generator-star-spacing": [2, { "before": true, "after": true }], "handle-callback-err": [2, "^(err|error)$" ], @@ -87,7 +87,7 @@ "no-mixed-spaces-and-tabs": 2, "no-multi-spaces": 2, "no-multi-str": 2, - "no-multiple-empty-lines": [2, { "max": 1 }], + "no-multiple-empty-lines": [1, { "max": 2 }], "no-native-reassign": 2, "no-negated-in-lhs": 2, "no-new": 2, @@ -112,7 +112,7 @@ "no-sparse-arrays": 2, "no-this-before-super": 2, "no-throw-literal": 2, - "no-trailing-spaces": 2, + "no-trailing-spaces": 1, "no-undef": 2, "no-undef-init": 2, "no-unexpected-multiline": 2, @@ -129,7 +129,7 @@ "no-with": 2, "one-var": [2, { "initialized": "never" }], "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], - "padded-blocks": [2, "never"], + "padded-blocks": [1, "never"], "quotes": [2, "single", "avoid-escape"], "semi": [2, "never"], "semi-spacing": [2, { "before": false, "after": true }], diff --git a/app/scripts/lib/notifications.js b/app/scripts/lib/notifications.js index af2dc2054..5762fd26b 100644 --- a/app/scripts/lib/notifications.js +++ b/app/scripts/lib/notifications.js @@ -1,5 +1,11 @@ const createId = require('hat') +const unmountComponentAtNode = require('react-dom').unmountComponentAtNode +const findDOMNode = require('react-dom').findDOMNode +const render = require('react-dom').render +const h = require('react-hyperscript') const uiUtils = require('../../../ui/app/util') +const renderPendingTx = require('../../../ui/app/components/pending-tx').prototype.renderGeneric +const MetaMaskUiCss = require('../../../ui/css') var notificationHandlers = {} module.exports = { @@ -49,31 +55,32 @@ function createUnlockRequestNotification (opts) { function createTxNotification (opts) { // guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236 if (!chrome.notifications) return console.error('Chrome notifications API missing...') - var message = [ - 'Submitted by ' + opts.txParams.origin, - 'to: ' + uiUtils.addressSummary(opts.txParams.to), - 'from: ' + uiUtils.addressSummary(opts.txParams.from), - 'value: ' + uiUtils.formatBalance(opts.txParams.value), - 'data: ' + uiUtils.dataSize(opts.txParams.data), - ].join('\n') - var id = createId() - chrome.notifications.create(id, { - type: 'basic', - requireInteraction: true, - iconUrl: '/images/icon-128.png', - title: opts.title, - message: message, - buttons: [{ - title: 'confirm', - }, { - title: 'cancel', - }], + renderTransactionNotificationSVG(opts, function(err, source){ + if (err) throw err + + var imageUrl = 'data:image/svg+xml;utf8,' + encodeURIComponent(source) + + var id = createId() + chrome.notifications.create(id, { + type: 'image', + // requireInteraction: true, + iconUrl: '/images/icon-128.png', + imageUrl: imageUrl, + title: opts.title, + message: '', + buttons: [{ + title: 'confirm', + }, { + title: 'cancel', + }], + }) + notificationHandlers[id] = { + confirm: opts.confirm, + cancel: opts.cancel, + } + }) - notificationHandlers[id] = { - confirm: opts.confirm, - cancel: opts.cancel, - } } function createMsgNotification (opts) { @@ -103,3 +110,54 @@ function createMsgNotification (opts) { cancel: opts.cancel, } } + +function renderTransactionNotificationSVG(opts, cb){ + var state = { + nonInteractive: true, + inlineIdenticons: true, + txData: { + txParams: opts.txParams, + time: (new Date()).getTime(), + }, + identities: { + + }, + accounts: { + + }, + } + + var container = document.createElement('div') + var confirmView = h('div.app-primary', { + style: { + width: '450px', + height: '300px', + padding: '16px', + // background: '#F7F7F7', + background: 'white', + }, + }, [ + h('style', MetaMaskUiCss()), + renderPendingTx(h, state), + ]) + + render(confirmView, container, function ready(){ + var rootElement = findDOMNode(this) + var viewSource = rootElement.outerHTML + unmountComponentAtNode(container) + var svgSource = svgWrapper(viewSource) + // insert content into svg wrapper + cb(null, svgSource) + }) +} + +function svgWrapper(content){ + var wrapperSource = ` + + + {{content}} + + + ` + return wrapperSource.split('{{content}}').join(content) +} \ No newline at end of file diff --git a/test/unit/lib/icon-factory-test.js b/test/unit/lib/icon-factory-test.js deleted file mode 100644 index 2f24d408c..000000000 --- a/test/unit/lib/icon-factory-test.js +++ /dev/null @@ -1,31 +0,0 @@ -const assert = require('assert') -const sinon = require('sinon') - -const path = require('path') -const IconFactoryGen = require(path.join(__dirname, '..', '..', '..', 'ui', 'lib', 'icon-factory.js')) - -describe('icon-factory', function() { - let iconFactory, address, diameter - - beforeEach(function() { - iconFactory = IconFactoryGen((d,n) => 'stubicon') - address = '0x012345671234567890' - diameter = 50 - }) - - it('should return a data-uri string for any address and diameter', function() { - const output = iconFactory.iconForAddress(address, diameter) - assert.ok(output.indexOf('data:image/svg') === 0) - assert.equal(output, iconFactory.cache[address][diameter]) - }) - - it('should default to cache first', function() { - const testOutput = 'foo' - const mockSizeCache = {} - mockSizeCache[diameter] = testOutput - iconFactory.cache[address] = mockSizeCache - - const output = iconFactory.iconForAddress(address, diameter) - assert.equal(output, testOutput) - }) -}) diff --git a/test/unit/linting_test.js b/test/unit/linting_test.js index 27b4b2b1e..75d90652d 100644 --- a/test/unit/linting_test.js +++ b/test/unit/linting_test.js @@ -3,7 +3,7 @@ const lint = require('mocha-eslint'); const lintPaths = ['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js'] const lintOptions = { - strict: true, + strict: false, } lint(lintPaths, lintOptions) \ No newline at end of file diff --git a/ui/app/accounts/account-panel.js b/ui/app/accounts/account-list-item.js similarity index 98% rename from ui/app/accounts/account-panel.js rename to ui/app/accounts/account-list-item.js index af3d0d347..b42de182e 100644 --- a/ui/app/accounts/account-panel.js +++ b/ui/app/accounts/account-list-item.js @@ -33,6 +33,7 @@ NewComponent.prototype.render = function () { this.pendingOrNot(), h(Identicon, { address: identity.address, + imageify: true, }), ]), diff --git a/ui/app/accounts/index.js b/ui/app/accounts/index.js index 775df400b..f7ae5c53e 100644 --- a/ui/app/accounts/index.js +++ b/ui/app/accounts/index.js @@ -5,7 +5,7 @@ const connect = require('react-redux').connect const actions = require('../actions') const valuesFor = require('../util').valuesFor const findDOMNode = require('react-dom').findDOMNode -const AccountPanel = require('./account-panel') +const AccountListItem = require('./account-list-item') module.exports = connect(mapStateToProps)(AccountsScreen) @@ -74,7 +74,7 @@ AccountsScreen.prototype.render = function () { } }) - return h(AccountPanel, { + return h(AccountListItem, { key: `acct-panel-${identity.address}`, identity, selectedAddress: this.props.selectedAddress, diff --git a/ui/app/components/account-panel.js b/ui/app/components/account-panel.js index 112b897d5..b98a8cb45 100644 --- a/ui/app/components/account-panel.js +++ b/ui/app/components/account-panel.js @@ -1,13 +1,13 @@ const inherits = require('util').inherits const Component = require('react').Component const h = require('react-hyperscript') +const Identicon = require('./identicon') const formatBalance = require('../util').formatBalance const addressSummary = require('../util').addressSummary -const Panel = require('./panel') - module.exports = AccountPanel + inherits(AccountPanel, Component) function AccountPanel () { Component.call(this) @@ -19,13 +19,8 @@ AccountPanel.prototype.render = function () { var account = state.account || {} var isFauceting = state.isFauceting - var panelOpts = { + var panelState = { key: `accountPanel${identity.address}`, - onClick: (event) => { - if (state.onShowDetail) { - state.onShowDetail(identity.address, event) - } - }, identiconKey: identity.address, identiconLabel: identity.name, attributes: [ @@ -37,10 +32,41 @@ AccountPanel.prototype.render = function () { ], } - return h(Panel, panelOpts, - !state.onShowDetail ? null : h('.arrow-right.cursor-pointer', [ - h('i.fa.fa-chevron-right.fa-lg'), - ])) + return ( + + h('.identity-panel.flex-row.flex-space-between', { + style: { + flex: '1 0 auto', + cursor: panelState.onClick ? 'pointer' : undefined, + }, + onClick: panelState.onClick, + }, [ + + // account identicon + h('.identicon-wrapper.flex-column.select-none', [ + h(Identicon, { + address: panelState.identiconKey, + imageify: !state.inlineIdenticons, + }), + h('span.font-small', panelState.identiconLabel), + ]), + + // account address, balance + h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ + + panelState.attributes.map((attr) => { + return h('.flex-row.flex-space-between', { + key: '' + Math.round(Math.random() * 1000000), + }, [ + h('label.font-small.no-select', attr.key), + h('span.font-small', attr.value), + ]) + }), + ]), + + ]) + + ) } function balanceOrFaucetingIndication (account, isFauceting) { diff --git a/ui/app/components/identicon.js b/ui/app/components/identicon.js index 5fe07ce7a..c17bd5122 100644 --- a/ui/app/components/identicon.js +++ b/ui/app/components/identicon.js @@ -39,12 +39,9 @@ IdenticonComponent.prototype.componentDidMount = function () { if (!address) return var container = findDOMNode(this) - var diameter = state.diameter || this.defaultDiameter - var dataUri = iconFactory.iconForAddress(address, diameter) - - var img = document.createElement('img') - img.src = dataUri + var imageify = state.imageify + var img = iconFactory.iconForAddress(address, diameter, imageify) container.appendChild(img) } diff --git a/ui/app/components/panel.js b/ui/app/components/panel.js deleted file mode 100644 index cbdc82982..000000000 --- a/ui/app/components/panel.js +++ /dev/null @@ -1,54 +0,0 @@ -const inherits = require('util').inherits -const Component = require('react').Component -const h = require('react-hyperscript') -const Identicon = require('./identicon') - -module.exports = Panel - -inherits(Panel, Component) -function Panel () { - Component.call(this) -} - -Panel.prototype.render = function () { - var state = this.props - - var style = { - flex: '1 0 auto', - } - - if (state.onClick) style.cursor = 'pointer' - - return ( - h('.identity-panel.flex-row.flex-space-between', { - style, - onClick: state.onClick, - }, [ - - // account identicon - h('.identicon-wrapper.flex-column.select-none', [ - h(Identicon, { - address: state.identiconKey, - }), - h('span.font-small', state.identiconLabel), - ]), - - // account address, balance - h('.identity-data.flex-column.flex-justify-center.flex-grow.select-none', [ - - state.attributes.map((attr) => { - return h('.flex-row.flex-space-between', { - key: '' + Math.round(Math.random() * 1000000), - }, [ - h('label.font-small.no-select', attr.key), - h('span.font-small', attr.value), - ]) - }), - ]), - - // outlet for inserting additional stuff - state.children, - ]) - ) -} - diff --git a/ui/app/components/pending-tx.js b/ui/app/components/pending-tx.js index 1835239e5..484046827 100644 --- a/ui/app/components/pending-tx.js +++ b/ui/app/components/pending-tx.js @@ -16,6 +16,10 @@ function PendingTx () { PendingTx.prototype.render = function () { var state = this.props + return this.renderGeneric(h, state) +} + +PendingTx.prototype.renderGeneric = function (h, state) { var txData = state.txData var txParams = txData.txParams || {} @@ -24,6 +28,7 @@ PendingTx.prototype.render = function () { var account = state.accounts[address] || { address: address } return ( + h('.transaction', { key: txData.id, }, [ @@ -40,6 +45,7 @@ PendingTx.prototype.render = function () { showFullAddress: true, identity: identity, account: account, + inlineIdenticons: state.inlineIdenticons, }), // tx data @@ -62,15 +68,25 @@ PendingTx.prototype.render = function () { ]), // send + cancel - h('.flex-row.flex-space-around', [ - h('button', { - onClick: state.cancelTransaction, - }, 'Cancel'), - h('button', { - onClick: state.sendTransaction, - }, 'Send'), - ]), + state.nonInteractive ? null : actionButtons(state), + ]) + ) + } +function actionButtons(state){ + return ( + + h('.flex-row.flex-space-around', [ + h('button', { + onClick: state.cancelTransaction, + }, 'Cancel'), + h('button', { + onClick: state.sendTransaction, + }, 'Send'), + ]) + + ) +} \ No newline at end of file diff --git a/ui/lib/icon-factory.js b/ui/lib/icon-factory.js index 1f7cca859..a30041114 100644 --- a/ui/lib/icon-factory.js +++ b/ui/lib/icon-factory.js @@ -12,42 +12,49 @@ function IconFactory (jazzicon) { this.cache = {} } -IconFactory.prototype.iconForAddress = function (address, diameter) { - if (this.isCached(address, diameter)) { - return this.cache[address][diameter] +IconFactory.prototype.iconForAddress = function (address, diameter, imageify) { + if (imageify) { + return this.generateIdenticonImg(address, diameter) + } else { + return this.generateIdenticonSvg(address, diameter) } - - const dataUri = this.generateNewUri(address, diameter) - this.cacheIcon(address, diameter, dataUri) - return dataUri } -IconFactory.prototype.generateNewUri = function (address, diameter) { +// returns img dom element +IconFactory.prototype.generateIdenticonImg = function (address, diameter) { + var identicon = this.generateIdenticonSvg(address, diameter) + var identiconSrc = identicon.innerHTML + var dataUri = toDataUri(identiconSrc) + var img = document.createElement('img') + img.src = dataUri + return img +} + +// returns svg dom element +IconFactory.prototype.generateIdenticonSvg = function (address, diameter) { + var cacheId = `${address}:${diameter}` + // check cache, lazily generate and populate cache + var identicon = this.cache[cacheId] || (this.cache[cacheId] = this.generateNewIdenticon(address, diameter)) + // create a clean copy so you can modify it + var cleanCopy = identicon.cloneNode(true) + return cleanCopy +} + +// creates a new identicon +IconFactory.prototype.generateNewIdenticon = function (address, diameter) { var numericRepresentation = jsNumberForAddress(address) var identicon = this.jazzicon(diameter, numericRepresentation) - var identiconSrc = identicon.innerHTML - var dataUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc) - return dataUri + return identicon } -IconFactory.prototype.cacheIcon = function (address, diameter, icon) { - if (!(address in this.cache)) { - var sizeCache = {} - sizeCache[diameter] = icon - this.cache[address] = sizeCache - return sizeCache - } else { - this.cache[address][diameter] = icon - return icon - } -} - -IconFactory.prototype.isCached = function (address, diameter) { - return address in this.cache && diameter in this.cache[address] -} +// util function jsNumberForAddress (address) { var addr = address.slice(2, 10) var seed = parseInt(addr, 16) return seed } + +function toDataUri(identiconSrc){ + return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc) +} \ No newline at end of file