From 7c04b864051016423fa0a54f31752b1bc7d6ea58 Mon Sep 17 00:00:00 2001 From: Nolan Lawson Date: Thu, 24 Oct 2019 19:03:10 -0700 Subject: [PATCH] fix: use smooth scroll polyfill in Chrome for scroll-to-top (#1601) * fix: use smooth scroll polyfill in Chrome for scroll-to-top * rename thunk to __thunk__ for safety --- bin/build-inline-script.js | 7 ++----- bin/terserOptions.js | 14 ++++++++++++++ .../dialog/components/MediaDialog.html | 2 +- .../shortcut/ScrollListShortcuts.html | 2 +- src/routes/_utils/emojiRegex.js | 4 ++-- src/routes/_utils/handleRegex.js | 4 ++-- src/routes/_utils/scrollIntoView.js | 2 +- src/routes/_utils/scrollToTop.js | 2 +- src/routes/_utils/smoothScroll.js | 13 ++++++++++--- src/routes/_utils/testStorage.js | 6 +++--- src/routes/_utils/thunk.js | 4 +++- src/routes/_utils/urlRegex.js | 4 ++-- src/routes/_utils/userAgent.js | 19 ++++++++++++------- src/routes/_workers/blurhash.js | 5 ++--- src/service-worker.js | 11 +++-------- webpack/terser.config.js | 13 ++----------- 16 files changed, 61 insertions(+), 51 deletions(-) create mode 100644 bin/terserOptions.js diff --git a/bin/build-inline-script.js b/bin/build-inline-script.js index ebadd1f4..ee986abc 100644 --- a/bin/build-inline-script.js +++ b/bin/build-inline-script.js @@ -8,6 +8,7 @@ import replace from 'rollup-plugin-replace' import fromPairs from 'lodash-es/fromPairs' import babel from 'rollup-plugin-babel' import { themes } from '../src/routes/_static/themes' +import terserOptions from './terserOptions' const writeFile = promisify(fs.writeFile) @@ -28,11 +29,7 @@ export async function buildInlineScript () { runtimeHelpers: true, presets: ['@babel/preset-env'] }), - !process.env.DEBUG && terser({ - mangle: true, - compress: true, - ecma: 8 - }) + !process.env.DEBUG && terser(terserOptions) ] }) const { output } = await bundle.generate({ diff --git a/bin/terserOptions.js b/bin/terserOptions.js new file mode 100644 index 00000000..cc3db731 --- /dev/null +++ b/bin/terserOptions.js @@ -0,0 +1,14 @@ +module.exports = { + ecma: 8, + mangle: true, + compress: { + pure_funcs: [ + 'console.log', // remove console logs in production + '__thunk__' // see thunk.js + ] + }, + output: { + comments: false + }, + safari10: true +} diff --git a/src/routes/_components/dialog/components/MediaDialog.html b/src/routes/_components/dialog/components/MediaDialog.html index 7f9d544a..04c0fa44 100644 --- a/src/routes/_components/dialog/components/MediaDialog.html +++ b/src/routes/_components/dialog/components/MediaDialog.html @@ -333,7 +333,7 @@ // smooth scroll, so disable smooth scrolling scroller.scrollLeft = scrollLeft } else { - smoothScroll(scroller, scrollLeft, true) + smoothScroll(scroller, scrollLeft, /* horizontal */ true, /* preferFast */ false) } } else { console.log('setting scrollLeft', scrollLeft) diff --git a/src/routes/_components/shortcut/ScrollListShortcuts.html b/src/routes/_components/shortcut/ScrollListShortcuts.html index d4e96cf9..566e180f 100644 --- a/src/routes/_components/shortcut/ScrollListShortcuts.html +++ b/src/routes/_components/shortcut/ScrollListShortcuts.html @@ -72,7 +72,7 @@ if (index === 0 && movement === -1) { activeItemKey = null this.set({ activeItemKey }) - smoothScroll(getScrollContainer(), 0) + smoothScroll(getScrollContainer(), 0, /* horizontal */ false, /* preferFast */ false) return } if (index === -1) { diff --git a/src/routes/_utils/emojiRegex.js b/src/routes/_utils/emojiRegex.js index ced53ece..54730ec1 100644 --- a/src/routes/_utils/emojiRegex.js +++ b/src/routes/_utils/emojiRegex.js @@ -1,4 +1,4 @@ import emojiRegex from 'emoji-regex/es2015/text' -import { thunk } from './thunk' +import { __thunk__ } from './thunk' -export const getEmojiRegex = thunk(emojiRegex) +export const getEmojiRegex = __thunk__(emojiRegex) diff --git a/src/routes/_utils/handleRegex.js b/src/routes/_utils/handleRegex.js index 0b4fe19e..d153582a 100644 --- a/src/routes/_utils/handleRegex.js +++ b/src/routes/_utils/handleRegex.js @@ -1,5 +1,5 @@ /* eslint-disable */ -import { thunk } from './thunk' +import { __thunk__ } from './thunk' -export const handleRegex = thunk(() => /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig) +export const handleRegex = __thunk__(() => /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig) /* eslint-enable */ diff --git a/src/routes/_utils/scrollIntoView.js b/src/routes/_utils/scrollIntoView.js index 181a232a..69c85292 100644 --- a/src/routes/_utils/scrollIntoView.js +++ b/src/routes/_utils/scrollIntoView.js @@ -66,5 +66,5 @@ export function scrollIntoViewIfNeeded (element) { } const scrollContainer = getScrollContainer() const scrollTop = scrollContainer.scrollTop - smoothScroll(scrollContainer, scrollTop + rect.top - scrollY) + smoothScroll(scrollContainer, scrollTop + rect.top - scrollY, /* horizontal */ false, /* preferFast */ false) } diff --git a/src/routes/_utils/scrollToTop.js b/src/routes/_utils/scrollToTop.js index 4b437ddb..790b7db9 100644 --- a/src/routes/_utils/scrollToTop.js +++ b/src/routes/_utils/scrollToTop.js @@ -8,7 +8,7 @@ export function scrollToTop (smooth) { return false } if (smooth) { - smoothScroll(scroller, 0) + smoothScroll(scroller, 0, /* horizontal */ false, /* preferFast */ true) } else { scroller.scrollTop = 0 } diff --git a/src/routes/_utils/smoothScroll.js b/src/routes/_utils/smoothScroll.js index 23060551..c8c2e3b0 100644 --- a/src/routes/_utils/smoothScroll.js +++ b/src/routes/_utils/smoothScroll.js @@ -1,4 +1,5 @@ import { store } from '../_store/store' +import { isChrome } from './userAgent' // via https://github.com/tootsuite/mastodon/blob/f59ed3a4fafab776b4eeb92f805dfe1fecc17ee3/app/javascript/mastodon/scroll.js const easingOutQuint = (x, t, b, c, d) => @@ -63,16 +64,22 @@ function testSupportsSmoothScroll () { export const hasNativeSmoothScroll = process.browser && testSupportsSmoothScroll() -export function smoothScroll (node, topOrLeft, horizontal) { +export function smoothScroll (node, topOrLeft, horizontal, preferFast) { if (store.get().reduceMotion) { - // don't do smooth-scroll at all for users who prefer reduced motion + console.log('smooth scroll: disabled') + // Don't do smooth-scroll at all for users who prefer reduced motion. node[horizontal ? 'scrollLeft' : 'scrollTop'] = topOrLeft - } else if (hasNativeSmoothScroll) { + } else if (hasNativeSmoothScroll && !(preferFast && isChrome())) { + // In some cases (e.g. scrolling to the top of the timeline), Chrome can take a really long time + // in their native smooth scroll implementation. If preferFast is true, just use the polyfill + // so we can control how long it takes. + console.log('smooth scroll: using native') return node.scrollTo({ [horizontal ? 'left' : 'top']: topOrLeft, behavior: 'smooth' }) } else { + console.log('smooth scroll: using polyfill') return smoothScrollPolyfill(node, horizontal ? 'scrollLeft' : 'scrollTop', topOrLeft) } } diff --git a/src/routes/_utils/testStorage.js b/src/routes/_utils/testStorage.js index 36bde6d6..2608d929 100644 --- a/src/routes/_utils/testStorage.js +++ b/src/routes/_utils/testStorage.js @@ -1,11 +1,11 @@ // LocalStorage and IDB may be disabled in private mode, when "blocking cookies" in Safari, // or other cases -import { thunk } from './thunk' +import { __thunk__ } from './thunk' const testKey = '__test__' -export const testHasLocalStorage = thunk(() => { +export const testHasLocalStorage = __thunk__(() => { try { localStorage.setItem(testKey, testKey) if (!localStorage.length || localStorage.getItem(testKey) !== testKey) { @@ -18,7 +18,7 @@ export const testHasLocalStorage = thunk(() => { return true }) -export const testHasIndexedDB = thunk(async () => { +export const testHasIndexedDB = __thunk__(async () => { if (typeof indexedDB === 'undefined') { return false } diff --git a/src/routes/_utils/thunk.js b/src/routes/_utils/thunk.js index 7418206c..29656532 100644 --- a/src/routes/_utils/thunk.js +++ b/src/routes/_utils/thunk.js @@ -1,4 +1,6 @@ -export function thunk (func) { +// We name this __thunk__ so that we can tell terser that it's a pure function, without possibly +// affecting third-party libraries that may also be using a function called "thunk". +export function __thunk__ (func) { let cached let runOnce return () => { diff --git a/src/routes/_utils/urlRegex.js b/src/routes/_utils/urlRegex.js index 1271cb5e..d252310a 100644 --- a/src/routes/_utils/urlRegex.js +++ b/src/routes/_utils/urlRegex.js @@ -1,6 +1,6 @@ -import { thunk } from './thunk' +import { __thunk__ } from './thunk' -export const urlRegex = thunk(() => { +export const urlRegex = __thunk__(() => { // this is provided at build time to avoid having a lot of runtime code just to build // a static regex return process.env.URL_REGEX diff --git a/src/routes/_utils/userAgent.js b/src/routes/_utils/userAgent.js index ca00151b..3cef63bd 100644 --- a/src/routes/_utils/userAgent.js +++ b/src/routes/_utils/userAgent.js @@ -1,19 +1,24 @@ -import { thunk } from './thunk' +import { __thunk__ } from './thunk' -export const isKaiOS = thunk(() => process.browser && /KAIOS/.test(navigator.userAgent)) +export const isKaiOS = __thunk__(() => process.browser && /KAIOS/.test(navigator.userAgent)) -export const isIOS = thunk(() => process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent)) +export const isIOS = __thunk__(() => process.browser && /iP(?:hone|ad|od)/.test(navigator.userAgent)) -export const isMac = thunk(() => process.browser && /mac/i.test(navigator.platform)) +export const isMac = __thunk__(() => process.browser && /mac/i.test(navigator.platform)) // IntersectionObserver introduced in iOS 12.2 https://caniuse.com/#feat=intersectionobserver -export const isIOSPre12Point2 = thunk(() => process.browser && isIOS() && +export const isIOSPre12Point2 = __thunk__(() => process.browser && isIOS() && !(typeof IntersectionObserver === 'function' && IntersectionObserver.toString().includes('[native code]'))) // PointerEvent introduced in iOS 13 https://caniuse.com/#feat=pointer -export const isIOSPre13 = thunk(() => process.browser && isIOS() && +export const isIOSPre13 = __thunk__(() => process.browser && isIOS() && !(typeof PointerEvent === 'function' && PointerEvent.toString().includes('[native code]'))) -export const isMobile = thunk(() => process.browser && navigator.userAgent.match(/(?:iPhone|iPod|iPad|Android|KAIOS)/)) +export const isMobile = __thunk__(() => process.browser && navigator.userAgent.match(/(?:iPhone|iPod|iPad|Android|KAIOS)/)) + +export const isSafari = __thunk__(() => process.browser && /Safari/.test(navigator.userAgent) && + !/Chrome/.test(navigator.userAgent)) + +export const isChrome = __thunk__(() => process.browser && /Chrome/.test(navigator.userAgent)) diff --git a/src/routes/_workers/blurhash.js b/src/routes/_workers/blurhash.js index 08b70a76..bd37f29c 100644 --- a/src/routes/_workers/blurhash.js +++ b/src/routes/_workers/blurhash.js @@ -1,12 +1,11 @@ import { decode as decodeBlurHash } from 'blurhash' import registerPromiseWorker from 'promise-worker/register' import { BLURHASH_RESOLUTION as RESOLUTION } from '../_static/blurhash' - -const isChrome = /Chrome/.test(navigator.userAgent) +import { isChrome } from '../_utils/userAgent' // Disabled in Chrome because convertToBlob() is slow // https://github.com/nolanlawson/pinafore/issues/1396 -const OFFSCREEN_CANVAS = !isChrome && typeof OffscreenCanvas === 'function' +const OFFSCREEN_CANVAS = !isChrome() && typeof OffscreenCanvas === 'function' ? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS ? OFFSCREEN_CANVAS.getContext('2d') : null diff --git a/src/service-worker.js b/src/service-worker.js index 37debc66..776bf472 100644 --- a/src/service-worker.js +++ b/src/service-worker.js @@ -3,11 +3,8 @@ import { shell as __shell__, routes as __routes__ } from '../__sapper__/service-worker.js' - -import { - get, - post -} from './routes/_utils/ajax' +import { get, post } from './routes/_utils/ajax' +import { isSafari } from './routes/_utils/userAgent' const timestamp = process.env.SAPPER_TIMESTAMP const ASSETS = `assets_${timestamp}` @@ -24,8 +21,6 @@ const ON_DEMAND_CACHE = [ } ] -const isSafari = /Safari/.test(navigator.userAgent) && !/Chrom/.test(navigator.userAgent) - // `static` is an array of everything in the `static` directory const assets = __assets__ .map(file => file.startsWith('/') ? file : `/${file}`) @@ -163,7 +158,7 @@ self.addEventListener('fetch', event => { // range request need to be be patched with a 206 response to satisfy // Safari (https://stackoverflow.com/questions/52087208) // Once this bug is fixed in WebKit we can remove this https://bugs.webkit.org/show_bug.cgi?id=186050 - if (isSafari && event.request.headers.get('range')) { + if (isSafari() && event.request.headers.get('range')) { return returnRangeRequest(req) } diff --git a/webpack/terser.config.js b/webpack/terser.config.js index 6409e474..97130459 100644 --- a/webpack/terser.config.js +++ b/webpack/terser.config.js @@ -1,19 +1,10 @@ const TerserWebpackPlugin = require('terser-webpack-plugin') +const terserOptions = require('../bin/terserOptions') module.exports = () => new TerserWebpackPlugin({ exclude: /(tesseract-asset|page-lifecycle)/, // tesseract causes problems, page-lifecycle is pre-minified cache: !process.env.TERSER_DISABLE_CACHE, parallel: true, sourceMap: true, - terserOptions: { - ecma: 8, - mangle: true, - compress: { - pure_funcs: ['console.log'] - }, - output: { - comments: false - }, - safari10: true - } + terserOptions })