diff --git a/.tool-versions b/.tool-versions
index 2f81e457af..bf2689b13f 100644
--- a/.tool-versions
+++ b/.tool-versions
@@ -1,3 +1,3 @@
elixir 1.7.1
erlang 21.0.4
-nodejs 10.5.0
+nodejs 10.11.0
diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js
index 20da637dae..b4ba0f62b7 100644
--- a/apps/block_scout_web/assets/__tests__/pages/transaction.js
+++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js
@@ -13,6 +13,229 @@ test('RECEIVED_NEW_BLOCK', () => {
expect(output.confirmations).toBe(4)
})
+describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
+ test('single transaction', () => {
+ const state = initialState
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x00',
+ transactionHtml: 'test'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual(['test'])
+ expect(output.newPendingTransactionHashesBatch.length).toEqual(0)
+ expect(output.pendingTransactionCount).toEqual(1)
+ })
+ test('large batch of transactions', () => {
+ const state = initialState
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x01',
+ transactionHtml: 'test 1'
+ },{
+ transactionHash: '0x02',
+ transactionHtml: 'test 2'
+ },{
+ transactionHash: '0x03',
+ transactionHtml: 'test 3'
+ },{
+ transactionHash: '0x04',
+ transactionHtml: 'test 4'
+ },{
+ transactionHash: '0x05',
+ transactionHtml: 'test 5'
+ },{
+ transactionHash: '0x06',
+ transactionHtml: 'test 6'
+ },{
+ transactionHash: '0x07',
+ transactionHtml: 'test 7'
+ },{
+ transactionHash: '0x08',
+ transactionHtml: 'test 8'
+ },{
+ transactionHash: '0x09',
+ transactionHtml: 'test 9'
+ },{
+ transactionHash: '0x10',
+ transactionHtml: 'test 10'
+ },{
+ transactionHash: '0x11',
+ transactionHtml: 'test 11'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual([])
+ expect(output.newPendingTransactionHashesBatch.length).toEqual(11)
+ expect(output.pendingTransactionCount).toEqual(11)
+ })
+ test('single transaction after single transaction', () => {
+ const state = Object.assign({}, initialState, {
+ newPendingTransactions: ['test 1'],
+ pendingTransactionCount: 1
+ })
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x02',
+ transactionHtml: 'test 2'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
+ expect(output.newPendingTransactionHashesBatch.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']
+ })
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x12',
+ transactionHtml: 'test 12'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual([])
+ expect(output.newPendingTransactionHashesBatch.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']
+ })
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x12',
+ transactionHtml: 'test 12'
+ },{
+ transactionHash: '0x13',
+ transactionHtml: 'test 13'
+ },{
+ transactionHash: '0x14',
+ transactionHtml: 'test 14'
+ },{
+ transactionHash: '0x15',
+ transactionHtml: 'test 15'
+ },{
+ transactionHash: '0x16',
+ transactionHtml: 'test 16'
+ },{
+ transactionHash: '0x17',
+ transactionHtml: 'test 17'
+ },{
+ transactionHash: '0x18',
+ transactionHtml: 'test 18'
+ },{
+ transactionHash: '0x19',
+ transactionHtml: 'test 19'
+ },{
+ transactionHash: '0x20',
+ transactionHtml: 'test 20'
+ },{
+ transactionHash: '0x21',
+ transactionHtml: 'test 21'
+ },{
+ transactionHash: '0x22',
+ transactionHtml: 'test 22'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual([])
+ expect(output.newPendingTransactionHashesBatch.length).toEqual(22)
+ })
+ test('after disconnection', () => {
+ const state = Object.assign({}, initialState, {
+ channelDisconnected: true
+ })
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x00',
+ transactionHtml: 'test'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual([])
+ expect(output.batchCountAccumulator).toEqual(0)
+ })
+ test('on page 2+', () => {
+ const state = Object.assign({}, initialState, {
+ beyondPageOne: true,
+ pendingTransactionCount: 1
+ })
+ const action = {
+ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
+ msgs: [{
+ transactionHash: '0x00',
+ transactionHtml: 'test'
+ }]
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactions).toEqual([])
+ expect(output.batchCountAccumulator).toEqual(0)
+ expect(output.pendingTransactionCount).toEqual(2)
+ })
+})
+
+describe('RECEIVED_NEW_TRANSACTION', () => {
+ test('single transaction collated', () => {
+ const state = { ...initialState, pendingTransactionCount: 2 }
+ const action = {
+ type: 'RECEIVED_NEW_TRANSACTION',
+ msg: {
+ transactionHash: '0x00'
+ }
+ }
+ const output = reducer(state, action)
+
+ expect(output.pendingTransactionCount).toBe(1)
+ expect(output.newTransactionHashes).toEqual(['0x00'])
+ })
+ test('single transaction collated after batch', () => {
+ const state = Object.assign({}, initialState, {
+ newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
+ })
+ const action = {
+ type: 'RECEIVED_NEW_TRANSACTION',
+ msg: {
+ transactionHash: '0x01'
+ }
+ }
+ const output = reducer(state, action)
+
+ expect(output.newPendingTransactionHashesBatch.length).toEqual(10)
+ expect(output.newPendingTransactionHashesBatch).not.toContain('0x01')
+ })
+ test('on page 2+', () => {
+ const state = Object.assign({}, initialState, {
+ beyondPageOne: true,
+ pendingTransactionCount: 2
+ })
+ const action = {
+ type: 'RECEIVED_NEW_TRANSACTION',
+ msg: {
+ transactionHash: '0x01'
+ }
+ }
+ const output = reducer(state, action)
+
+ expect(output.pendingTransactionCount).toEqual(1)
+ })
+})
+
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
@@ -79,7 +302,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
- newTransactions: [],
batchCountAccumulator: 11
})
const action = {
@@ -95,7 +317,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
- newTransactions: [],
batchCountAccumulator: 11
})
const action = {
@@ -146,7 +367,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
- beyondPageOne: true
+ beyondPageOne: true,
+ transactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@@ -158,5 +380,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
+ expect(output.transactionCount).toEqual(2)
})
})
diff --git a/apps/block_scout_web/assets/css/components/_animations.scss b/apps/block_scout_web/assets/css/components/_animations.scss
index 0db1f9c125..622f8de130 100644
--- a/apps/block_scout_web/assets/css/components/_animations.scss
+++ b/apps/block_scout_web/assets/css/components/_animations.scss
@@ -60,8 +60,20 @@
}
}
+@keyframes shrink-out {
+ 0% {
+ transform: scale(1);
+ opacity: 1;
+ }
+ 100% {
+ opacity: 0;
+ transform: scale(0.5);
+ }
+}
+
.fade-in {
- animation: fade-in 0.6s ease-out forwards;
+ opacity: 0;
+ animation: fade-in 0.6s ease-out 0.4s forwards;
}
.fade-up-blocks-chain {
@@ -84,3 +96,7 @@
animation: fade-up--mobile 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
}
+
+.shrink-out {
+ animation: shrink-out 0.45s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
+}
diff --git a/apps/block_scout_web/assets/css/components/_dropdown.scss b/apps/block_scout_web/assets/css/components/_dropdown.scss
index 84bc109cb8..d62e7820f2 100644
--- a/apps/block_scout_web/assets/css/components/_dropdown.scss
+++ b/apps/block_scout_web/assets/css/components/_dropdown.scss
@@ -4,3 +4,14 @@
width: 100%;
box-shadow: $box-shadow;
}
+
+.dropdown-search-icon {
+ top: 0.5rem;
+ left: 0.625rem;
+ pointer-events: none;
+ color: $gray-300;
+}
+
+.dropdown-search-field {
+ padding-left: 2rem;
+}
diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js
index caa01d32d4..b26832696f 100644
--- a/apps/block_scout_web/assets/js/app.js
+++ b/apps/block_scout_web/assets/js/app.js
@@ -32,6 +32,7 @@ import './lib/smart_contract/wei_ether_converter'
import './lib/pretty_json'
import './lib/try_api'
import './lib/token_balance_dropdown'
+import './lib/token_balance_dropdown_search'
import './lib/token_transfers_toggle'
import './lib/stop_propagation'
diff --git a/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js b/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js
new file mode 100644
index 0000000000..ce31e0cb90
--- /dev/null
+++ b/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js
@@ -0,0 +1,51 @@
+import $ from 'jquery'
+
+const stringContains = (query, string) => {
+ return string.toLowerCase().search(query) === -1
+}
+
+const hideUnmatchedToken = (query, token) => {
+ const $token = $(token)
+ const tokenName = $token.data('token-name')
+ const tokenSymbol = $token.data('token-symbol')
+
+ if (stringContains(query, tokenName) && stringContains(query, tokenSymbol)) {
+ $token.addClass('d-none')
+ } else {
+ $token.removeClass('d-none')
+ }
+}
+
+const hideEmptyType = (container) => {
+ const $container = $(container)
+ const type = $container.data('token-type')
+ const countVisibleTokens = $container.children('[data-token-name]:not(.d-none)').length
+
+ if (countVisibleTokens === 0) {
+ $container.addClass('d-none')
+ } else {
+ $(`[data-number-of-tokens-by-type='${type}']`).empty().append(countVisibleTokens)
+ $container.removeClass('d-none')
+ }
+}
+
+const TokenBalanceDropdownSearch = (element, event) => {
+ const $element = $(element)
+ const $tokensCount = $element.find('[data-tokens-count]')
+ const $tokens = $element.find('[data-token-name]')
+ const $tokenTypes = $element.find('[data-token-type]')
+ const query = event.target.value.toLowerCase()
+
+ $tokens.each((_index, token) => hideUnmatchedToken(query, token))
+ $tokenTypes.each((_index, container) => hideEmptyType(container))
+
+ $tokensCount.html($tokensCount.html().replace(/\d+/g, $tokens.not('.d-none').length))
+}
+
+$('[data-token-balance-dropdown]').on('hidden.bs.dropdown', _event => {
+ $('[data-filter-dropdown-tokens]').val('').trigger('input')
+})
+
+$('[data-token-balance-dropdown]').on('input', function (event) {
+ TokenBalanceDropdownSearch(this, event)
+})
diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js
index c6fc48cbc9..08ed8fe023 100644
--- a/apps/block_scout_web/assets/js/pages/chain.js
+++ b/apps/block_scout_web/assets/js/pages/chain.js
@@ -4,7 +4,7 @@ import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { exchangeRateChannel, formatUsdValue } from '../lib/currency'
-import { batchChannel, initRedux } from '../utils'
+import { batchChannel, initRedux, slideDownPrepend } from '../utils'
import { createMarketHistoryChart } from '../lib/market_history_chart'
const BATCH_THRESHOLD = 10
@@ -131,7 +131,7 @@ if ($chainDetailsPage.length) {
.children()
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length)
.remove()
- $transactionsList.prepend(newTransactionsToInsert.reverse().join(''))
+ slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js
index 6598e33659..5468cef4a8 100644
--- a/apps/block_scout_web/assets/js/pages/transaction.js
+++ b/apps/block_scout_web/assets/js/pages/transaction.js
@@ -1,4 +1,5 @@
import $ from 'jquery'
+import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
@@ -10,12 +11,16 @@ const BATCH_THRESHOLD = 10
export const initialState = {
batchCountAccumulator: 0,
+ newPendingTransactionHashesBatch: [],
beyondPageOne: null,
blockNumber: null,
channelDisconnected: false,
confirmations: null,
+ newPendingTransactions: [],
newTransactions: [],
- transactionCount: null
+ newTransactionHashes: [],
+ transactionCount: null,
+ pendingTransactionCount: null
}
export function reducer (state = initialState, action) {
@@ -24,7 +29,8 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
blockNumber: parseInt(action.blockNumber, 10),
- transactionCount: numeral(action.transactionCount).value()
+ transactionCount: numeral(action.transactionCount).value(),
+ pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
}
case 'CHANNEL_DISCONNECTED': {
@@ -40,8 +46,46 @@ export function reducer (state = initialState, action) {
})
} else return state
}
+ case 'RECEIVED_NEW_TRANSACTION': {
+ if (state.channelDisconnected) return state
+
+ return Object.assign({}, state, {
+ newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash),
+ pendingTransactionCount: state.pendingTransactionCount - 1,
+ newTransactionHashes: [action.msg.transactionHash]
+ })
+ }
+ case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
+ if (state.channelDisconnected) return state
+
+ const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length
+
+ if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount })
+
+ if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) {
+ return Object.assign({}, state, {
+ newPendingTransactions: [
+ ...state.newPendingTransactions,
+ ..._.map(action.msgs, 'transactionHtml')
+ ],
+ pendingTransactionCount
+ })
+ } else {
+ return Object.assign({}, state, {
+ newPendingTransactionHashesBatch: [
+ ...state.newPendingTransactionHashesBatch,
+ ..._.map(action.msgs, 'transactionHash')
+ ],
+ pendingTransactionCount
+ })
+ }
+ }
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
- if (state.channelDisconnected || state.beyondPageOne) return state
+ if (state.channelDisconnected) return state
+
+ const transactionCount = state.transactionCount + action.msgs.length
+
+ if (state.beyondPageOne) return Object.assign({}, state, { transactionCount })
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
@@ -49,12 +93,12 @@ export function reducer (state = initialState, action) {
...state.newTransactions,
...action.msgs.map(({transactionHtml}) => transactionHtml)
],
- transactionCount: state.transactionCount + action.msgs.length
+ transactionCount
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
- transactionCount: state.transactionCount + action.msgs.length
+ transactionCount
})
}
}
@@ -91,23 +135,89 @@ if ($transactionDetailsPage.length) {
})
}
+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('new_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('new_pending_transaction', batchChannel((msgs) =>
+ store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
+ )
+ },
+ render (state, oldState) {
+ const $channelBatching = $('[data-selector="channel-batching-message"]')
+ const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
+ const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
+ const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]')
+ const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]')
+
+ if (state.channelDisconnected) $channelDisconnected.show()
+ if (oldState.pendingTransactionCount !== state.pendingTransactionCount) {
+ $pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format())
+ }
+ if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) {
+ const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`)
+ $transaction.addClass('shrink-out')
+ setTimeout(() => $transaction.slideUp({
+ complete: () => {
+ if ($pendingTransactionsList.children().length < 2 && state.pendingTransactionCount > 0) {
+ window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString()
+ } else {
+ $transaction.remove()
+ }
+ }
+ }), 400)
+ }
+ if (state.newPendingTransactionHashesBatch.length) {
+ $channelBatching.show()
+ $channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format()
+ } else {
+ $channelBatching.hide()
+ }
+ if (oldState.newPendingTransactions !== state.newPendingTransactions) {
+ const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length)
+ $pendingTransactionsList
+ .children()
+ .slice($pendingTransactionsList.children().length - newTransactionsToInsert.length,
+ $pendingTransactionsList.children().length
+ )
+ .remove()
+ prependWithClingBottom($pendingTransactionsList, newTransactionsToInsert.reverse().join(''))
+
+ updateAllAges()
+ }
+ }
+ })
+}
+
const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) {
initRedux(reducer, {
main (store) {
- const state = store.dispatch({
+ store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
})
- if (!state.beyondPageOne) {
- const transactionsChannel = socket.channel(`transactions:new_transaction`)
- transactionsChannel.join()
- transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
- transactionsChannel.on('new_transaction', batchChannel((msgs) =>
- store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
- )
- }
+ const transactionsChannel = socket.channel(`transactions:new_transaction`)
+ transactionsChannel.join()
+ transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
+ transactionsChannel.on('new_transaction', batchChannel((msgs) =>
+ store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
+ )
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js
index bbb21ba718..b0a2f89c3c 100644
--- a/apps/block_scout_web/assets/js/utils.js
+++ b/apps/block_scout_web/assets/js/utils.js
@@ -34,11 +34,16 @@ export function initRedux (reducer, { main, render, debug } = {}) {
if (main) main(store)
}
+export function slideDownPrepend ($el, content, callback) {
+ const $content = $(content)
+ $el.prepend($content.hide())
+ $content.slideDown({ complete: callback })
+}
export function prependWithClingBottom ($el, content) {
function userAtTop () {
return window.scrollY < $('[data-selector="navbar"]').outerHeight()
}
- if (userAtTop()) return $el.prepend(content)
+ if (userAtTop()) return slideDownPrepend($el, content)
let isAnimating
function setIsAnimating () {
@@ -67,8 +72,9 @@ export function prependWithClingBottom ($el, content) {
$el.off('animationstart', setIsAnimating)
$el.off('animationend animationcancel', stopClinging)
}
- $el.on('animationend animationcancel', stopClinging)
- setTimeout(() => !isAnimating && stopClinging(), 100)
- return $el.prepend(content)
+ return slideDownPrepend($el, content, () => {
+ $el.on('animationend animationcancel', stopClinging)
+ setTimeout(() => !isAnimating && stopClinging(), 100)
+ })
}
diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json
index 6f0ffa5568..83b0b292fd 100644
--- a/apps/block_scout_web/assets/package-lock.json
+++ b/apps/block_scout_web/assets/package-lock.json
@@ -1481,9 +1481,9 @@
"dev": true
},
"bootstrap": {
- "version": "4.1.1",
- "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.1.tgz",
- "integrity": "sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg=="
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz",
+ "integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w=="
},
"brace-expansion": {
"version": "1.1.11",
diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json
index f363ff1246..a3c05cba5a 100644
--- a/apps/block_scout_web/assets/package.json
+++ b/apps/block_scout_web/assets/package.json
@@ -21,7 +21,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.1.0-4",
"bignumber.js": "^7.2.1",
- "bootstrap": "^4.1.0",
+ "bootstrap": "^4.1.3",
"chart.js": "^2.7.2",
"clipboard": "^2.0.1",
"humps": "^2.0.1",
diff --git a/apps/block_scout_web/assets/static/android-chrome-192x192.png b/apps/block_scout_web/assets/static/android-chrome-192x192.png
new file mode 100644
index 0000000000..5a5c281449
Binary files /dev/null and b/apps/block_scout_web/assets/static/android-chrome-192x192.png differ
diff --git a/apps/block_scout_web/assets/static/android-chrome-512x512.png b/apps/block_scout_web/assets/static/android-chrome-512x512.png
new file mode 100644
index 0000000000..0b4840d41a
Binary files /dev/null and b/apps/block_scout_web/assets/static/android-chrome-512x512.png differ
diff --git a/apps/block_scout_web/assets/static/apple-touch-icon.png b/apps/block_scout_web/assets/static/apple-touch-icon.png
new file mode 100644
index 0000000000..70a5198240
Binary files /dev/null and b/apps/block_scout_web/assets/static/apple-touch-icon.png differ
diff --git a/apps/block_scout_web/assets/static/browserconfig.xml b/apps/block_scout_web/assets/static/browserconfig.xml
new file mode 100644
index 0000000000..b3930d0f04
--- /dev/null
+++ b/apps/block_scout_web/assets/static/browserconfig.xml
@@ -0,0 +1,9 @@
+
+