Merge pull request #312 from MetaMask/svg-notif

initial svg notifications
feature/default_network_editable
kumavis 8 years ago committed by GitHub
commit ac2269b16e
  1. 8
      .eslintrc
  2. 104
      app/scripts/lib/notifications.js
  3. 31
      test/unit/lib/icon-factory-test.js
  4. 2
      test/unit/linting_test.js
  5. 1
      ui/app/accounts/account-list-item.js
  6. 4
      ui/app/accounts/index.js
  7. 50
      ui/app/components/account-panel.js
  8. 7
      ui/app/components/identicon.js
  9. 54
      ui/app/components/panel.js
  10. 32
      ui/app/components/pending-tx.js
  11. 55
      ui/lib/icon-factory.js

@ -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 }],

@ -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 = `
<svg xmlns="http://www.w3.org/2000/svg" width="450" height="300">
<foreignObject x="0" y="0" width="100%" height="100%">
<body xmlns="http://www.w3.org/1999/xhtml" height="100%">{{content}}</body>
</foreignObject>
</svg>
`
return wrapperSource.split('{{content}}').join(content)
}

@ -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)
})
})

@ -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)

@ -33,6 +33,7 @@ NewComponent.prototype.render = function () {
this.pendingOrNot(),
h(Identicon, {
address: identity.address,
imageify: true,
}),
]),

@ -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,

@ -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) {

@ -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)
}

@ -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,
])
)
}

@ -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'),
])
)
}

@ -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) {
var numericRepresentation = jsNumberForAddress(address)
var identicon = this.jazzicon(diameter, numericRepresentation)
// returns img dom element
IconFactory.prototype.generateIdenticonImg = function (address, diameter) {
var identicon = this.generateIdenticonSvg(address, diameter)
var identiconSrc = identicon.innerHTML
var dataUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc)
return dataUri
var dataUri = toDataUri(identiconSrc)
var img = document.createElement('img')
img.src = dataUri
return img
}
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
}
// 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
}
IconFactory.prototype.isCached = function (address, diameter) {
return address in this.cache && diameter in this.cache[address]
// creates a new identicon
IconFactory.prototype.generateNewIdenticon = function (address, diameter) {
var numericRepresentation = jsNumberForAddress(address)
var identicon = this.jazzicon(diameter, numericRepresentation)
return identicon
}
// 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)
}
Loading…
Cancel
Save