Merge pull request #1049 from poanetwork/batching-on-homepage-and-refactoring

Batching message on homepage and refactoring JS
pull/1050/head
Jimmy Lauzau 6 years ago committed by GitHub
commit 49677afc06
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 147
      apps/block_scout_web/assets/__tests__/pages/blocks.js
  2. 296
      apps/block_scout_web/assets/__tests__/pages/chain.js
  3. 74
      apps/block_scout_web/assets/__tests__/pages/pending_transactions.js
  4. 41
      apps/block_scout_web/assets/__tests__/pages/transactions.js
  5. 199
      apps/block_scout_web/assets/js/lib/list_morph.js
  6. 34
      apps/block_scout_web/assets/js/lib/redux_helpers.js
  7. 27
      apps/block_scout_web/assets/js/lib/utils.js
  8. 6
      apps/block_scout_web/assets/js/pages/address.js
  9. 141
      apps/block_scout_web/assets/js/pages/blocks.js
  10. 285
      apps/block_scout_web/assets/js/pages/chain.js
  11. 166
      apps/block_scout_web/assets/js/pages/pending_transactions.js
  12. 48
      apps/block_scout_web/assets/js/pages/transaction.js
  13. 121
      apps/block_scout_web/assets/js/pages/transactions.js
  14. 4
      apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex
  15. 7
      apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex
  16. 9
      apps/block_scout_web/priv/gettext/default.pot
  17. 9
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po

@ -1,4 +1,4 @@
import { reducer, initialState } from '../../js/pages/blocks' import { reducer, initialState, placeHolderBlock } from '../../js/pages/blocks'
test('CHANNEL_DISCONNECTED', () => { test('CHANNEL_DISCONNECTED', () => {
const state = initialState const state = initialState
@ -10,58 +10,48 @@ test('CHANNEL_DISCONNECTED', () => {
expect(output.channelDisconnected).toBe(true) expect(output.channelDisconnected).toBe(true)
}) })
describe('PAGE_LOAD', () => { describe('ELEMENTS_LOAD', () => {
test('page 1 loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: false,
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(false)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('page 2 loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
beyondPageOne: true,
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(true)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('page 1 with skipped blocks', () => { test('page 1 with skipped blocks', () => {
const state = initialState window.localized = {}
const state = Object.assign({}, initialState, {
beyondPageOne: false
})
const action = { const action = {
type: 'PAGE_LOAD', type: 'ELEMENTS_LOAD',
beyondPageOne: false, blocks: [
blockNumbers: [4, 1] { blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 1, blockHtml: 'test 1' }
]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.beyondPageOne).toBe(false) expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([4, 3, 2, 1]) { blockNumber: 4, blockHtml: 'test 4' },
expect(output.skippedBlockNumbers).toEqual([3, 2]) { blockNumber: 3, blockHtml: placeHolderBlock(3) },
{ blockNumber: 2, blockHtml: placeHolderBlock(2) },
{ blockNumber: 1, blockHtml: 'test 1' }
])
}) })
test('page 2 with skipped blocks', () => { test('page 2 with skipped blocks', () => {
const state = initialState window.localized = {}
const state = Object.assign({}, initialState, {
beyondPageOne: true
})
const action = { const action = {
type: 'PAGE_LOAD', type: 'ELEMENTS_LOAD',
beyondPageOne: true, blocks: [
blockNumbers: [4, 1] { blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 1, blockHtml: 'test 1' }
]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.beyondPageOne).toBe(true) expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([4, 3, 2, 1]) { blockNumber: 4, blockHtml: 'test 4' },
expect(output.skippedBlockNumbers).toEqual([3, 2]) { blockNumber: 3, blockHtml: placeHolderBlock(3) },
{ blockNumber: 2, blockHtml: placeHolderBlock(2) },
{ blockNumber: 1, blockHtml: 'test 1' }
])
}) })
}) })
@ -76,8 +66,9 @@ describe('RECEIVED_NEW_BLOCK', () => {
} }
const output = reducer(initialState, action) const output = reducer(initialState, action)
expect(output.newBlock).toBe('test') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([1]) { blockNumber: 1, blockHtml: 'test' }
])
}) })
test('on page 2+', () => { test('on page 2+', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
@ -91,48 +82,37 @@ describe('RECEIVED_NEW_BLOCK', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe(null) expect(output.blocks).toEqual([])
expect(output.blockNumbers).toEqual([])
expect(output.skippedBlockNumbers).toEqual([])
}) })
test('inserts place holders if block received out of order', () => { test('inserts place holders if block received out of order', () => {
window.localized = {}
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [2] blocks: [
{ blockNumber: 2, blockHtml: 'test 2' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
blockHtml: 'test5', blockHtml: 'test 5',
blockNumber: 5 blockNumber: 5
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe('test5') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([5, 4, 3, 2]) { blockNumber: 5, blockHtml: 'test 5' },
expect(output.skippedBlockNumbers).toEqual([4, 3]) { blockNumber: 4, blockHtml: placeHolderBlock(4) },
}) { blockNumber: 3, blockHtml: placeHolderBlock(3) },
test('replaces skipped block', () => { { blockNumber: 2, blockHtml: 'test 2' }
const state = Object.assign({}, initialState, { ])
blockNumbers: [5, 4, 3, 2, 1],
skippedBlockNumbers: [4, 3, 1]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
blockHtml: 'test3',
blockNumber: 3
}
}
const output = reducer(state, action)
expect(output.newBlock).toBe('test3')
expect(output.blockNumbers).toEqual([5, 4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([4, 1])
}) })
test('replaces duplicated block', () => { test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [5, 4] blocks: [
{ blockNumber: 5, blockHtml: 'test 5' },
{ blockNumber: 4, blockHtml: 'test 4' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
@ -143,23 +123,34 @@ describe('RECEIVED_NEW_BLOCK', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe('test5') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([5, 4]) { blockNumber: 5, blockHtml: 'test5' },
{ blockNumber: 4, blockHtml: 'test 4' }
])
}) })
test('skips if new block height is lower than lowest on page', () => { test('skips if new block height is lower than lowest on page', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2] blocks: [
{ blockNumber: 5, blockHtml: 'test 5' },
{ blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 3, blockHtml: 'test 3' },
{ blockNumber: 2, blockHtml: 'test 2' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
blockHtml: 'test1', blockNumber: 1,
blockNumber: 1 blockHtml: 'test 1'
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe(null) expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([5, 4, 3, 2]) { blockNumber: 5, blockHtml: 'test 5' },
{ blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 3, blockHtml: 'test 3' },
{ blockNumber: 2, blockHtml: 'test 2' }
])
}) })
}) })

@ -1,27 +1,26 @@
import { reducer, initialState } from '../../js/pages/chain' import { reducer, initialState, placeHolderBlock } from '../../js/pages/chain'
describe('PAGE_LOAD', () => { describe('ELEMENTS_LOAD', () => {
test('loads block numbers', () => {
const state = initialState
const action = {
type: 'PAGE_LOAD',
blockNumbers: [2, 1]
}
const output = reducer(state, action)
expect(output.blockNumbers).toEqual([2, 1])
expect(output.skippedBlockNumbers).toEqual([])
})
test('loads with skipped blocks', () => { test('loads with skipped blocks', () => {
window.localized = {}
const state = initialState const state = initialState
const action = { const action = {
type: 'PAGE_LOAD', type: 'ELEMENTS_LOAD',
blockNumbers: [4, 1] blocks: [
{ blockNumber: 6, chainBlockHtml: 'test 6' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' },
{ blockNumber: 1, chainBlockHtml: 'test 1' }
]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.blockNumbers).toEqual([4, 3, 2, 1]) expect(output.blocks).toEqual([
expect(output.skippedBlockNumbers).toEqual([3, 2]) { blockNumber: 6, chainBlockHtml: 'test 6' },
{ blockNumber: 5, chainBlockHtml: placeHolderBlock(5) },
{ blockNumber: 4, chainBlockHtml: placeHolderBlock(4) },
{ blockNumber: 3, chainBlockHtml: 'test 3' }
])
}) })
}) })
@ -44,8 +43,10 @@ describe('RECEIVED_NEW_BLOCK', () => {
test('receives new block', () => { test('receives new block', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
averageBlockTime: '6 seconds', averageBlockTime: '6 seconds',
blockNumbers: [1], blocks: [
newBlock: 'last new block' { blockNumber: 1, chainBlockHtml: 'test 1' },
{ blockNumber: 0, chainBlockHtml: 'test 0' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
@ -58,121 +59,138 @@ describe('RECEIVED_NEW_BLOCK', () => {
const output = reducer(state, action) const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds') expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toEqual('new block') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([2, 1]) { blockNumber: 2, chainBlockHtml: 'new block', averageBlockTime: '5 seconds' },
{ blockNumber: 1, chainBlockHtml: 'test 1' }
])
}) })
test('inserts place holders if block received out of order', () => { test('inserts place holders if block received out of order', () => {
window.localized = {}
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [2] blocks: [
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' },
{ blockNumber: 1, chainBlockHtml: 'test 1' },
{ blockNumber: 0, chainBlockHtml: 'test 0' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
averageBlockTime: '5 seconds', chainBlockHtml: 'test 6',
chainBlockHtml: 'test5', blockNumber: 6
blockNumber: 5
}
}
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe('test5')
expect(output.blockNumbers).toEqual([5, 4, 3, 2])
expect(output.skippedBlockNumbers).toEqual([4, 3])
})
test('replaces skipped block', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [4, 3, 2, 1],
skippedBlockNumbers: [3, 2, 1]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
averageBlockTime: '5 seconds',
chainBlockHtml: 'test2',
blockNumber: 2
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds') expect(output.blocks).toEqual([
expect(output.newBlock).toBe('test2') { blockNumber: 6, chainBlockHtml: 'test 6' },
expect(output.blockNumbers).toEqual([4, 3, 2, 1]) { blockNumber: 5, chainBlockHtml: placeHolderBlock(5) },
expect(output.skippedBlockNumbers).toEqual([3, 1]) { blockNumber: 4, chainBlockHtml: placeHolderBlock(4) },
{ blockNumber: 3, chainBlockHtml: 'test 3' }
])
}) })
test('replaces duplicated block', () => { test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [5, 4] blocks: [
{ blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
averageBlockTime: '5 seconds',
chainBlockHtml: 'test5', chainBlockHtml: 'test5',
blockNumber: 5 blockNumber: 5
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds') expect(output.blocks).toEqual([
expect(output.newBlock).toBe('test5') { blockNumber: 5, chainBlockHtml: 'test5' },
expect(output.blockNumbers).toEqual([5, 4]) { blockNumber: 4, chainBlockHtml: 'test 4' }
])
}) })
test('skips if new block height is lower than lowest on page', () => { test('skips if new block height is lower than lowest on page', () => {
window.localized = {}
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
averageBlockTime: '5 seconds', averageBlockTime: '5 seconds',
blockNumbers: [5, 4, 3, 2] blocks: [
{ blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
averageBlockTime: '9 seconds', averageBlockTime: '9 seconds',
chainBlockHtml: 'test1', blockNumber: 1,
blockNumber: 1 chainBlockHtml: 'test 1'
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds') expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe(null) expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([5, 4, 3, 2]) { blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' }
])
}) })
test('only tracks 4 blocks based on page display limit', () => { test('only tracks 4 blocks based on page display limit', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2], blocks: [
skippedBlockNumbers: [4, 3, 2] { blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
chainBlockHtml: 'test6', chainBlockHtml: 'test 6',
blockNumber: 6 blockNumber: 6
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe('test6') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([6, 5, 4, 3]) { blockNumber: 6, chainBlockHtml: 'test 6' },
expect(output.skippedBlockNumbers).toEqual([4, 3]) { blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' },
{ blockNumber: 3, chainBlockHtml: 'test 3' }
])
}) })
test('skipped blocks list replaced when another block comes in with +3 blockheight', () => { test('skipped blocks list replaced when another block comes in with +3 blockheight', () => {
window.localized = {}
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2], blocks: [
skippedBlockNumbers: [4, 3, 2] { blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' },
{ blockNumber: 3, chainBlockHtml: 'test 3' },
{ blockNumber: 2, chainBlockHtml: 'test 2' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_BLOCK', type: 'RECEIVED_NEW_BLOCK',
msg: { msg: {
chainBlockHtml: 'test10', blockNumber: 10,
blockNumber: 10 chainBlockHtml: 'test 10'
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newBlock).toBe('test10') expect(output.blocks).toEqual([
expect(output.blockNumbers).toEqual([10, 9, 8, 7]) { blockNumber: 10, chainBlockHtml: 'test 10' },
expect(output.skippedBlockNumbers).toEqual([9, 8, 7]) { blockNumber: 9, chainBlockHtml: placeHolderBlock(9) },
{ blockNumber: 8, chainBlockHtml: placeHolderBlock(8) },
{ blockNumber: 7, chainBlockHtml: placeHolderBlock(7) }
])
}) })
}) })
@ -195,32 +213,144 @@ test('RECEIVED_NEW_EXCHANGE_RATE', () => {
expect(output.usdMarketCap).toEqual(1230000) expect(output.usdMarketCap).toEqual(1230000)
}) })
describe('RECEIVED_NEW_TRANSACTION', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => { test('single transaction', () => {
const state = initialState const state = initialState
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msg: { msgs: [{
transactionHtml: 'test' transactionHtml: 'test'
} }]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test']) expect(output.transactions).toEqual([{ transactionHtml: 'test' }])
expect(output.transactionsBatch.length).toEqual(0)
expect(output.transactionCount).toEqual(1) expect(output.transactionCount).toEqual(1)
}) })
test('single transaction after single transaction', () => { test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 1'
},{
transactionHtml: 'test 2'
},{
transactionHtml: 'test 3'
},{
transactionHtml: 'test 4'
},{
transactionHtml: 'test 5'
},{
transactionHtml: 'test 6'
},{
transactionHtml: 'test 7'
},{
transactionHtml: 'test 8'
},{
transactionHtml: 'test 9'
},{
transactionHtml: 'test 10'
},{
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.transactions).toEqual([])
expect(output.transactionsBatch.length).toEqual(11)
expect(output.transactionCount).toEqual(11)
})
test('maintains list size', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newTransactions: ['test 1'] transactions: [
{ transactionHash: '0x4', transactionHtml: 'test 4' },
{ transactionHash: '0x3', transactionHtml: 'test 3' },
{ transactionHash: '0x2', transactionHtml: 'test 2' },
{ transactionHash: '0x1', transactionHtml: 'test 1' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msg: { msgs: [
transactionHtml: 'test 2' { transactionHash: '0x5', transactionHtml: 'test 5' },
{ transactionHash: '0x6', transactionHtml: 'test 6' }
]
}
const output = reducer(state, action)
expect(output.transactions).toEqual([
{ transactionHash: '0x6', transactionHtml: 'test 6' },
{ transactionHash: '0x5', transactionHtml: 'test 5' },
{ transactionHash: '0x4', transactionHtml: 'test 4' },
{ transactionHash: '0x3', transactionHtml: 'test 3' },
])
expect(output.transactionsBatch.length).toEqual(0)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
}]
} }
const output = reducer(state, action)
expect(output.transactions).toEqual([])
expect(output.transactionsBatch.length).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test 12'
},{
transactionHtml: 'test 13'
},{
transactionHtml: 'test 14'
},{
transactionHtml: 'test 15'
},{
transactionHtml: 'test 16'
},{
transactionHtml: 'test 17'
},{
transactionHtml: 'test 18'
},{
transactionHtml: 'test 19'
},{
transactionHtml: 'test 20'
},{
transactionHtml: 'test 21'
},{
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.transactions).toEqual([])
expect(output.transactionsBatch.length).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}]
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test 1', 'test 2']) expect(output.transactions).toEqual([])
expect(output.transactionsBatch.length).toEqual(0)
}) })
}) })

@ -1,3 +1,4 @@
import _ from 'lodash'
import { reducer, initialState } from '../../js/pages/pending_transactions' import { reducer, initialState } from '../../js/pages/pending_transactions'
test('CHANNEL_DISCONNECTED', () => { test('CHANNEL_DISCONNECTED', () => {
@ -22,8 +23,11 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test']) expect(output.pendingTransactions).toEqual([{
expect(output.newPendingTransactionHashesBatch.length).toEqual(0) transactionHash: '0x00',
transactionHtml: 'test'
}])
expect(output.pendingTransactionsBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(1) expect(output.pendingTransactionCount).toEqual(1)
}) })
test('large batch of transactions', () => { test('large batch of transactions', () => {
@ -67,13 +71,16 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(11) expect(output.pendingTransactionsBatch.length).toEqual(11)
expect(output.pendingTransactionCount).toEqual(11) expect(output.pendingTransactionCount).toEqual(11)
}) })
test('single transaction after single transaction', () => { test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'], pendingTransactions: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
}],
pendingTransactionCount: 1 pendingTransactionCount: 1
}) })
const action = { const action = {
@ -85,13 +92,16 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) expect(output.pendingTransactions).toEqual([
expect(output.newPendingTransactionHashesBatch.length).toEqual(0) { transactionHash: '0x02', transactionHtml: 'test 2' },
{ transactionHash: '0x01', transactionHtml: 'test 1' }
])
expect(output.pendingTransactionsBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2) expect(output.pendingTransactionCount).toEqual(2)
}) })
test('single transaction after large batch of transactions', () => { test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
}) })
const action = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
@ -102,12 +112,12 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(12) expect(output.pendingTransactionsBatch.length).toEqual(12)
}) })
test('large batch of transactions after large batch of transactions', () => { test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] pendingTransactionsBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
}) })
const action = { const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
@ -148,8 +158,8 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(22) expect(output.pendingTransactionsBatch.length).toEqual(22)
}) })
test('after disconnection', () => { test('after disconnection', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
@ -164,7 +174,7 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
}) })
test('on page 2+', () => { test('on page 2+', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
@ -180,28 +190,50 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([]) expect(output.pendingTransactions).toEqual([])
expect(output.pendingTransactionCount).toEqual(2) expect(output.pendingTransactionCount).toEqual(2)
}) })
}) })
describe('RECEIVED_NEW_TRANSACTION', () => { describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction collated', () => { test('single transaction collated', () => {
const state = { ...initialState, pendingTransactionCount: 2 } const state = Object.assign({}, initialState, {
pendingTransactionCount: 2,
pendingTransactions: [{
transactionHash: '0x00',
transactionHtml: 'old'
}]
})
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
msg: { msg: {
transactionHash: '0x00' transactionHash: '0x00',
transactionHtml: 'new'
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.pendingTransactionCount).toBe(1) expect(output.pendingTransactionCount).toBe(1)
expect(output.newTransactionHashes).toEqual(['0x00']) expect(output.pendingTransactions).toEqual([{
transactionHash: '0x00',
transactionHtml: 'new'
}])
}) })
test('single transaction collated after batch', () => { test('single transaction collated after batch', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11'] pendingTransactionsBatch: [
{ transactionHash: '0x01' },
{ transactionHash: '2' },
{ transactionHash: '3' },
{ transactionHash: '4' },
{ transactionHash: '5' },
{ transactionHash: '6' },
{ transactionHash: '7' },
{ transactionHash: '8' },
{ transactionHash: '9' },
{ transactionHash: '10' },
{ transactionHash: '11' }
]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION', type: 'RECEIVED_NEW_TRANSACTION',
@ -211,8 +243,8 @@ describe('RECEIVED_NEW_TRANSACTION', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newPendingTransactionHashesBatch.length).toEqual(10) expect(output.pendingTransactionsBatch.length).toEqual(10)
expect(output.newPendingTransactionHashesBatch).not.toContain('0x01') expect(_.map(output.pendingTransactionsBatch, 'transactionHash')).not.toContain('0x01')
}) })
test('on page 2+', () => { test('on page 2+', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {

@ -8,7 +8,7 @@ test('CHANNEL_DISCONNECTED', () => {
const output = reducer(state, action) const output = reducer(state, action)
expect(output.channelDisconnected).toBe(true) expect(output.channelDisconnected).toBe(true)
expect(output.batchCountAccumulator).toBe(0) expect(output.transactionsBatch.length).toBe(0)
}) })
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
@ -22,8 +22,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test']) expect(output.transactions).toEqual([{ transactionHtml: 'test' }])
expect(output.batchCountAccumulator).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
expect(output.transactionCount).toEqual(1) expect(output.transactionCount).toEqual(1)
}) })
test('large batch of transactions', () => { test('large batch of transactions', () => {
@ -56,13 +56,15 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.transactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(11) expect(output.transactionsBatch.length).toEqual(11)
expect(output.transactionCount).toEqual(11) expect(output.transactionCount).toEqual(11)
}) })
test('single transaction after single transaction', () => { test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
newTransactions: ['test 1'] transactions: [{
transactionHtml: 'test 1'
}]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -72,12 +74,15 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test 1', 'test 2']) expect(output.transactions).toEqual([
expect(output.batchCountAccumulator).toEqual(0) { transactionHtml: 'test 2' },
{ transactionHtml: 'test 1' }
])
expect(output.transactionsBatch.length).toEqual(0)
}) })
test('single transaction after large batch of transactions', () => { test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
batchCountAccumulator: 11 transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -87,12 +92,12 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.transactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(12) expect(output.transactionsBatch.length).toEqual(12)
}) })
test('large batch of transactions after large batch of transactions', () => { test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
batchCountAccumulator: 11 transactionsBatch: [1,2,3,4,5,6,7,8,9,10,11]
}) })
const action = { const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH', type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -122,8 +127,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.transactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(22) expect(output.transactionsBatch.length).toEqual(22)
}) })
test('after disconnection', () => { test('after disconnection', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
@ -137,8 +142,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.transactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
}) })
test('on page 2+', () => { test('on page 2+', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
@ -153,8 +158,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.newTransactions).toEqual([]) expect(output.transactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0) expect(output.transactionsBatch.length).toEqual(0)
expect(output.transactionCount).toEqual(2) expect(output.transactionCount).toEqual(2)
}) })
}) })

@ -1,105 +1,78 @@
import $ from 'jquery' import $ from 'jquery'
import _ from 'lodash' import _ from 'lodash'
import { createStore as reduxCreateStore } from 'redux'
import morph from 'nanomorph' import morph from 'nanomorph'
import { updateAllAges } from './lib/from_now' import { updateAllAges } from './from_now'
export function batchChannel (func) { // The goal of this function is to DOM diff lists, so upon completion `container.innerHTML` should be
let msgs = [] // equivalent to `newElements.join('')`.
const debouncedFunc = _.debounce(() => { //
func.apply(this, [msgs]) // We could simply do `container.innerHTML = newElements.join('')` but that would not be efficient and
msgs = [] // it not animate appropriately. We could also simply use `morph` (or a similar library) on the entire
}, 1000, { maxWait: 5000 }) // list, however that doesn't give us the proper amount of control for animations.
return (msg) => { //
msgs.push(msg) // This function will walk though, remove items currently in `container` which are not in the new list.
debouncedFunc() // Then it will swap the contents of the items that are in both lists in case the items were updated or
} // the order changed. Finally, it will add elements to `container` which are in the new list and didn't
} // already exist in the DOM.
//
export function buildFullBlockList (blockNumbers) { // Params:
const newestBlock = _.first(blockNumbers) // container: the DOM element which contents need replaced
const oldestBlock = _.last(blockNumbers) // newElements: a list of elements that need to be put into the container
return skippedBlockListBuilder([], newestBlock + 1, oldestBlock - 1) // options:
} // key: the path to the unique identifier of each element
// horizontal: our horizontal animations are handled in CSS, so passing in `true` will not play JS
export function initRedux (reducer, { main, render, debug } = {}) { // animations
if (!reducer) { export default function (container, newElements, { key, horizontal } = {}) {
console.error('initRedux: You need a reducer to initialize Redux.') if (!container) return
return const oldElements = $(container).children().get()
} let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el }))
if (!render) console.warn('initRedux: You have not passed a render function.') const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el }))
const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] }))
const store = createStore(reducer) // remove old items
if (debug) store.subscribe(() => { console.log(store.getState()) }) const removals = _.differenceBy(currentList, newList, 'id')
let oldState = store.getState() let canAnimate = !horizontal && removals.length <= 1
if (render) { removals.forEach(({ el }) => {
store.subscribe(() => { if (!canAnimate) return el.remove()
const state = store.getState() const $el = $(el)
render(state, oldState) $el.addClass('shrink-out')
oldState = state setTimeout(() => { slideUpRemove($el) }, 400)
}) })
} currentList = _.differenceBy(currentList, removals, 'id')
if (main) main(store)
}
export function createStore (reducer) { // update kept items
return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) currentList = currentList.map(({ el }, i) => ({
} id: overlap[i].id,
el: el.outerHTML === overlap[i].el.outerHTML ? el : morph(el, overlap[i].el)
}))
export function connectElements ({ elements, store }) { // add new items
function loadElements () { const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse()
return _.reduce(elements, (pageLoadParams, { load }, selector) => { canAnimate = !horizontal
if (!load) return pageLoadParams finalList.forEach((el, i) => {
const $el = $(selector) if (el.parentElement) return
if (!$el.length) return pageLoadParams if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`))
const morePageLoadParams = load($el, store) canAnimate = false
return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
}, {}) slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el)
}
function renderElements (state, oldState) {
_.forIn(elements, ({ render }, selector) => {
if (!render) return
const $el = $(selector)
if (!$el.length) return
render($el, state, oldState)
})
}
store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' }))
let oldState = store.getState()
store.subscribe(() => {
const state = store.getState()
renderElements(state, oldState)
oldState = state
}) })
} }
export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) { function slideDownAppend ($container, content) {
for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i)
return skippedBlockNumbers
}
export function slideDownPrepend ($container, content) {
smarterSlideDown($(content), {
insert ($el) {
$container.prepend($el)
}
})
}
export function slideDownAppend ($container, content) {
smarterSlideDown($(content), { smarterSlideDown($(content), {
insert ($el) { insert ($el) {
$container.append($el) $container.append($el)
} }
}) })
} }
export function slideDownBefore ($container, content) { function slideDownBefore ($container, content) {
smarterSlideDown($(content), { smarterSlideDown($(content), {
insert ($el) { insert ($el) {
$container.before($el) $container.before($el)
} }
}) })
} }
export function slideUpRemove ($el) { function slideUpRemove ($el) {
smarterSlideUp($el, { smarterSlideUp($el, {
complete () { complete () {
$el.remove() $el.remove()
@ -145,71 +118,3 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) {
$el.slideUp({ complete, easing: 'linear' }) $el.slideUp({ complete, easing: 'linear' })
} }
} }
// The goal of this function is to DOM diff lists, so upon completion `container.innerHTML` should be
// equivalent to `newElements.join('')`.
//
// We could simply do `container.innerHTML = newElements.join('')` but that would not be efficient and
// it not animate appropriately. We could also simply use `morph` (or a similar library) on the entire
// list, however that doesn't give us the proper amount of control for animations.
//
// This function will walk though, remove items currently in `container` which are not in the new list.
// Then it will swap the contents of the items that are in both lists in case the items were updated or
// the order changed. Finally, it will add elements to `container` which are in the new list and didn't
// already exist in the DOM.
//
// Params:
// container: the DOM element which contents need replaced
// newElements: a list of elements that need to be put into the container
// options:
// key: the path to the unique identifier of each element
// horizontal: our horizontal animations are handled in CSS, so passing in `true` will not play JS
// animations
export function listMorph (container, newElements, { key, horizontal } = {}) {
if (!container) return
const oldElements = $(container).children().get()
let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el }))
const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el }))
const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] }))
// remove old items
const removals = _.differenceBy(currentList, newList, 'id')
let canAnimate = !horizontal && removals.length <= 1
removals.forEach(({ el }) => {
if (!canAnimate) return el.remove()
const $el = $(el)
$el.addClass('shrink-out')
setTimeout(() => { slideUpRemove($el) }, 400)
})
currentList = _.differenceBy(currentList, removals, 'id')
// update kept items
currentList = currentList.map(({ el }, i) => ({
id: overlap[i].id,
el: el.outerHTML === overlap[i].el.outerHTML ? el : morph(el, overlap[i].el)
}))
// add new items
const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse()
canAnimate = !horizontal
finalList.forEach((el, i) => {
if (el.parentElement) return
if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`))
canAnimate = false
if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el)
slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el)
})
}
export function onScrollBottom (callback) {
const $window = $(window)
function infiniteScrollChecker () {
const scrollHeight = $(document).height()
const scrollPosition = $window.height() + $window.scrollTop()
if ((scrollHeight - scrollPosition) / scrollHeight === 0) {
callback()
}
}
infiniteScrollChecker()
$window.on('scroll', infiniteScrollChecker)
}

@ -0,0 +1,34 @@
import $ from 'jquery'
import _ from 'lodash'
import { createStore as reduxCreateStore } from 'redux'
export function createStore (reducer) {
return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
}
export function connectElements ({ elements, store }) {
function loadElements () {
return _.reduce(elements, (pageLoadParams, { load }, selector) => {
if (!load) return pageLoadParams
const $el = $(selector)
if (!$el.length) return pageLoadParams
const morePageLoadParams = load($el, store)
return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams
}, {})
}
function renderElements (state, oldState) {
_.forIn(elements, ({ render }, selector) => {
if (!render) return
const $el = $(selector)
if (!$el.length) return
render($el, state, oldState)
})
}
let oldState = store.getState()
store.subscribe(() => {
const state = store.getState()
renderElements(state, oldState)
oldState = state
})
store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' }))
}

@ -0,0 +1,27 @@
import $ from 'jquery'
import _ from 'lodash'
export function batchChannel (func) {
let msgs = []
const debouncedFunc = _.debounce(() => {
func.apply(this, [msgs])
msgs = []
}, 1000, { maxWait: 5000 })
return (msg) => {
msgs.push(msg)
debouncedFunc()
}
}
export function onScrollBottom (callback) {
const $window = $(window)
function infiniteScrollChecker () {
const scrollHeight = $(document).height()
const scrollPosition = $window.height() + $window.scrollTop()
if ((scrollHeight - scrollPosition) / scrollHeight === 0) {
callback()
}
}
infiniteScrollChecker()
$window.on('scroll', infiniteScrollChecker)
}

@ -4,7 +4,9 @@ import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { createStore, connectElements, batchChannel, listMorph, onScrollBottom } from '../utils' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel, onScrollBottom } from '../lib/utils'
import listMorph from '../lib/list_morph'
import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { updateAllCalculatedUsdValues } from '../lib/currency.js'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
@ -233,7 +235,7 @@ const elements = {
} }
}, },
'[data-selector="transactions-list"]': { '[data-selector="transactions-list"]': {
load ($el, store) { load ($el) {
return { return {
transactions: $el.children().map((index, el) => ({ transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash, transactionHash: el.dataset.transactionHash,

@ -3,28 +3,24 @@ import _ from 'lodash'
import URI from 'urijs' import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import socket from '../socket' import socket from '../socket'
import { updateAllAges } from '../lib/from_now' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { buildFullBlockList, initRedux, slideDownBefore, skippedBlockListBuilder } from '../utils' import listMorph from '../lib/list_morph'
export const initialState = { export const initialState = {
blockNumbers: [],
beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
newBlock: null,
replaceBlock: null, blocks: [],
skippedBlockNumbers: []
beyondPageOne: null
} }
export function reducer (state = initialState, action) { export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'PAGE_LOAD':
const blockNumbers = buildFullBlockList(action.blockNumbers) case 'ELEMENTS_LOAD': {
const skippedBlockNumbers = _.difference(blockNumbers, action.blockNumbers) return Object.assign({}, state, _.omit(action, 'type'))
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
blockNumbers,
skippedBlockNumbers
})
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, { return Object.assign({}, state, {
@ -34,30 +30,16 @@ export function reducer (state = initialState, action) {
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
if (state.channelDisconnected || state.beyondPageOne) return state if (state.channelDisconnected || state.beyondPageOne) return state
const blockNumber = parseInt(action.msg.blockNumber) if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
if (_.includes(state.blockNumbers, blockNumber)) {
return Object.assign({}, state, { return Object.assign({}, state, {
newBlock: action.msg.blockHtml, blocks: [
replaceBlock: blockNumber, action.msg,
skippedBlockNumbers: _.without(state.skippedBlockNumbers, blockNumber) ...state.blocks
]
}) })
} else if (blockNumber < _.last(state.blockNumbers)) {
return state
} else { } else {
let skippedBlockNumbers = state.skippedBlockNumbers.slice(0)
if (blockNumber > state.blockNumbers[0] + 1) {
skippedBlockListBuilder(skippedBlockNumbers, blockNumber, state.blockNumbers[0])
}
const newBlockNumbers = _.chain([blockNumber])
.union(skippedBlockNumbers, state.blockNumbers)
.orderBy([], ['desc'])
.value()
return Object.assign({}, state, { return Object.assign({}, state, {
blockNumbers: newBlockNumbers, blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block)
newBlock: action.msg.blockHtml,
replaceBlock: null,
skippedBlockNumbers
}) })
} }
} }
@ -66,49 +48,70 @@ export function reducer (state = initialState, action) {
} }
} }
const $blockListPage = $('[data-page="block-list"]') function withMissingBlocks (reducer) {
if ($blockListPage.length) { return (...args) => {
initRedux(reducer, { const result = reducer(...args)
main (store) {
const state = store.dispatch({ if (result.blocks.length < 2) return result
type: 'PAGE_LOAD',
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber, const maxBlock = _.first(result.blocks).blockNumber
blockNumbers: $('[data-selector="block-number"]').map((index, el) => parseInt(el.innerText)).toArray() const minBlock = _.last(result.blocks).blockNumber
return Object.assign({}, result, {
blocks: _.rangeRight(minBlock, maxBlock + 1)
.map((blockNumber) => _.find(result.blocks, ['blockNumber', blockNumber]) || {
blockNumber,
blockHtml: placeHolderBlock(blockNumber)
})
}) })
if (!state.beyondPageOne) {
const blocksChannel = socket.channel(`blocks:new_block`, {})
blocksChannel.join()
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
blocksChannel.on('new_block', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })
)
} }
}, }
render (state, oldState) {
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const skippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers)
if (state.channelDisconnected) $channelDisconnected.show() const elements = {
if ((state.newBlock && oldState.newBlock !== state.newBlock) || skippedBlockNumbers.length) { '[data-selector="channel-disconnected-message"]': {
if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) { render ($el, state) {
const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`) if (state.channelDisconnected) $el.show()
$replaceBlock.addClass('shrink-out')
setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400)
} else {
if (skippedBlockNumbers.length) {
_.forEachRight(skippedBlockNumbers, (skippedBlockNumber) => {
slideDownBefore($(`[data-block-number="${skippedBlockNumber - 1}"]`), placeHolderBlock(skippedBlockNumber))
})
} }
slideDownBefore($(`[data-block-number="${state.blockNumbers[0] - 1}"]`), state.newBlock) },
'[data-selector="blocks-list"]': {
load ($el) {
return {
blocks: $el.children().map((index, el) => ({
blockNumber: parseInt(el.dataset.blockNumber),
blockHtml: el.outerHTML
})).toArray()
} }
updateAllAges() },
render ($el, state, oldState) {
if (oldState.blocks === state.blocks) return
const container = $el[0]
const newElements = _.map(state.blocks, ({ blockHtml }) => $(blockHtml)[0])
listMorph(container, newElements, { key: 'dataset.blockNumber' })
} }
} }
}
const $blockListPage = $('[data-page="block-list"]')
if ($blockListPage.length) {
const store = createStore(reducer)
store.dispatch({
type: 'PAGE_LOAD',
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).blockNumber
}) })
connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {})
blocksChannel.join()
blocksChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
blocksChannel.on('new_block', (msg) => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
} }
function placeHolderBlock (blockNumber) { export function placeHolderBlock (blockNumber) {
return ` return `
<div class="my-3" style="height: 98px;" data-selector="place-holder" data-block-number="${blockNumber}"> <div class="my-3" style="height: 98px;" data-selector="place-holder" data-block-number="${blockNumber}">
<div <div

@ -3,36 +3,32 @@ import _ from 'lodash'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { exchangeRateChannel, formatUsdValue } from '../lib/currency' import { exchangeRateChannel, formatUsdValue } from '../lib/currency'
import { buildFullBlockList, initRedux, skippedBlockListBuilder, slideDownPrepend } from '../utils' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
import { createMarketHistoryChart } from '../lib/market_history_chart' import { createMarketHistoryChart } from '../lib/market_history_chart'
const BATCH_THRESHOLD = 6
export const initialState = { export const initialState = {
addressCount: null, addressCount: null,
availableSupply: null, availableSupply: null,
averageBlockTime: null, averageBlockTime: null,
blockNumbers: [],
marketHistoryData: null, marketHistoryData: null,
newBlock: null, blocks: [],
newTransactions: [], transactions: [],
replaceBlock: null, transactionsBatch: [],
skippedBlockNumbers: [],
transactionCount: null, transactionCount: null,
usdMarketCap: null usdMarketCap: null
} }
export function reducer (state = initialState, action) { export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'ELEMENTS_LOAD': {
const fullBlockNumberList = buildFullBlockList(action.blockNumbers) return Object.assign({}, state, _.omit(action, 'type'))
const fullSkippedBlockNumberList = _.difference(fullBlockNumberList, action.blockNumbers)
const blockNumbers = fullBlockNumberList.slice(0, 4)
return Object.assign({}, state, {
blockNumbers,
transactionCount: numeral(action.transactionCount).value(),
skippedBlockNumbers: _.intersection(fullSkippedBlockNumberList, blockNumbers)
})
} }
case 'RECEIVED_NEW_ADDRESS_COUNT': { case 'RECEIVED_NEW_ADDRESS_COUNT': {
return Object.assign({}, state, { return Object.assign({}, state, {
@ -40,38 +36,17 @@ export function reducer (state = initialState, action) {
}) })
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
const blockNumber = parseInt(action.msg.blockNumber) if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
if (_.includes(state.blockNumbers, blockNumber)) {
return Object.assign({}, state, { return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime, averageBlockTime: action.msg.averageBlockTime,
newBlock: action.msg.chainBlockHtml, blocks: [
replaceBlock: blockNumber, action.msg,
skippedBlockNumbers: _.without(state.skippedBlockNumbers, blockNumber) ...state.blocks.slice(0, -1)
]
}) })
} else if (blockNumber < _.last(state.blockNumbers)) {
return Object.assign({}, state, { newBlock: null })
} else { } else {
let skippedBlockNumbers = state.skippedBlockNumbers.slice(0)
if (blockNumber > state.blockNumbers[0] + 1) {
let oldestBlock = state.blockNumbers[0]
if (blockNumber - oldestBlock >= 3) {
skippedBlockNumbers = []
if (blockNumber - oldestBlock > 3) oldestBlock = blockNumber - 4
}
skippedBlockListBuilder(skippedBlockNumbers, blockNumber, oldestBlock)
}
const newBlockNumbers = _.chain([blockNumber])
.union(skippedBlockNumbers, state.blockNumbers)
.orderBy([], ['desc'])
.slice(0, 4)
.value()
const newSkippedBlockNumbers = _.intersection(skippedBlockNumbers, newBlockNumbers)
return Object.assign({}, state, { return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime, blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block)
blockNumbers: newBlockNumbers,
newBlock: action.msg.chainBlockHtml,
replaceBlock: null,
skippedBlockNumbers: newSkippedBlockNumbers
}) })
} }
} }
@ -82,122 +57,176 @@ export function reducer (state = initialState, action) {
usdMarketCap: action.msg.exchangeRate.marketCapUsd usdMarketCap: action.msg.exchangeRate.marketCapUsd
}) })
} }
case 'RECEIVED_NEW_TRANSACTION': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const transactionCount = state.transactionCount + action.msgs.length
if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
newTransactions: [ transactions: [
...state.newTransactions, ...action.msgs.reverse(),
action.msg.transactionHtml ...state.transactions.slice(0, -1 * action.msgs.length)
], ],
transactionCount: state.transactionCount + 1 transactionCount
})
} else {
return Object.assign({}, state, {
transactionsBatch: [
...action.msgs.reverse(),
...state.transactionsBatch
],
transactionCount
}) })
} }
}
default: default:
return state return state
} }
} }
let chart function withMissingBlocks (reducer) {
const $chainDetailsPage = $('[data-page="chain-details"]') return (...args) => {
if ($chainDetailsPage.length) { const result = reducer(...args)
initRedux(reducer, {
main (store) {
const addressesChannel = socket.channel(`addresses:new_address`)
addressesChannel.join()
addressesChannel.on('count', msg => store.dispatch({ type: 'RECEIVED_NEW_ADDRESS_COUNT', msg: humps.camelizeKeys(msg) }))
const blocksChannel = socket.channel(`blocks:new_block`) if (!result.blocks || result.blocks.length < 2) return result
store.dispatch({
type: 'PAGE_LOAD',
blockNumbers: $('[data-selector="block-number"]').map((index, el) => parseInt(el.innerText)).toArray(),
transactionCount: $('[data-selector="transaction-count"]').text()
})
blocksChannel.join()
blocksChannel.on('new_block', msg => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }))
exchangeRateChannel.on('new_rate', (msg) => store.dispatch({ type: 'RECEIVED_NEW_EXCHANGE_RATE', msg: humps.camelizeKeys(msg) })) const maxBlock = _.first(result.blocks).blockNumber
const minBlock = maxBlock - (result.blocks.length - 1)
const transactionsChannel = socket.channel(`transactions:new_transaction`) return Object.assign({}, result, {
transactionsChannel.join() blocks: _.rangeRight(minBlock, maxBlock + 1)
transactionsChannel.on('transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })) .map((blockNumber) => _.find(result.blocks, ['blockNumber', blockNumber]) || {
blockNumber,
chainBlockHtml: placeHolderBlock(blockNumber)
})
})
}
}
chart = createMarketHistoryChart($('[data-chart="marketHistoryChart"]')[0]) let chart
const elements = {
'[data-chart="marketHistoryChart"]': {
load ($el) {
chart = createMarketHistoryChart($el[0])
}, },
render (state, oldState) { render ($el, state, oldState) {
const $addressCount = $('[data-selector="address-count"]') if (oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) return
const $averageBlockTime = $('[data-selector="average-block-time"]') chart.update(state.availableSupply, state.marketHistoryData)
const $blockList = $('[data-selector="chain-block-list"]')
const $marketCap = $('[data-selector="market-cap"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
const newTransactions = _.difference(state.newTransactions, oldState.newTransactions)
const skippedBlockNumbers = _.difference(state.skippedBlockNumbers, oldState.skippedBlockNumbers)
if (oldState.addressCount !== state.addressCount) {
$addressCount.empty().append(state.addressCount)
}
if (oldState.averageBlockTime !== state.averageBlockTime) {
$averageBlockTime.empty().append(state.averageBlockTime)
}
if (oldState.usdMarketCap !== state.usdMarketCap) {
$marketCap.empty().append(formatUsdValue(state.usdMarketCap))
}
if ((state.newBlock && oldState.newBlock !== state.newBlock) || skippedBlockNumbers.length) {
if (state.replaceBlock && oldState.replaceBlock !== state.replaceBlock) {
const $replaceBlock = $(`[data-block-number="${state.replaceBlock}"]`)
$replaceBlock.addClass('shrink-out')
setTimeout(() => $replaceBlock.replaceWith(state.newBlock), 400)
} else {
if (state.newBlock) {
$blockList.children().last().remove()
$blockList.prepend(newBlockHtml(state.newBlock))
}
if (skippedBlockNumbers.length) {
const newSkippedBlockNumbers = _.intersection(skippedBlockNumbers, state.blockNumbers)
_.each(newSkippedBlockNumbers, (skippedBlockNumber) => {
$blockList.children().last().remove()
$(`[data-block-number="${skippedBlockNumber + 1}"]`).parent().after(placeHolderBlock(skippedBlockNumber))
})
} }
},
'[data-selector="transaction-count"]': {
load ($el) {
return { transactionCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format())
} }
updateAllAges() },
'[data-selector="address-count"]': {
render ($el, state, oldState) {
if (oldState.addressCount === state.addressCount) return
$el.empty().append(state.addressCount)
} }
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) },
if (newTransactions.length) { '[data-selector="average-block-time"]': {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) render ($el, state, oldState) {
$transactionsList if (oldState.averageBlockTime === state.averageBlockTime) return
.children() $el.empty().append(state.averageBlockTime)
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length)
.remove()
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
} }
if (oldState.availableSupply !== state.availableSupply || oldState.marketHistoryData !== state.marketHistoryData) { },
chart.update(state.availableSupply, state.marketHistoryData) '[data-selector="market-cap"]': {
render ($el, state, oldState) {
if (oldState.usdMarketCap === state.usdMarketCap) return
$el.empty().append(formatUsdValue(state.usdMarketCap))
}
},
'[data-selector="chain-block-list"]': {
load ($el) {
return {
blocks: $el.children().map((index, el) => ({
blockNumber: parseInt(el.dataset.blockNumber),
chainBlockHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.blocks === state.blocks) return
const container = $el[0]
const newElements = _.map(state.blocks, ({ chainBlockHtml }) => $(chainBlockHtml)[0])
listMorph(container, newElements, { key: 'dataset.blockNumber', horizontal: true })
}
},
'[data-selector="transactions-list"]': {
load ($el) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
},
'[data-selector="channel-batching-count"]': {
render ($el, state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
if (!state.transactionsBatch.length) return $channelBatching.hide()
$channelBatching.show()
$el[0].innerHTML = numeral(state.transactionsBatch.length).format()
} }
} }
})
} }
function newBlockHtml (blockHtml) { const $chainDetailsPage = $('[data-page="chain-details"]')
return ` if ($chainDetailsPage.length) {
<div class="col-lg-3 fade-up-blocks-chain"> const store = createStore(reducer)
${blockHtml} connectElements({ store, elements })
</div>
` exchangeRateChannel.on('new_rate', (msg) => store.dispatch({
type: 'RECEIVED_NEW_EXCHANGE_RATE',
msg: humps.camelizeKeys(msg)
}))
const addressesChannel = socket.channel(`addresses:new_address`)
addressesChannel.join()
addressesChannel.on('count', msg => store.dispatch({
type: 'RECEIVED_NEW_ADDRESS_COUNT',
msg: humps.camelizeKeys(msg)
}))
const blocksChannel = socket.channel(`blocks:new_block`)
blocksChannel.join()
blocksChannel.on('new_block', msg => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})))
} }
function placeHolderBlock (blockNumber) { export function placeHolderBlock (blockNumber) {
return ` return `
<div <div
class="col-lg-3 fade-up-blocks-chain" class="col-lg-3 fade-up-blocks-chain"
style="min-height: 100px;" style="min-height: 100px;"
data-selector="place-holder"
data-block-number="${blockNumber}"
> >
<div <div
class="tile tile-type-block d-flex align-items-center fade-up" class="tile tile-type-block d-flex align-items-center fade-up"
style="height: 100px;" style="height: 100px;"
data-selector="place-holder"
data-block-number="${blockNumber}"
> >
<span class="loading-spinner-small ml-1 mr-4"> <span class="loading-spinner-small ml-1 mr-4">
<span class="loading-spinner-block-1"></span> <span class="loading-spinner-block-1"></span>

@ -4,27 +4,28 @@ import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { updateAllAges } from '../lib/from_now' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
const BATCH_THRESHOLD = 10 const BATCH_THRESHOLD = 10
export const initialState = { export const initialState = {
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
newPendingTransactions: [],
newTransactionHashes: [], pendingTransactionCount: null,
pendingTransactionCount: null
pendingTransactions: [],
pendingTransactionsBatch: [],
beyondPageOne: null
} }
export function reducer (state = initialState, action) { export function reducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'PAGE_LOAD':
return Object.assign({}, state, { case 'ELEMENTS_LOAD': {
beyondPageOne: action.beyondPageOne, return Object.assign({}, state, _.omit(action, 'type'))
pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, { return Object.assign({}, state, {
@ -35,9 +36,9 @@ export function reducer (state = initialState, action) {
if (state.channelDisconnected) return state if (state.channelDisconnected) return state
return Object.assign({}, state, { return Object.assign({}, state, {
newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash), pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? action.msg : transaction),
pendingTransactionCount: state.pendingTransactionCount - 1, pendingTransactionsBatch: state.pendingTransactionsBatch.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash),
newTransactionHashes: [action.msg.transactionHash] pendingTransactionCount: state.pendingTransactionCount - 1
}) })
} }
case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': { case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
@ -47,85 +48,110 @@ export function reducer (state = initialState, action) {
if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount }) if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount })
if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) { if (!state.pendingTransactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
newPendingTransactions: [ pendingTransactions: [
...state.newPendingTransactions, ...action.msgs.reverse(),
..._.map(action.msgs, 'transactionHtml') ...state.pendingTransactions
], ],
pendingTransactionCount pendingTransactionCount
}) })
} else { } else {
return Object.assign({}, state, { return Object.assign({}, state, {
newPendingTransactionHashesBatch: [ pendingTransactionsBatch: [
...state.newPendingTransactionHashesBatch, ...action.msgs.reverse(),
..._.map(action.msgs, 'transactionHash') ...state.pendingTransactionsBatch
], ],
pendingTransactionCount pendingTransactionCount
}) })
} }
} }
case 'REMOVE_PENDING_TRANSACTION': {
return Object.assign({}, state, {
pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash)
})
}
default: default:
return state return state
} }
} }
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
},
'[data-selector="channel-batching-count"]': {
render ($el, state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
if (state.pendingTransactionsBatch.length) {
$channelBatching.show()
$el[0].innerHTML = numeral(state.pendingTransactionsBatch.length).format()
} else {
$channelBatching.hide()
}
}
},
'[data-selector="transaction-pending-count"]': {
load ($el) {
return { pendingTransactionCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format())
}
},
'[data-selector="transactions-pending-list"]': {
load ($el) {
return {
pendingTransactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.pendingTransactions === state.pendingTransactions) return
const container = $el[0]
const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
}
}
const $transactionPendingListPage = $('[data-page="transaction-pending-list"]') const $transactionPendingListPage = $('[data-page="transaction-pending-list"]')
if ($transactionPendingListPage.length) { if ($transactionPendingListPage.length) {
initRedux(reducer, { const store = createStore(reducer)
main (store) {
store.dispatch({ store.dispatch({
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt
}) })
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`) const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join() transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) transactionsChannel.onError(() => store.dispatch({
transactionsChannel.on('transaction', (msg) => type: 'CHANNEL_DISCONNECTED'
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) }))
) transactionsChannel.on('transaction', (msg) => {
store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION',
msg: humps.camelizeKeys(msg)
})
setTimeout(() => store.dispatch({
type: 'REMOVE_PENDING_TRANSACTION',
msg: humps.camelizeKeys(msg)
}), 1000)
})
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`) const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)
pendingTransactionsChannel.join() pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) pendingTransactionsChannel.onError(() => store.dispatch({
pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) => type: 'CHANNEL_DISCONNECTED'
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) }))
) pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) => store.dispatch({
}, type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
render (state, oldState) { msgs: humps.camelizeKeys(msgs)
const $channelBatching = $('[data-selector="channel-batching-message"]') })))
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]')
const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.pendingTransactionCount !== state.pendingTransactionCount) {
$pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format())
}
if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) {
const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`)
$transaction.addClass('shrink-out')
setTimeout(() => {
if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) {
window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString()
} else {
slideUpRemove($transaction)
}
}, 400)
}
if (state.newPendingTransactionHashesBatch.length) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format()
} else {
$channelBatching.hide()
}
if (oldState.newPendingTransactions !== state.newPendingTransactions) {
const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length)
slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
} }

@ -1,8 +1,9 @@
import $ from 'jquery' import $ from 'jquery'
import _ from 'lodash'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { initRedux } from '../utils' import { createStore, connectElements } from '../lib/redux_helpers.js'
export const initialState = { export const initialState = {
blockNumber: null, blockNumber: null,
@ -11,10 +12,8 @@ export const initialState = {
export function reducer (state = initialState, action) { export function reducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'ELEMENTS_LOAD': {
return Object.assign({}, state, { return Object.assign({}, state, _.omit(action, 'type'))
blockNumber: parseInt(action.blockNumber, 10)
})
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) { if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) {
@ -28,30 +27,35 @@ export function reducer (state = initialState, action) {
} }
} }
const elements = {
'[data-selector="block-number"]': {
load ($el) {
return { blockNumber: parseInt($el.text(), 10) }
}
},
'[data-selector="block-confirmations"]': {
render ($el, state, oldState) {
if (oldState.confirmations !== state.confirmations) {
$el.empty().append(numeral(state.confirmations).format())
}
}
}
}
const $transactionDetailsPage = $('[data-page="transaction-details"]') const $transactionDetailsPage = $('[data-page="transaction-details"]')
if ($transactionDetailsPage.length) { if ($transactionDetailsPage.length) {
initRedux(reducer, { const store = createStore(reducer)
main (store) { connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {}) const blocksChannel = socket.channel(`blocks:new_block`, {})
const $transactionBlockNumber = $('[data-selector="block-number"]')
store.dispatch({
type: 'PAGE_LOAD',
blockNumber: $transactionBlockNumber.text()
})
blocksChannel.join() blocksChannel.join()
blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) blocksChannel.on('new_block', (msg) => store.dispatch({
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
const transactionHash = $transactionDetailsPage[0].dataset.pageTransactionHash const transactionHash = $transactionDetailsPage[0].dataset.pageTransactionHash
const transactionChannel = socket.channel(`transactions:${transactionHash}`, {}) const transactionChannel = socket.channel(`transactions:${transactionHash}`, {})
transactionChannel.join() transactionChannel.join()
transactionChannel.on('collated', () => window.location.reload()) transactionChannel.on('collated', () => window.location.reload())
},
render (state, oldState) {
const $blockConfirmations = $('[data-selector="block-confirmations"]')
if (oldState.confirmations !== state.confirmations) {
$blockConfirmations.empty().append(numeral(state.confirmations).format())
}
}
})
} }

@ -4,31 +4,33 @@ import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import socket from '../socket' import socket from '../socket'
import { updateAllAges } from '../lib/from_now' import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel, initRedux, slideDownPrepend } from '../utils' import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
const BATCH_THRESHOLD = 10 const BATCH_THRESHOLD = 10
export const initialState = { export const initialState = {
batchCountAccumulator: 0,
beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
newTransactions: [],
transactionCount: null transactionCount: null,
transactions: [],
transactionsBatch: [],
beyondPageOne: null
} }
export function reducer (state = initialState, action) { export function reducer (state = initialState, action) {
switch (action.type) { switch (action.type) {
case 'PAGE_LOAD': { case 'PAGE_LOAD':
return Object.assign({}, state, { case 'ELEMENTS_LOAD': {
beyondPageOne: action.beyondPageOne, return Object.assign({}, state, _.omit(action, 'type'))
transactionCount: numeral(action.transactionCount).value()
})
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, { return Object.assign({}, state, {
channelDisconnected: true, channelDisconnected: true,
batchCountAccumulator: 0 transactionsBatch: []
}) })
} }
case 'RECEIVED_NEW_TRANSACTION_BATCH': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
@ -38,17 +40,20 @@ export function reducer (state = initialState, action) {
if (state.beyondPageOne) return Object.assign({}, state, { transactionCount }) if (state.beyondPageOne) return Object.assign({}, state, { transactionCount })
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
newTransactions: [ transactions: [
...state.newTransactions, ...action.msgs.reverse(),
..._.map(action.msgs, 'transactionHtml') ...state.transactions
], ],
transactionCount transactionCount
}) })
} else { } else {
return Object.assign({}, state, { return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, transactionsBatch: [
...action.msgs.reverse(),
...state.transactionsBatch
],
transactionCount transactionCount
}) })
} }
@ -58,43 +63,63 @@ export function reducer (state = initialState, action) {
} }
} }
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
},
'[data-selector="channel-batching-count"]': {
render ($el, state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
if (!state.transactionsBatch.length) return $channelBatching.hide()
$channelBatching.show()
$el[0].innerHTML = numeral(state.transactionsBatch.length).format()
}
},
'[data-selector="transaction-count"]': {
load ($el) {
return { transactionCount: numeral($el.text()).value() }
},
render ($el, state, oldState) {
if (oldState.transactionCount === state.transactionCount) return
$el.empty().append(numeral(state.transactionCount).format())
}
},
'[data-selector="transactions-list"]': {
load ($el, store) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,
transactionHtml: el.outerHTML
})).toArray()
}
},
render ($el, state, oldState) {
if (oldState.transactions === state.transactions) return
const container = $el[0]
const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0])
listMorph(container, newElements, { key: 'dataset.transactionHash' })
}
}
}
const $transactionListPage = $('[data-page="transaction-list"]') const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) { if ($transactionListPage.length) {
initRedux(reducer, { const store = createStore(reducer)
main (store) {
store.dispatch({ store.dispatch({
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
}) })
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`) const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join() transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) transactionsChannel.onError(() => store.dispatch({
transactionsChannel.on('transaction', batchChannel((msgs) => type: 'CHANNEL_DISCONNECTED'
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) }))
) transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({
}, type: 'RECEIVED_NEW_TRANSACTION_BATCH',
render (state, oldState) { msgs: humps.camelizeKeys(msgs)
const $channelBatching = $('[data-selector="channel-batching-message"]') })))
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $transactionsList = $('[data-selector="transactions-list"]')
const $transactionCount = $('[data-selector="transaction-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (state.batchCountAccumulator) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format()
} else {
$channelBatching.hide()
}
if (oldState.newTransactions !== state.newTransactions) {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length)
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
} }

@ -1,4 +1,5 @@
<div class="tile tile-type-block d-flex flex-column" data-selector="chain-block" data-block-number="<%= @block.number %>"> <div class="col-lg-3 fade-up-blocks-chain" data-selector="chain-block" data-block-number="<%= @block.number %>">
<div class="tile tile-type-block d-flex flex-column">
<%= link( <%= link(
@block, @block,
class: "tile-title", class: "tile-title",
@ -17,4 +18,5 @@
address: @block.miner, address: @block.miner,
contract: false %> contract: false %>
</span> </span>
</div>
</div> </div>

@ -57,9 +57,7 @@
<h2 class="card-title"><%= gettext "Blocks" %></h2> <h2 class="card-title"><%= gettext "Blocks" %></h2>
<div class="row" data-selector="chain-block-list"> <div class="row" data-selector="chain-block-list">
<%= for block <- @blocks do %> <%= for block <- @blocks do %>
<div class="col-lg-3 fade-up-blocks-chain">
<%= render BlockScoutWeb.ChainView, "_block.html", block: block %> <%= render BlockScoutWeb.ChainView, "_block.html", block: block %>
</div>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -69,6 +67,11 @@
<div class="card-body"> <div class="card-body">
<%= link(gettext("View All Transactions →"), to: transaction_path(BlockScoutWeb.Endpoint, :index), class: "button button-secondary button-xsmall float-right") %> <%= link(gettext("View All Transactions →"), to: transaction_path(BlockScoutWeb.Endpoint, :index), class: "button button-secondary button-xsmall float-right") %>
<h2 class="card-title"><%= gettext "Transactions" %></h2> <h2 class="card-title"><%= gettext "Transactions" %></h2>
<div data-selector="channel-batching-message" style="display: none;">
<div data-selector="reload-button" class="alert alert-info">
<a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More transactions have come in" %></a>
</div>
</div>
<span data-selector="transactions-list"> <span data-selector="transactions-list">
<%= for transaction <- @transactions do %> <%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %>

@ -41,7 +41,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:21 #: lib/block_scout_web/templates/block/overview.html.eex:21
#: lib/block_scout_web/templates/chain/_block.html.eex:9 #: lib/block_scout_web/templates/chain/_block.html.eex:10
msgid "%{count} Transactions" msgid "%{count} Transactions"
msgstr "" msgstr ""
@ -555,7 +555,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:37 #: lib/block_scout_web/templates/block/_tile.html.eex:37
#: lib/block_scout_web/templates/block/overview.html.eex:99 #: lib/block_scout_web/templates/block/overview.html.eex:99
#: lib/block_scout_web/templates/chain/_block.html.eex:13 #: lib/block_scout_web/templates/chain/_block.html.eex:14
msgid "Miner" msgid "Miner"
msgstr "" msgstr ""
@ -575,6 +575,7 @@ msgid "More internal transactions have come in"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:72
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:14 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:14
#: lib/block_scout_web/templates/transaction/index.html.eex:14 #: lib/block_scout_web/templates/transaction/index.html.eex:14
msgid "More transactions have come in" msgid "More transactions have come in"
@ -989,7 +990,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:23
#: lib/block_scout_web/templates/block_transaction/index.html.eex:26 #: lib/block_scout_web/templates/block_transaction/index.html.eex:26
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: lib/block_scout_web/templates/block_transaction/index.html.eex:35
#: lib/block_scout_web/templates/chain/show.html.eex:71 #: lib/block_scout_web/templates/chain/show.html.eex:69
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35
#: lib/block_scout_web/views/address_view.ex:212 #: lib/block_scout_web/views/address_view.ex:212
msgid "Transactions" msgid "Transactions"
@ -1069,7 +1070,7 @@ msgid "View All Blocks →"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:70 #: lib/block_scout_web/templates/chain/show.html.eex:68
msgid "View All Transactions →" msgid "View All Transactions →"
msgstr "" msgstr ""

@ -41,7 +41,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:21 #: lib/block_scout_web/templates/block/overview.html.eex:21
#: lib/block_scout_web/templates/chain/_block.html.eex:9 #: lib/block_scout_web/templates/chain/_block.html.eex:10
msgid "%{count} Transactions" msgid "%{count} Transactions"
msgstr "" msgstr ""
@ -555,7 +555,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:37 #: lib/block_scout_web/templates/block/_tile.html.eex:37
#: lib/block_scout_web/templates/block/overview.html.eex:99 #: lib/block_scout_web/templates/block/overview.html.eex:99
#: lib/block_scout_web/templates/chain/_block.html.eex:13 #: lib/block_scout_web/templates/chain/_block.html.eex:14
msgid "Miner" msgid "Miner"
msgstr "" msgstr ""
@ -575,6 +575,7 @@ msgid "More internal transactions have come in"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:72
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:14 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:14
#: lib/block_scout_web/templates/transaction/index.html.eex:14 #: lib/block_scout_web/templates/transaction/index.html.eex:14
msgid "More transactions have come in" msgid "More transactions have come in"
@ -989,7 +990,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:23 #: lib/block_scout_web/templates/block_transaction/index.html.eex:23
#: lib/block_scout_web/templates/block_transaction/index.html.eex:26 #: lib/block_scout_web/templates/block_transaction/index.html.eex:26
#: lib/block_scout_web/templates/block_transaction/index.html.eex:35 #: lib/block_scout_web/templates/block_transaction/index.html.eex:35
#: lib/block_scout_web/templates/chain/show.html.eex:71 #: lib/block_scout_web/templates/chain/show.html.eex:69
#: lib/block_scout_web/templates/layout/_topnav.html.eex:35 #: lib/block_scout_web/templates/layout/_topnav.html.eex:35
#: lib/block_scout_web/views/address_view.ex:212 #: lib/block_scout_web/views/address_view.ex:212
msgid "Transactions" msgid "Transactions"
@ -1069,7 +1070,7 @@ msgid "View All Blocks →"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:70 #: lib/block_scout_web/templates/chain/show.html.eex:68
msgid "View All Transactions →" msgid "View All Transactions →"
msgstr "" msgstr ""

Loading…
Cancel
Save