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:
commit
ac2269b16e
@ -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) {
|
||||
// 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)
|
||||
}
|
Loading…
Reference in New Issue
Block a user