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 `
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': { - return Object.assign({}, state, { - newTransactions: [ - ...state.newTransactions, - action.msg.transactionHtml - ], - transactionCount: state.transactionCount + 1 - }) + 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, { + transactions: [ + ...action.msgs.reverse(), + ...state.transactions.slice(0, -1 * action.msgs.length) + ], + 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) })) - - 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) })) +function withMissingBlocks (reducer) { + return (...args) => { + const result = reducer(...args) + + 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)) - }) - } - } - updateAllAges() - } - 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() + 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()) + } + }, + '[data-selector="address-count"]': { + render ($el, state, oldState) { + if (oldState.addressCount === state.addressCount) return + $el.empty().append(state.addressCount) + } + }, + '[data-selector="average-block-time"]': { + render ($el, state, oldState) { + if (oldState.averageBlockTime === state.averageBlockTime) return + $el.empty().append(state.averageBlockTime) + } + }, + '[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() } - if (oldState.availableSupply !== state.availableSupply || oldState.marketHistoryData !== state.marketHistoryData) { - chart.update(state.availableSupply, state.marketHistoryData) + }, + 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 ` -
- ${blockHtml} -
- ` +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 `
diff --git a/apps/block_scout_web/assets/js/pages/pending_transactions.js b/apps/block_scout_web/assets/js/pages/pending_transactions.js index 75c3ef8d0f..ce2b306d98 100644 --- a/apps/block_scout_web/assets/js/pages/pending_transactions.js +++ b/apps/block_scout_web/assets/js/pages/pending_transactions.js @@ -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 $transactionPendingListPage = $('[data-page="transaction-pending-list"]') -if ($transactionPendingListPage.length) { - initRedux(reducer, { - main (store) { - store.dispatch({ - type: 'PAGE_LOAD', - pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(), - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt - }) - 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) }) - ) - 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 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"]') - 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) { + if (state.pendingTransactionsBatch.length) { $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format() + $el[0].innerHTML = numeral(state.pendingTransactionsBatch.length).format() } else { $channelBatching.hide() } - if (oldState.newPendingTransactions !== state.newPendingTransactions) { - const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length) - slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join('')) - - updateAllAges() + } + }, + '[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) { + const store = createStore(reducer) + store.dispatch({ + type: 'PAGE_LOAD', + 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) + }) + 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) + }))) } diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js index 335a59e974..cd247d530a 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -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 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) })) + const store = createStore(reducer) + connectElements({ store, elements }) - 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"]') + const blocksChannel = socket.channel(`blocks:new_block`, {}) + blocksChannel.join() + blocksChannel.on('new_block', (msg) => store.dispatch({ + type: 'RECEIVED_NEW_BLOCK', + msg: humps.camelizeKeys(msg) + })) - if (oldState.confirmations !== state.confirmations) { - $blockConfirmations.empty().append(numeral(state.confirmations).format()) - } - } - }) + const transactionHash = $transactionDetailsPage[0].dataset.pageTransactionHash + const transactionChannel = socket.channel(`transactions:${transactionHash}`, {}) + transactionChannel.join() + transactionChannel.on('collated', () => window.location.reload()) } diff --git a/apps/block_scout_web/assets/js/pages/transactions.js b/apps/block_scout_web/assets/js/pages/transactions.js index 04544e83b4..937c14881c 100644 --- a/apps/block_scout_web/assets/js/pages/transactions.js +++ b/apps/block_scout_web/assets/js/pages/transactions.js @@ -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 $transactionListPage = $('[data-page="transaction-list"]') -if ($transactionListPage.length) { - initRedux(reducer, { - main (store) { - store.dispatch({ - type: 'PAGE_LOAD', - transactionCount: $('[data-selector="transaction-count"]').text(), - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index - }) - 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 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"]') - 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() + 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) { + const store = createStore(reducer) + store.dispatch({ + type: 'PAGE_LOAD', + 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) + }))) } diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex index cfcce5f33e..88443ccd0e 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/_block.html.eex @@ -1,20 +1,22 @@ -
- <%= link( - @block, - class: "tile-title", - to: block_path(BlockScoutWeb.Endpoint, :show, @block), - "data-selector": "block-number" - ) %> -
- <%= gettext("%{count} Transactions", count: Enum.count(@block.transactions)) %> - -
- - <%= gettext "Miner" %> +
+
+ <%= link( + @block, + class: "tile-title", + to: block_path(BlockScoutWeb.Endpoint, :show, @block), + "data-selector": "block-number" + ) %> +
+ <%= gettext("%{count} Transactions", count: Enum.count(@block.transactions)) %> + +
+ + <%= gettext "Miner" %> - <%= render BlockScoutWeb.AddressView, - "_link.html", - address: @block.miner, - contract: false %> - + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @block.miner, + contract: false %> + +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex index c0f4051811..39752d33ab 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/show.html.eex @@ -57,9 +57,7 @@

<%= gettext "Blocks" %>

<%= for block <- @blocks do %> -
- <%= render BlockScoutWeb.ChainView, "_block.html", block: block %> -
+ <%= render BlockScoutWeb.ChainView, "_block.html", block: block %> <% end %>
@@ -69,6 +67,11 @@
<%= link(gettext("View All Transactions →"), to: transaction_path(BlockScoutWeb.Endpoint, :index), class: "button button-secondary button-xsmall float-right") %>

<%= gettext "Transactions" %>

+ <%= for transaction <- @transactions do %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index f2daa6151b..eba53626ff 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -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 "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 6f3b430f32..8aac131ef9 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -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 ""