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', () => {
const state = initialState
@ -10,58 +10,48 @@ test('CHANNEL_DISCONNECTED', () => {
expect(output.channelDisconnected).toBe(true)
})
describe('PAGE_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([])
})
describe('ELEMENTS_LOAD', () => {
test('page 1 with skipped blocks', () => {
const state = initialState
window.localized = {}
const state = Object.assign({}, initialState, {
beyondPageOne: false
})
const action = {
type: 'PAGE_LOAD',
beyondPageOne: false,
blockNumbers: [4, 1]
type: 'ELEMENTS_LOAD',
blocks: [
{ blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 1, blockHtml: 'test 1' }
]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(false)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
expect(output.blocks).toEqual([
{ blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 3, blockHtml: placeHolderBlock(3) },
{ blockNumber: 2, blockHtml: placeHolderBlock(2) },
{ blockNumber: 1, blockHtml: 'test 1' }
])
})
test('page 2 with skipped blocks', () => {
const state = initialState
window.localized = {}
const state = Object.assign({}, initialState, {
beyondPageOne: true
})
const action = {
type: 'PAGE_LOAD',
beyondPageOne: true,
blockNumbers: [4, 1]
type: 'ELEMENTS_LOAD',
blocks: [
{ blockNumber: 4, blockHtml: 'test 4' },
{ blockNumber: 1, blockHtml: 'test 1' }
]
}
const output = reducer(state, action)
expect(output.beyondPageOne).toBe(true)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
expect(output.blocks).toEqual([
{ blockNumber: 4, blockHtml: 'test 4' },
{ 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)
expect(output.newBlock).toBe('test')
expect(output.blockNumbers).toEqual([1])
expect(output.blocks).toEqual([
{ blockNumber: 1, blockHtml: 'test' }
])
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
@ -91,48 +82,37 @@ describe('RECEIVED_NEW_BLOCK', () => {
}
const output = reducer(state, action)
expect(output.newBlock).toBe(null)
expect(output.blockNumbers).toEqual([])
expect(output.skippedBlockNumbers).toEqual([])
expect(output.blocks).toEqual([])
})
test('inserts place holders if block received out of order', () => {
window.localized = {}
const state = Object.assign({}, initialState, {
blockNumbers: [2]
blocks: [
{ blockNumber: 2, blockHtml: 'test 2' }
]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
blockHtml: 'test5',
blockHtml: 'test 5',
blockNumber: 5
}
}
const output = reducer(state, action)
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: [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])
expect(output.blocks).toEqual([
{ blockNumber: 5, blockHtml: 'test 5' },
{ blockNumber: 4, blockHtml: placeHolderBlock(4) },
{ blockNumber: 3, blockHtml: placeHolderBlock(3) },
{ blockNumber: 2, blockHtml: 'test 2' }
])
})
test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4]
blocks: [
{ blockNumber: 5, blockHtml: 'test 5' },
{ blockNumber: 4, blockHtml: 'test 4' }
]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -143,23 +123,34 @@ describe('RECEIVED_NEW_BLOCK', () => {
}
const output = reducer(state, action)
expect(output.newBlock).toBe('test5')
expect(output.blockNumbers).toEqual([5, 4])
expect(output.blocks).toEqual([
{ blockNumber: 5, blockHtml: 'test5' },
{ blockNumber: 4, blockHtml: 'test 4' }
])
})
test('skips if new block height is lower than lowest on page', () => {
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 = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
blockHtml: 'test1',
blockNumber: 1
blockNumber: 1,
blockHtml: 'test 1'
}
}
const output = reducer(state, action)
expect(output.newBlock).toBe(null)
expect(output.blockNumbers).toEqual([5, 4, 3, 2])
expect(output.blocks).toEqual([
{ 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', () => {
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([])
})
describe('ELEMENTS_LOAD', () => {
test('loads with skipped blocks', () => {
window.localized = {}
const state = initialState
const action = {
type: 'PAGE_LOAD',
blockNumbers: [4, 1]
type: 'ELEMENTS_LOAD',
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)
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 2])
expect(output.blocks).toEqual([
{ 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', () => {
const state = Object.assign({}, initialState, {
averageBlockTime: '6 seconds',
blockNumbers: [1],
newBlock: 'last new block'
blocks: [
{ blockNumber: 1, chainBlockHtml: 'test 1' },
{ blockNumber: 0, chainBlockHtml: 'test 0' }
]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
@ -58,121 +59,138 @@ describe('RECEIVED_NEW_BLOCK', () => {
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toEqual('new block')
expect(output.blockNumbers).toEqual([2, 1])
expect(output.blocks).toEqual([
{ blockNumber: 2, chainBlockHtml: 'new block', averageBlockTime: '5 seconds' },
{ blockNumber: 1, chainBlockHtml: 'test 1' }
])
})
test('inserts place holders if block received out of order', () => {
window.localized = {}
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 = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
averageBlockTime: '5 seconds',
chainBlockHtml: 'test5',
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
chainBlockHtml: 'test 6',
blockNumber: 6
}
}
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe('test2')
expect(output.blockNumbers).toEqual([4, 3, 2, 1])
expect(output.skippedBlockNumbers).toEqual([3, 1])
expect(output.blocks).toEqual([
{ blockNumber: 6, chainBlockHtml: 'test 6' },
{ blockNumber: 5, chainBlockHtml: placeHolderBlock(5) },
{ blockNumber: 4, chainBlockHtml: placeHolderBlock(4) },
{ blockNumber: 3, chainBlockHtml: 'test 3' }
])
})
test('replaces duplicated block', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4]
blocks: [
{ blockNumber: 5, chainBlockHtml: 'test 5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' }
]
})
const action = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
averageBlockTime: '5 seconds',
chainBlockHtml: 'test5',
blockNumber: 5
}
}
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe('test5')
expect(output.blockNumbers).toEqual([5, 4])
expect(output.blocks).toEqual([
{ blockNumber: 5, chainBlockHtml: 'test5' },
{ blockNumber: 4, chainBlockHtml: 'test 4' }
])
})
test('skips if new block height is lower than lowest on page', () => {
window.localized = {}
const state = Object.assign({}, initialState, {
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 = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
averageBlockTime: '9 seconds',
chainBlockHtml: 'test1',
blockNumber: 1
blockNumber: 1,
chainBlockHtml: 'test 1'
}
}
const output = reducer(state, action)
expect(output.averageBlockTime).toEqual('5 seconds')
expect(output.newBlock).toBe(null)
expect(output.blockNumbers).toEqual([5, 4, 3, 2])
expect(output.blocks).toEqual([
{ 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', () => {
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2],
skippedBlockNumbers: [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 = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
chainBlockHtml: 'test6',
chainBlockHtml: 'test 6',
blockNumber: 6
}
}
const output = reducer(state, action)
expect(output.newBlock).toBe('test6')
expect(output.blockNumbers).toEqual([6, 5, 4, 3])
expect(output.skippedBlockNumbers).toEqual([4, 3])
expect(output.blocks).toEqual([
{ blockNumber: 6, chainBlockHtml: 'test 6' },
{ 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', () => {
window.localized = {}
const state = Object.assign({}, initialState, {
blockNumbers: [5, 4, 3, 2],
skippedBlockNumbers: [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 = {
type: 'RECEIVED_NEW_BLOCK',
msg: {
chainBlockHtml: 'test10',
blockNumber: 10
blockNumber: 10,
chainBlockHtml: 'test 10'
}
}
const output = reducer(state, action)
expect(output.newBlock).toBe('test10')
expect(output.blockNumbers).toEqual([10, 9, 8, 7])
expect(output.skippedBlockNumbers).toEqual([9, 8, 7])
expect(output.blocks).toEqual([
{ blockNumber: 10, chainBlockHtml: 'test 10' },
{ 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)
})
describe('RECEIVED_NEW_TRANSACTION', () => {
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHtml: 'test'
}
}]
}
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)
})
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, {
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 = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHtml: 'test 2'
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [
{ 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)
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'
test('CHANNEL_DISCONNECTED', () => {
@ -22,8 +23,11 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactions).toEqual([{
transactionHash: '0x00',
transactionHtml: 'test'
}])
expect(output.pendingTransactionsBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(1)
})
test('large batch of transactions', () => {
@ -67,13 +71,16 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(11)
expect(output.pendingTransactions).toEqual([])
expect(output.pendingTransactionsBatch.length).toEqual(11)
expect(output.pendingTransactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'],
pendingTransactions: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
}],
pendingTransactionCount: 1
})
const action = {
@ -85,13 +92,16 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactions).toEqual([
{ transactionHash: '0x02', transactionHtml: 'test 2' },
{ transactionHash: '0x01', transactionHtml: 'test 1' }
])
expect(output.pendingTransactionsBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
test('single transaction after large batch of transactions', () => {
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 = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
@ -102,12 +112,12 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(12)
expect(output.pendingTransactions).toEqual([])
expect(output.pendingTransactionsBatch.length).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
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 = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
@ -148,8 +158,8 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.newPendingTransactionHashesBatch.length).toEqual(22)
expect(output.pendingTransactions).toEqual([])
expect(output.pendingTransactionsBatch.length).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
@ -164,7 +174,7 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactions).toEqual([])
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
@ -180,28 +190,50 @@ describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactions).toEqual([])
expect(output.pendingTransactionCount).toEqual(2)
})
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction collated', () => {
const state = { ...initialState, pendingTransactionCount: 2 }
const state = Object.assign({}, initialState, {
pendingTransactionCount: 2,
pendingTransactions: [{
transactionHash: '0x00',
transactionHtml: 'old'
}]
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x00'
transactionHash: '0x00',
transactionHtml: 'new'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toBe(1)
expect(output.newTransactionHashes).toEqual(['0x00'])
expect(output.pendingTransactions).toEqual([{
transactionHash: '0x00',
transactionHtml: 'new'
}])
})
test('single transaction collated after batch', () => {
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 = {
type: 'RECEIVED_NEW_TRANSACTION',
@ -211,8 +243,8 @@ describe('RECEIVED_NEW_TRANSACTION', () => {
}
const output = reducer(state, action)
expect(output.newPendingTransactionHashesBatch.length).toEqual(10)
expect(output.newPendingTransactionHashesBatch).not.toContain('0x01')
expect(output.pendingTransactionsBatch.length).toEqual(10)
expect(_.map(output.pendingTransactionsBatch, 'transactionHash')).not.toContain('0x01')
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {

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

@ -1,105 +1,78 @@
import $ from 'jquery'
import _ from 'lodash'
import { createStore as reduxCreateStore } from 'redux'
import morph from 'nanomorph'
import { updateAllAges } from './lib/from_now'
import { updateAllAges } from './from_now'
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 buildFullBlockList (blockNumbers) {
const newestBlock = _.first(blockNumbers)
const oldestBlock = _.last(blockNumbers)
return skippedBlockListBuilder([], newestBlock + 1, oldestBlock - 1)
}
export function initRedux (reducer, { main, render, debug } = {}) {
if (!reducer) {
console.error('initRedux: You need a reducer to initialize Redux.')
return
}
if (!render) console.warn('initRedux: You have not passed a render function.')
// 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 default function (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] }))
const store = createStore(reducer)
if (debug) store.subscribe(() => { console.log(store.getState()) })
let oldState = store.getState()
if (render) {
store.subscribe(() => {
const state = store.getState()
render(state, oldState)
oldState = state
// 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)
})
}
if (main) main(store)
}
currentList = _.differenceBy(currentList, removals, 'id')
export function createStore (reducer) {
return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__())
}
// 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)
}))
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)
})
}
store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' }))
let oldState = store.getState()
store.subscribe(() => {
const state = store.getState()
renderElements(state, oldState)
oldState = state
// 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 skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) {
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) {
function slideDownAppend ($container, content) {
smarterSlideDown($(content), {
insert ($el) {
$container.append($el)
}
})
}
export function slideDownBefore ($container, content) {
function slideDownBefore ($container, content) {
smarterSlideDown($(content), {
insert ($el) {
$container.before($el)
}
})
}
export function slideUpRemove ($el) {
function slideUpRemove ($el) {
smarterSlideUp($el, {
complete () {
$el.remove()
@ -145,71 +118,3 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) {
$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 numeral from 'numeral'
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 { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
@ -233,7 +235,7 @@ const elements = {
}
},
'[data-selector="transactions-list"]': {
load ($el, store) {
load ($el) {
return {
transactions: $el.children().map((index, el) => ({
transactionHash: el.dataset.transactionHash,

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

@ -3,36 +3,32 @@ import _ from 'lodash'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
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'
const BATCH_THRESHOLD = 6
export const initialState = {
addressCount: null,
availableSupply: null,
averageBlockTime: null,
blockNumbers: [],
marketHistoryData: null,
newBlock: null,
newTransactions: [],
replaceBlock: null,
skippedBlockNumbers: [],
blocks: [],
transactions: [],
transactionsBatch: [],
transactionCount: null,
usdMarketCap: null
}
export function reducer (state = initialState, action) {
export const reducer = withMissingBlocks(baseReducer)
function baseReducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
const fullBlockNumberList = buildFullBlockList(action.blockNumbers)
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 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'RECEIVED_NEW_ADDRESS_COUNT': {
return Object.assign({}, state, {
@ -40,38 +36,17 @@ export function reducer (state = initialState, action) {
})
}
case 'RECEIVED_NEW_BLOCK': {
const blockNumber = parseInt(action.msg.blockNumber)
if (_.includes(state.blockNumbers, blockNumber)) {
if (!state.blocks.length || state.blocks[0].blockNumber < action.msg.blockNumber) {
return Object.assign({}, state, {
averageBlockTime: action.msg.averageBlockTime,
newBlock: action.msg.chainBlockHtml,
replaceBlock: blockNumber,
skippedBlockNumbers: _.without(state.skippedBlockNumbers, blockNumber)
blocks: [
action.msg,
...state.blocks.slice(0, -1)
]
})
} else if (blockNumber < _.last(state.blockNumbers)) {
return Object.assign({}, state, { newBlock: null })
} 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, {
averageBlockTime: action.msg.averageBlockTime,
blockNumbers: newBlockNumbers,
newBlock: action.msg.chainBlockHtml,
replaceBlock: null,
skippedBlockNumbers: newSkippedBlockNumbers
blocks: state.blocks.map((block) => block.blockNumber === action.msg.blockNumber ? action.msg : block)
})
}
}
@ -82,122 +57,176 @@ export function reducer (state = initialState, action) {
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, {
newTransactions: [
...state.newTransactions,
action.msg.transactionHtml
transactions: [
...action.msgs.reverse(),
...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:
return state
}
}
let chart
const $chainDetailsPage = $('[data-page="chain-details"]')
if ($chainDetailsPage.length) {
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) }))
function withMissingBlocks (reducer) {
return (...args) => {
const result = reducer(...args)
const blocksChannel = socket.channel(`blocks:new_block`)
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) }))
if (!result.blocks || result.blocks.length < 2) return result
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`)
transactionsChannel.join()
transactionsChannel.on('transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }))
return Object.assign({}, result, {
blocks: _.rangeRight(minBlock, maxBlock + 1)
.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) {
const $addressCount = $('[data-selector="address-count"]')
const $averageBlockTime = $('[data-selector="average-block-time"]')
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))
})
render ($el, state, oldState) {
if (oldState.availableSupply === state.availableSupply && oldState.marketHistoryData === state.marketHistoryData) return
chart.update(state.availableSupply, state.marketHistoryData)
}
},
'[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) {
const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length)
$transactionsList
.children()
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length)
.remove()
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
},
'[data-selector="average-block-time"]': {
render ($el, state, oldState) {
if (oldState.averageBlockTime === state.averageBlockTime) return
$el.empty().append(state.averageBlockTime)
}
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) {
return `
<div class="col-lg-3 fade-up-blocks-chain">
${blockHtml}
</div>
`
const $chainDetailsPage = $('[data-page="chain-details"]')
if ($chainDetailsPage.length) {
const store = createStore(reducer)
connectElements({ store, elements })
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 `
<div
class="col-lg-3 fade-up-blocks-chain"
style="min-height: 100px;"
data-selector="place-holder"
data-block-number="${blockNumber}"
>
<div
class="tile tile-type-block d-flex align-items-center fade-up"
style="height: 100px;"
data-selector="place-holder"
data-block-number="${blockNumber}"
>
<span class="loading-spinner-small ml-1 mr-4">
<span class="loading-spinner-block-1"></span>

@ -4,27 +4,28 @@ import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
const BATCH_THRESHOLD = 10
export const initialState = {
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
channelDisconnected: false,
newPendingTransactions: [],
newTransactionHashes: [],
pendingTransactionCount: null
pendingTransactionCount: null,
pendingTransactions: [],
pendingTransactionsBatch: [],
beyondPageOne: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
@ -35,9 +36,9 @@ export function reducer (state = initialState, action) {
if (state.channelDisconnected) return state
return Object.assign({}, state, {
newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash),
pendingTransactionCount: state.pendingTransactionCount - 1,
newTransactionHashes: [action.msg.transactionHash]
pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? action.msg : transaction),
pendingTransactionsBatch: state.pendingTransactionsBatch.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash),
pendingTransactionCount: state.pendingTransactionCount - 1
})
}
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.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) {
if (!state.pendingTransactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
..._.map(action.msgs, 'transactionHtml')
pendingTransactions: [
...action.msgs.reverse(),
...state.pendingTransactions
],
pendingTransactionCount
})
} else {
return Object.assign({}, state, {
newPendingTransactionHashesBatch: [
...state.newPendingTransactionHashesBatch,
..._.map(action.msgs, 'transactionHash')
pendingTransactionsBatch: [
...action.msgs.reverse(),
...state.pendingTransactionsBatch
],
pendingTransactionCount
})
}
}
case 'REMOVE_PENDING_TRANSACTION': {
return Object.assign({}, state, {
pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash)
})
}
default:
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"]')
if ($transactionPendingListPage.length) {
initRedux(reducer, {
main (store) {
const store = createStore(reducer)
store.dispatch({
type: 'PAGE_LOAD',
pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt
})
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })
)
transactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
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`)
pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
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()
}
}
})
pendingTransactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
pendingTransactionsChannel.on('pending_transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})))
}

@ -1,8 +1,9 @@
import $ from 'jquery'
import _ from 'lodash'
import humps from 'humps'
import numeral from 'numeral'
import socket from '../socket'
import { initRedux } from '../utils'
import { createStore, connectElements } from '../lib/redux_helpers.js'
export const initialState = {
blockNumber: null,
@ -11,10 +12,8 @@ export const initialState = {
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
blockNumber: parseInt(action.blockNumber, 10)
})
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'RECEIVED_NEW_BLOCK': {
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"]')
if ($transactionDetailsPage.length) {
initRedux(reducer, {
main (store) {
const store = createStore(reducer)
connectElements({ store, elements })
const blocksChannel = socket.channel(`blocks:new_block`, {})
const $transactionBlockNumber = $('[data-selector="block-number"]')
store.dispatch({
type: 'PAGE_LOAD',
blockNumber: $transactionBlockNumber.text()
})
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 transactionChannel = socket.channel(`transactions:${transactionHash}`, {})
transactionChannel.join()
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 numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { batchChannel, initRedux, slideDownPrepend } from '../utils'
import { createStore, connectElements } from '../lib/redux_helpers.js'
import { batchChannel } from '../lib/utils'
import listMorph from '../lib/list_morph'
const BATCH_THRESHOLD = 10
export const initialState = {
batchCountAccumulator: 0,
beyondPageOne: null,
channelDisconnected: false,
newTransactions: [],
transactionCount: null
transactionCount: null,
transactions: [],
transactionsBatch: [],
beyondPageOne: null
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'PAGE_LOAD': {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
transactionCount: numeral(action.transactionCount).value()
})
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, _.omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
channelDisconnected: true,
batchCountAccumulator: 0
transactionsBatch: []
})
}
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.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
if (!state.transactionsBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newTransactions: [
...state.newTransactions,
..._.map(action.msgs, 'transactionHtml')
transactions: [
...action.msgs.reverse(),
...state.transactions
],
transactionCount
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
transactionsBatch: [
...action.msgs.reverse(),
...state.transactionsBatch
],
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"]')
if ($transactionListPage.length) {
initRedux(reducer, {
main (store) {
const store = createStore(reducer)
store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
})
connectElements({ store, elements })
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
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()
}
}
})
transactionsChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
transactionsChannel.on('transaction', batchChannel((msgs) => store.dispatch({
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: humps.camelizeKeys(msgs)
})))
}

@ -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(
@block,
class: "tile-title",
@ -17,4 +18,5 @@
address: @block.miner,
contract: false %>
</span>
</div>
</div>

@ -57,9 +57,7 @@
<h2 class="card-title"><%= gettext "Blocks" %></h2>
<div class="row" data-selector="chain-block-list">
<%= for block <- @blocks do %>
<div class="col-lg-3 fade-up-blocks-chain">
<%= render BlockScoutWeb.ChainView, "_block.html", block: block %>
</div>
<% end %>
</div>
</div>
@ -69,6 +67,11 @@
<div class="card-body">
<%= 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>
<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">
<%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %>

@ -41,7 +41,7 @@ msgstr ""
#, elixir-format
#: 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"
msgstr ""
@ -555,7 +555,7 @@ msgstr ""
#, elixir-format
#: 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/chain/_block.html.eex:13
#: lib/block_scout_web/templates/chain/_block.html.eex:14
msgid "Miner"
msgstr ""
@ -575,6 +575,7 @@ msgid "More internal transactions have come in"
msgstr ""
#, 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/transaction/index.html.eex:14
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:26
#: 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/views/address_view.ex:212
msgid "Transactions"
@ -1069,7 +1070,7 @@ msgid "View All Blocks →"
msgstr ""
#, 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 →"
msgstr ""

@ -41,7 +41,7 @@ msgstr ""
#, elixir-format
#: 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"
msgstr ""
@ -555,7 +555,7 @@ msgstr ""
#, elixir-format
#: 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/chain/_block.html.eex:13
#: lib/block_scout_web/templates/chain/_block.html.eex:14
msgid "Miner"
msgstr ""
@ -575,6 +575,7 @@ msgid "More internal transactions have come in"
msgstr ""
#, 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/transaction/index.html.eex:14
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:26
#: 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/views/address_view.ex:212
msgid "Transactions"
@ -1069,7 +1070,7 @@ msgid "View All Blocks →"
msgstr ""
#, 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 →"
msgstr ""

Loading…
Cancel
Save