diff --git a/apps/block_scout_web/assets/__tests__/pages/blocks.js b/apps/block_scout_web/assets/__tests__/pages/blocks.js index 2ddad6a5a3..099bbd762a 100644 --- a/apps/block_scout_web/assets/__tests__/pages/blocks.js +++ b/apps/block_scout_web/assets/__tests__/pages/blocks.js @@ -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' } + ]) }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/chain.js b/apps/block_scout_web/assets/__tests__/pages/chain.js index 9ff632f2d4..a27487756d 100644 --- a/apps/block_scout_web/assets/__tests__/pages/chain.js +++ b/apps/block_scout_web/assets/__tests__/pages/chain.js @@ -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] - }) - 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] + 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: '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) }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js index 25d03aed61..08361f7337 100644 --- a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js +++ b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js @@ -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, { diff --git a/apps/block_scout_web/assets/__tests__/pages/transactions.js b/apps/block_scout_web/assets/__tests__/pages/transactions.js index f45a4960d4..bc12aee212 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transactions.js +++ b/apps/block_scout_web/assets/__tests__/pages/transactions.js @@ -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) }) }) diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/lib/list_morph.js similarity index 56% rename from apps/block_scout_web/assets/js/utils.js rename to apps/block_scout_web/assets/js/lib/list_morph.js index 28f36b3cfd..4f3b0736c4 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/lib/list_morph.js @@ -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.') - - 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 - }) - } - if (main) main(store) -} - -export function createStore (reducer) { - return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) -} +// 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] })) -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 + // 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') -export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) { - for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i) - return skippedBlockNumbers -} + // 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 slideDownPrepend ($container, content) { - smarterSlideDown($(content), { - insert ($el) { - $container.prepend($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 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) -} diff --git a/apps/block_scout_web/assets/js/lib/redux_helpers.js b/apps/block_scout_web/assets/js/lib/redux_helpers.js new file mode 100644 index 0000000000..d163e05ef1 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/redux_helpers.js @@ -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' })) +} diff --git a/apps/block_scout_web/assets/js/lib/utils.js b/apps/block_scout_web/assets/js/lib/utils.js new file mode 100644 index 0000000000..24caef96be --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/utils.js @@ -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) +} diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index fb969ac73f..32c6e6f464 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -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, diff --git a/apps/block_scout_web/assets/js/pages/blocks.js b/apps/block_scout_web/assets/js/pages/blocks.js index 550aafda28..1bf4b32209 100644 --- a/apps/block_scout_web/assets/js/pages/blocks.js +++ b/apps/block_scout_web/assets/js/pages/blocks.js @@ -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() - }) - 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) +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 - 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)) - }) - } - slideDownBefore($(`[data-block-number="${state.blockNumbers[0] - 1}"]`), state.newBlock) - } - updateAllAges() + return Object.assign({}, result, { + blocks: _.rangeRight(minBlock, maxBlock + 1) + .map((blockNumber) => _.find(result.blocks, ['blockNumber', blockNumber]) || { + blockNumber, + blockHtml: placeHolderBlock(blockNumber) + }) + }) + } +} + +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + }, + '[data-selector="blocks-list"]': { + load ($el) { + return { + blocks: $el.children().map((index, el) => ({ + blockNumber: parseInt(el.dataset.blockNumber), + blockHtml: el.outerHTML + })).toArray() } + }, + 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 `