1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 09:52:26 +01:00

Merge pull request #312 from MetaMask/svg-notif

initial svg notifications
This commit is contained in:
kumavis 2016-06-23 17:55:10 -07:00 committed by GitHub
commit ac2269b16e
11 changed files with 186 additions and 166 deletions

View File

@ -42,7 +42,7 @@
"constructor-super": 2, "constructor-super": 2,
"curly": [2, "multi-line"], "curly": [2, "multi-line"],
"dot-location": [2, "property"], "dot-location": [2, "property"],
"eol-last": 2, "eol-last": 1,
"eqeqeq": [2, "allow-null"], "eqeqeq": [2, "allow-null"],
"generator-star-spacing": [2, { "before": true, "after": true }], "generator-star-spacing": [2, { "before": true, "after": true }],
"handle-callback-err": [2, "^(err|error)$" ], "handle-callback-err": [2, "^(err|error)$" ],
@ -87,7 +87,7 @@
"no-mixed-spaces-and-tabs": 2, "no-mixed-spaces-and-tabs": 2,
"no-multi-spaces": 2, "no-multi-spaces": 2,
"no-multi-str": 2, "no-multi-str": 2,
"no-multiple-empty-lines": [2, { "max": 1 }], "no-multiple-empty-lines": [1, { "max": 2 }],
"no-native-reassign": 2, "no-native-reassign": 2,
"no-negated-in-lhs": 2, "no-negated-in-lhs": 2,
"no-new": 2, "no-new": 2,
@ -112,7 +112,7 @@
"no-sparse-arrays": 2, "no-sparse-arrays": 2,
"no-this-before-super": 2, "no-this-before-super": 2,
"no-throw-literal": 2, "no-throw-literal": 2,
"no-trailing-spaces": 2, "no-trailing-spaces": 1,
"no-undef": 2, "no-undef": 2,
"no-undef-init": 2, "no-undef-init": 2,
"no-unexpected-multiline": 2, "no-unexpected-multiline": 2,
@ -129,7 +129,7 @@
"no-with": 2, "no-with": 2,
"one-var": [2, { "initialized": "never" }], "one-var": [2, { "initialized": "never" }],
"operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }],
"padded-blocks": [2, "never"], "padded-blocks": [1, "never"],
"quotes": [2, "single", "avoid-escape"], "quotes": [2, "single", "avoid-escape"],
"semi": [2, "never"], "semi": [2, "never"],
"semi-spacing": [2, { "before": false, "after": true }], "semi-spacing": [2, { "before": false, "after": true }],

View File

@ -1,5 +1,11 @@
const createId = require('hat') 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 uiUtils = require('../../../ui/app/util')
const renderPendingTx = require('../../../ui/app/components/pending-tx').prototype.renderGeneric
const MetaMaskUiCss = require('../../../ui/css')
var notificationHandlers = {} var notificationHandlers = {}
module.exports = { module.exports = {
@ -49,31 +55,32 @@ function createUnlockRequestNotification (opts) {
function createTxNotification (opts) { function createTxNotification (opts) {
// guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236 // guard for chrome bug https://github.com/MetaMask/metamask-plugin/issues/236
if (!chrome.notifications) return console.error('Chrome notifications API missing...') 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() renderTransactionNotificationSVG(opts, function(err, source){
chrome.notifications.create(id, { if (err) throw err
type: 'basic',
requireInteraction: true, var imageUrl = 'data:image/svg+xml;utf8,' + encodeURIComponent(source)
iconUrl: '/images/icon-128.png',
title: opts.title, var id = createId()
message: message, chrome.notifications.create(id, {
buttons: [{ type: 'image',
title: 'confirm', // requireInteraction: true,
}, { iconUrl: '/images/icon-128.png',
title: 'cancel', 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) { function createMsgNotification (opts) {
@ -103,3 +110,54 @@ function createMsgNotification (opts) {
cancel: opts.cancel, 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)
}

View File

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

View File

@ -3,7 +3,7 @@ const lint = require('mocha-eslint');
const lintPaths = ['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js'] const lintPaths = ['app/**/*.js', 'ui/**/*.js', '!node_modules/**', '!dist/**', '!docs/**', '!app/scripts/chromereload.js']
const lintOptions = { const lintOptions = {
strict: true, strict: false,
} }
lint(lintPaths, lintOptions) lint(lintPaths, lintOptions)

View File

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

View File

@ -5,7 +5,7 @@ const connect = require('react-redux').connect
const actions = require('../actions') const actions = require('../actions')
const valuesFor = require('../util').valuesFor const valuesFor = require('../util').valuesFor
const findDOMNode = require('react-dom').findDOMNode const findDOMNode = require('react-dom').findDOMNode
const AccountPanel = require('./account-panel') const AccountListItem = require('./account-list-item')
module.exports = connect(mapStateToProps)(AccountsScreen) module.exports = connect(mapStateToProps)(AccountsScreen)
@ -74,7 +74,7 @@ AccountsScreen.prototype.render = function () {
} }
}) })
return h(AccountPanel, { return h(AccountListItem, {
key: `acct-panel-${identity.address}`, key: `acct-panel-${identity.address}`,
identity, identity,
selectedAddress: this.props.selectedAddress, selectedAddress: this.props.selectedAddress,

View File

@ -1,13 +1,13 @@
const inherits = require('util').inherits const inherits = require('util').inherits
const Component = require('react').Component const Component = require('react').Component
const h = require('react-hyperscript') const h = require('react-hyperscript')
const Identicon = require('./identicon')
const formatBalance = require('../util').formatBalance const formatBalance = require('../util').formatBalance
const addressSummary = require('../util').addressSummary const addressSummary = require('../util').addressSummary
const Panel = require('./panel')
module.exports = AccountPanel module.exports = AccountPanel
inherits(AccountPanel, Component) inherits(AccountPanel, Component)
function AccountPanel () { function AccountPanel () {
Component.call(this) Component.call(this)
@ -19,13 +19,8 @@ AccountPanel.prototype.render = function () {
var account = state.account || {} var account = state.account || {}
var isFauceting = state.isFauceting var isFauceting = state.isFauceting
var panelOpts = { var panelState = {
key: `accountPanel${identity.address}`, key: `accountPanel${identity.address}`,
onClick: (event) => {
if (state.onShowDetail) {
state.onShowDetail(identity.address, event)
}
},
identiconKey: identity.address, identiconKey: identity.address,
identiconLabel: identity.name, identiconLabel: identity.name,
attributes: [ attributes: [
@ -37,10 +32,41 @@ AccountPanel.prototype.render = function () {
], ],
} }
return h(Panel, panelOpts, return (
!state.onShowDetail ? null : h('.arrow-right.cursor-pointer', [
h('i.fa.fa-chevron-right.fa-lg'), 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) { function balanceOrFaucetingIndication (account, isFauceting) {

View File

@ -39,12 +39,9 @@ IdenticonComponent.prototype.componentDidMount = function () {
if (!address) return if (!address) return
var container = findDOMNode(this) var container = findDOMNode(this)
var diameter = state.diameter || this.defaultDiameter var diameter = state.diameter || this.defaultDiameter
var dataUri = iconFactory.iconForAddress(address, diameter) var imageify = state.imageify
var img = iconFactory.iconForAddress(address, diameter, imageify)
var img = document.createElement('img')
img.src = dataUri
container.appendChild(img) container.appendChild(img)
} }

View File

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

View File

@ -16,6 +16,10 @@ function PendingTx () {
PendingTx.prototype.render = function () { PendingTx.prototype.render = function () {
var state = this.props var state = this.props
return this.renderGeneric(h, state)
}
PendingTx.prototype.renderGeneric = function (h, state) {
var txData = state.txData var txData = state.txData
var txParams = txData.txParams || {} var txParams = txData.txParams || {}
@ -24,6 +28,7 @@ PendingTx.prototype.render = function () {
var account = state.accounts[address] || { address: address } var account = state.accounts[address] || { address: address }
return ( return (
h('.transaction', { h('.transaction', {
key: txData.id, key: txData.id,
}, [ }, [
@ -40,6 +45,7 @@ PendingTx.prototype.render = function () {
showFullAddress: true, showFullAddress: true,
identity: identity, identity: identity,
account: account, account: account,
inlineIdenticons: state.inlineIdenticons,
}), }),
// tx data // tx data
@ -62,15 +68,25 @@ PendingTx.prototype.render = function () {
]), ]),
// send + cancel // send + cancel
h('.flex-row.flex-space-around', [ state.nonInteractive ? null : actionButtons(state),
h('button', {
onClick: state.cancelTransaction,
}, 'Cancel'),
h('button', {
onClick: state.sendTransaction,
}, 'Send'),
]),
]) ])
) )
} }
function actionButtons(state){
return (
h('.flex-row.flex-space-around', [
h('button', {
onClick: state.cancelTransaction,
}, 'Cancel'),
h('button', {
onClick: state.sendTransaction,
}, 'Send'),
])
)
}

View File

@ -12,42 +12,49 @@ function IconFactory (jazzicon) {
this.cache = {} this.cache = {}
} }
IconFactory.prototype.iconForAddress = function (address, diameter) { IconFactory.prototype.iconForAddress = function (address, diameter, imageify) {
if (this.isCached(address, diameter)) { if (imageify) {
return this.cache[address][diameter] 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 numericRepresentation = jsNumberForAddress(address)
var identicon = this.jazzicon(diameter, numericRepresentation) var identicon = this.jazzicon(diameter, numericRepresentation)
var identiconSrc = identicon.innerHTML return identicon
var dataUri = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc)
return dataUri
} }
IconFactory.prototype.cacheIcon = function (address, diameter, icon) { // util
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]
}
function jsNumberForAddress (address) { function jsNumberForAddress (address) {
var addr = address.slice(2, 10) var addr = address.slice(2, 10)
var seed = parseInt(addr, 16) var seed = parseInt(addr, 16)
return seed return seed
} }
function toDataUri(identiconSrc){
return 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(identiconSrc)
}