feat(media): Blurhash (#1381)
* chore(npm): Install blurhash * feat(media): Show blurhash * fix(media/blurhash): Better sensitive video handling * feat(media): Preference for using blurhash * chore(utils/blurhash): Add performance marks * fix(utils/blurhash): Performance marks * fix(utils/blurhash): Use correct dimension * refactor(utils/blurhash): Use constant for number of pixels * refactor(media): Simplify logic for displaying blurhash * chore(tests/spec): Attempt to adjust sensitivity tests for blurhash * chore(tests/spec): Update sensitivity tests for blurhash * chore(tests/spec): Check for sensitive * fix(media/blurhash): Handle videos * fix: Video handling * fix: Videos * minor refactoring, fix Svelte warning * fix: Large inline images and videos * feat(settings): Rename blurhash setting * refactor: Use toBlob, block media rendering until blurhash ready * refactor: Move computations to Web Worker * fix(workers/blurhash): More error handling * feat(workers/blurhash): Use quick-lru for caching * fix: Don't create Context2D needlessly * fix(workers/blurhash): Increase cache size to 100 * fix(workers/blurhash): Don't resolve promise twice * fix(utils/decode-image): Ignore data URLs Throws exception which prevents the image from loading.
This commit is contained in:
parent
d52049cca5
commit
77bb784efd
|
@ -48,6 +48,7 @@
|
||||||
"@webcomponents/custom-elements": "^1.2.4",
|
"@webcomponents/custom-elements": "^1.2.4",
|
||||||
"babel-loader": "^8.0.6",
|
"babel-loader": "^8.0.6",
|
||||||
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
"babel-plugin-transform-react-remove-prop-types": "^0.4.24",
|
||||||
|
"blurhash": "^1.1.3",
|
||||||
"cheerio": "^1.0.0-rc.2",
|
"cheerio": "^1.0.0-rc.2",
|
||||||
"child-process-promise": "^2.2.1",
|
"child-process-promise": "^2.2.1",
|
||||||
"chokidar": "^3.0.1",
|
"chokidar": "^3.0.1",
|
||||||
|
@ -109,7 +110,8 @@
|
||||||
"mocha": "^6.1.4",
|
"mocha": "^6.1.4",
|
||||||
"now": "^15.7.0",
|
"now": "^15.7.0",
|
||||||
"standard": "^13.1.0",
|
"standard": "^13.1.0",
|
||||||
"testcafe": "^1.2.1"
|
"testcafe": "^1.2.1",
|
||||||
|
"worker-loader": "^2.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 8"
|
"node": ">= 8"
|
||||||
|
@ -150,7 +152,10 @@
|
||||||
"customElements",
|
"customElements",
|
||||||
"AbortController",
|
"AbortController",
|
||||||
"matchMedia",
|
"matchMedia",
|
||||||
"MessageChannel"
|
"MessageChannel",
|
||||||
|
"ImageData",
|
||||||
|
"OffscreenCanvas",
|
||||||
|
"postMessage"
|
||||||
],
|
],
|
||||||
"ignore": [
|
"ignore": [
|
||||||
"dist",
|
"dist",
|
||||||
|
|
|
@ -53,7 +53,8 @@
|
||||||
height: void 0,
|
height: void 0,
|
||||||
ariaHidden: false,
|
ariaHidden: false,
|
||||||
alt: '',
|
alt: '',
|
||||||
title: ''
|
title: '',
|
||||||
|
blurhash: void 0
|
||||||
}),
|
}),
|
||||||
computed: {
|
computed: {
|
||||||
computedStyle: ({ background }) => {
|
computedStyle: ({ background }) => {
|
||||||
|
@ -71,7 +72,7 @@
|
||||||
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
|
return `object-position: ${coordsToPercent(focus.x)}% ${100 - coordsToPercent(focus.y)}%;`
|
||||||
},
|
},
|
||||||
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
|
fillFixSize: ({ forceSize, $largeInlineMedia }) => !$largeInlineMedia && !forceSize,
|
||||||
displaySrc: ({ error, src, fallback }) => ((error && fallback) || src)
|
displaySrc: ({ blurhash, error, src, fallback }) => (blurhash || (error && fallback) || src)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
alt={label || ''}
|
alt={label || ''}
|
||||||
title={label || ''}
|
title={label || ''}
|
||||||
src={staticSrc}
|
src={staticSrc}
|
||||||
|
blurhash={blurhash}
|
||||||
fallback={oneTransparentPixel}
|
fallback={oneTransparentPixel}
|
||||||
{width}
|
{width}
|
||||||
{height}
|
{height}
|
||||||
|
@ -24,7 +25,9 @@
|
||||||
{focus}
|
{focus}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if !blurhash}
|
||||||
<PlayVideoIcon className={playing ? 'hidden' : ''}/>
|
<PlayVideoIcon className={playing ? 'hidden' : ''}/>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
.non-autoplay-gifv {
|
.non-autoplay-gifv {
|
||||||
|
|
|
@ -1,4 +1,19 @@
|
||||||
{#if type === 'video' || type === 'audio'}
|
{#if type === 'video' || type === 'audio'}
|
||||||
|
{#if blurhash}
|
||||||
|
{#if type === 'video'}
|
||||||
|
<LazyImage
|
||||||
|
alt={description}
|
||||||
|
title={description}
|
||||||
|
src={previewUrl}
|
||||||
|
fallback={oneTransparentPixel}
|
||||||
|
blurhash={blurhash}
|
||||||
|
width={inlineWidth}
|
||||||
|
height={inlineHeight}
|
||||||
|
background="var(--loading-bg)"
|
||||||
|
{focus}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
<button id={elementId}
|
<button id={elementId}
|
||||||
type="button"
|
type="button"
|
||||||
class="play-video-button focus-after {$largeInlineMedia ? '' : 'fixed-size'} {type === 'audio' ? 'play-audio-button' : ''}"
|
class="play-video-button focus-after {$largeInlineMedia ? '' : 'fixed-size'} {type === 'audio' ? 'play-audio-button' : ''}"
|
||||||
|
@ -11,6 +26,7 @@
|
||||||
title={description}
|
title={description}
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
fallback={oneTransparentPixel}
|
fallback={oneTransparentPixel}
|
||||||
|
blurhash={blurhash}
|
||||||
width={inlineWidth}
|
width={inlineWidth}
|
||||||
height={inlineHeight}
|
height={inlineHeight}
|
||||||
background="var(--loading-bg)"
|
background="var(--loading-bg)"
|
||||||
|
@ -18,6 +34,7 @@
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<button id={elementId}
|
<button id={elementId}
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -40,6 +57,7 @@
|
||||||
class={noNativeWidthHeight ? 'no-native-width-height' : ''}
|
class={noNativeWidthHeight ? 'no-native-width-height' : ''}
|
||||||
label="Animated GIF: {description}"
|
label="Animated GIF: {description}"
|
||||||
poster={previewUrl}
|
poster={previewUrl}
|
||||||
|
blurhash={blurhash}
|
||||||
src={url}
|
src={url}
|
||||||
staticSrc={previewUrl}
|
staticSrc={previewUrl}
|
||||||
width={inlineWidth}
|
width={inlineWidth}
|
||||||
|
@ -53,6 +71,7 @@
|
||||||
title={description}
|
title={description}
|
||||||
src={previewUrl}
|
src={previewUrl}
|
||||||
fallback={oneTransparentPixel}
|
fallback={oneTransparentPixel}
|
||||||
|
blurhash={blurhash}
|
||||||
width={inlineWidth}
|
width={inlineWidth}
|
||||||
height={inlineHeight}
|
height={inlineHeight}
|
||||||
background="var(--loading-bg)"
|
background="var(--loading-bg)"
|
||||||
|
@ -91,11 +110,16 @@
|
||||||
import LazyImage from '../LazyImage.html'
|
import LazyImage from '../LazyImage.html'
|
||||||
import AutoplayVideo from '../AutoplayVideo.html'
|
import AutoplayVideo from '../AutoplayVideo.html'
|
||||||
import { registerClickDelegate } from '../../_utils/delegate'
|
import { registerClickDelegate } from '../../_utils/delegate'
|
||||||
|
import { decode } from '../../_utils/blurhash'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
oncreate () {
|
async oncreate () {
|
||||||
const { elementId } = this.get()
|
const { elementId, media } = this.get()
|
||||||
registerClickDelegate(this, elementId, () => this.onClick())
|
registerClickDelegate(this, elementId, () => this.onClick())
|
||||||
|
|
||||||
|
if (media.blurhash) {
|
||||||
|
this.set({ decodedBlurhash: await decode(media.blurhash) })
|
||||||
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
focus: ({ meta }) => meta && meta.focus,
|
focus: ({ meta }) => meta && meta.focus,
|
||||||
|
@ -126,6 +150,7 @@
|
||||||
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
|
elementId: ({ media, uuid }) => `media-${uuid}-${media.id}`,
|
||||||
description: ({ media }) => media.description || '',
|
description: ({ media }) => media.description || '',
|
||||||
previewUrl: ({ media }) => media.preview_url,
|
previewUrl: ({ media }) => media.preview_url,
|
||||||
|
blurhash: ({ showBlurhash, decodedBlurhash }) => showBlurhash && decodedBlurhash,
|
||||||
url: ({ media }) => media.url,
|
url: ({ media }) => media.url,
|
||||||
type: ({ media }) => media.type
|
type: ({ media }) => media.type
|
||||||
},
|
},
|
||||||
|
@ -141,6 +166,7 @@
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
|
oneTransparentPixel: ONE_TRANSPARENT_PIXEL,
|
||||||
|
decodedBlurhash: ONE_TRANSPARENT_PIXEL,
|
||||||
mouseover: void 0
|
mouseover: void 0
|
||||||
}),
|
}),
|
||||||
store: () => store,
|
store: () => store,
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<div class={computedClass}
|
<div class={computedClass}
|
||||||
style="grid-template-columns: repeat({nCols}, 1fr);" >
|
style="grid-template-columns: repeat({nCols}, 1fr);" >
|
||||||
{#each mediaAttachments as media, index}
|
{#each mediaAttachments as media, index}
|
||||||
<Media {media} {uuid} {mediaAttachments} {index} />
|
<Media {media} {uuid} {mediaAttachments} {index} {showBlurhash} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<style>
|
<style>
|
||||||
|
@ -39,6 +39,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-media.status-media-is-sensitive {
|
.status-media.status-media-is-sensitive {
|
||||||
|
height: inherit;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -55,6 +56,10 @@
|
||||||
twoCols && 'two-cols',
|
twoCols && 'two-cols',
|
||||||
!$largeInlineMedia && 'grouped-images'
|
!$largeInlineMedia && 'grouped-images'
|
||||||
),
|
),
|
||||||
|
showBlurhash:
|
||||||
|
({ sensitive, sensitiveShown, mediaAttachments }) => {
|
||||||
|
return sensitive && mediaAttachments.every(attachment => !!attachment.blurhash) ? !sensitiveShown : false
|
||||||
|
},
|
||||||
nCols:
|
nCols:
|
||||||
({ mediaAttachments, $largeInlineMedia }) => {
|
({ mediaAttachments, $largeInlineMedia }) => {
|
||||||
return (!$largeInlineMedia && mediaAttachments.length > 1) ? 2 : 1
|
return (!$largeInlineMedia && mediaAttachments.length > 1) ? 2 : 1
|
||||||
|
|
|
@ -10,28 +10,32 @@
|
||||||
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye-slash" />
|
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye-slash" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<MediaAttachments {mediaAttachments} {sensitive} {uuid} />
|
|
||||||
{:else}
|
{:else}
|
||||||
<button id={elementId}
|
<button id={elementId}
|
||||||
type="button"
|
type="button"
|
||||||
class="status-sensitive-media-button"
|
class="status-sensitive-media-button"
|
||||||
aria-label="Show sensitive media" >
|
aria-label="Show sensitive media" >
|
||||||
|
|
||||||
<div class="status-sensitive-media-warning">
|
<div class="{customWarningClass}">
|
||||||
|
<div class="status-sensitive-media-warning-text">
|
||||||
Sensitive content. Click to show.
|
Sensitive content. Click to show.
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="svg-wrapper">
|
<div class="svg-wrapper">
|
||||||
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye" />
|
<SvgIcon className="status-sensitive-media-svg" href="#fa-eye" />
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if sensitiveShown || canUseBlurhash}
|
||||||
|
<MediaAttachments {mediaAttachments} {sensitive} {sensitiveShown} {uuid} />
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if enableShortcuts}
|
{#if enableShortcuts}
|
||||||
<Shortcut scope={shortcutScope} key="y" on:pressed="toggleSensitiveMedia()"/>
|
<Shortcut scope={shortcutScope} key="y" on:pressed="toggleSensitiveMedia()"/>
|
||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<MediaAttachments {mediaAttachments} {sensitive} {uuid} />
|
<MediaAttachments {mediaAttachments} {sensitive} {sensitiveShown} {uuid} />
|
||||||
{/if}
|
{/if}
|
||||||
<style>
|
<style>
|
||||||
.status-sensitive-media-container {
|
.status-sensitive-media-container {
|
||||||
|
@ -81,6 +85,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-sensitive-media-hidden .status-sensitive-media-button {
|
.status-sensitive-media-hidden .status-sensitive-media-button {
|
||||||
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -96,7 +101,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-sensitive-media-container .status-sensitive-media-warning {
|
.status-sensitive-media-container .status-sensitive-media-warning {
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -109,6 +113,21 @@
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.status-sensitive-media-container .status-sensitive-media-warning-transparent {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-sensitive-media-container .status-sensitive-media-warning-opaque {
|
||||||
|
background: var(--mask-bg);
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-sensitive-media-container .status-sensitive-media-warning-transparent .status-sensitive-media-warning-text {
|
||||||
|
background: var(--mask-bg);
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.status-sensitive-media-container .svg-wrapper {
|
.status-sensitive-media-container .svg-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
@ -119,6 +138,7 @@
|
||||||
}
|
}
|
||||||
.status-sensitive-media-hidden .svg-wrapper {
|
.status-sensitive-media-hidden .svg-wrapper {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
background: none;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
@ -171,6 +191,7 @@
|
||||||
$largeInlineMedia ? 'not-grouped-images' : 'grouped-images'
|
$largeInlineMedia ? 'not-grouped-images' : 'grouped-images'
|
||||||
),
|
),
|
||||||
mediaAttachments: ({ originalStatus }) => originalStatus.media_attachments,
|
mediaAttachments: ({ originalStatus }) => originalStatus.media_attachments,
|
||||||
|
canUseBlurhash: ({ $ignoreBlurhash, mediaAttachments }) => !$ignoreBlurhash && mediaAttachments && mediaAttachments.every(media => !!media.blurhash),
|
||||||
sensitiveShown: ({ $sensitivesShown, uuid }) => !!$sensitivesShown[uuid],
|
sensitiveShown: ({ $sensitivesShown, uuid }) => !!$sensitivesShown[uuid],
|
||||||
sensitive: ({ originalStatus, $markMediaAsSensitive, $neverMarkMediaAsSensitive }) => (
|
sensitive: ({ originalStatus, $markMediaAsSensitive, $neverMarkMediaAsSensitive }) => (
|
||||||
!$neverMarkMediaAsSensitive && ($markMediaAsSensitive || originalStatus.sensitive)
|
!$neverMarkMediaAsSensitive && ($markMediaAsSensitive || originalStatus.sensitive)
|
||||||
|
@ -181,7 +202,11 @@
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
return `padding-bottom: ${Math.ceil(mediaAttachments.length / 2) * 29}%;`
|
return `padding-bottom: ${Math.ceil(mediaAttachments.length / 2) * 29}%;`
|
||||||
}
|
},
|
||||||
|
customWarningClass: ({ canUseBlurhash }) => classname(
|
||||||
|
'status-sensitive-media-warning',
|
||||||
|
canUseBlurhash ? 'status-sensitive-media-warning-transparent' : 'status-sensitive-media-warning-opaque'
|
||||||
|
)
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
toggleSensitiveMedia () {
|
toggleSensitiveMedia () {
|
||||||
|
|
|
@ -8,6 +8,11 @@
|
||||||
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
|
bind:checked="$neverMarkMediaAsSensitive" on:change="onChange(event)">
|
||||||
<label for="choice-never-mark-media-sensitive">Show sensitive media by default</label>
|
<label for="choice-never-mark-media-sensitive">Show sensitive media by default</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="setting-group">
|
||||||
|
<input type="checkbox" id="choice-use-blurhash"
|
||||||
|
bind:checked="$ignoreBlurhash" on:change="onChange(event)">
|
||||||
|
<label for="choice-use-blurhash">Show a plain gray color for sensitive media</label>
|
||||||
|
</div>
|
||||||
<div class="setting-group">
|
<div class="setting-group">
|
||||||
<input type="checkbox" id="choice-mark-media-sensitive"
|
<input type="checkbox" id="choice-mark-media-sensitive"
|
||||||
bind:checked="$markMediaAsSensitive" on:change="onChange(event)">
|
bind:checked="$markMediaAsSensitive" on:change="onChange(event)">
|
||||||
|
|
|
@ -30,6 +30,7 @@ const persistedState = {
|
||||||
loggedInInstancesInOrder: [],
|
loggedInInstancesInOrder: [],
|
||||||
markMediaAsSensitive: false,
|
markMediaAsSensitive: false,
|
||||||
neverMarkMediaAsSensitive: false,
|
neverMarkMediaAsSensitive: false,
|
||||||
|
ignoreBlurhash: false,
|
||||||
omitEmojiInDisplayNames: undefined,
|
omitEmojiInDisplayNames: undefined,
|
||||||
pinnedPages: {},
|
pinnedPages: {},
|
||||||
pushSubscriptions: {},
|
pushSubscriptions: {},
|
||||||
|
|
51
src/routes/_utils/blurhash.js
Normal file
51
src/routes/_utils/blurhash.js
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import BlurhashWorker from 'worker-loader!../_workers/blurhash' // eslint-disable-line
|
||||||
|
|
||||||
|
const RESOLUTION = 32
|
||||||
|
let worker
|
||||||
|
let canvas
|
||||||
|
let canvasContext2D
|
||||||
|
|
||||||
|
export function init () {
|
||||||
|
worker = worker || new BlurhashWorker()
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decode (blurhash) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
init()
|
||||||
|
|
||||||
|
const onMessage = ({ data: { encoded, decoded, imageData, error } }) => {
|
||||||
|
if (encoded !== blurhash) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.removeEventListener('message', onMessage)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return reject(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (decoded) {
|
||||||
|
resolve(decoded)
|
||||||
|
} else {
|
||||||
|
if (!canvas) {
|
||||||
|
canvas = document.createElement('canvas')
|
||||||
|
canvas.height = RESOLUTION
|
||||||
|
canvas.width = RESOLUTION
|
||||||
|
canvasContext2D = canvas.getContext('2d')
|
||||||
|
}
|
||||||
|
|
||||||
|
canvasContext2D.putImageData(imageData, 0, 0)
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
resolve(URL.createObjectURL(blob))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
worker.addEventListener('message', onMessage)
|
||||||
|
worker.postMessage({ encoded: blurhash })
|
||||||
|
} catch (e) {
|
||||||
|
reject(e)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ export function decodeImage (img) {
|
||||||
// Remove this UA sniff when the Firefox bug is fixed
|
// Remove this UA sniff when the Firefox bug is fixed
|
||||||
// https://github.com/nolanlawson/pinafore/issues/1344#issuecomment-514312672
|
// https://github.com/nolanlawson/pinafore/issues/1344#issuecomment-514312672
|
||||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1565542
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1565542
|
||||||
if (!IS_FIREFOX && typeof img.decode === 'function') {
|
if (!IS_FIREFOX && typeof img.decode === 'function' && !img.src.startsWith('data:image/png;base64,')) {
|
||||||
return img.decode()
|
return img.decode()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
45
src/routes/_workers/blurhash.js
Normal file
45
src/routes/_workers/blurhash.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import { decode as decodeBlurHash } from 'blurhash'
|
||||||
|
import QuickLRU from 'quick-lru'
|
||||||
|
|
||||||
|
const RESOLUTION = 32
|
||||||
|
const OFFSCREEN_CANVAS = typeof OffscreenCanvas === 'function'
|
||||||
|
? new OffscreenCanvas(RESOLUTION, RESOLUTION) : null
|
||||||
|
const OFFSCREEN_CANVAS_CONTEXT_2D = OFFSCREEN_CANVAS
|
||||||
|
? OFFSCREEN_CANVAS.getContext('2d') : null
|
||||||
|
const CACHE = new QuickLRU({ maxSize: 100 })
|
||||||
|
|
||||||
|
self.addEventListener('message', ({ data: { encoded } }) => {
|
||||||
|
try {
|
||||||
|
if (CACHE.has(encoded)) {
|
||||||
|
if (OFFSCREEN_CANVAS) {
|
||||||
|
postMessage({ encoded, decoded: CACHE.get(encoded), imageData: null, error: null })
|
||||||
|
} else {
|
||||||
|
postMessage({ encoded, imageData: CACHE.get(encoded), decoded: null, error: null })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const pixels = decodeBlurHash(encoded, RESOLUTION, RESOLUTION)
|
||||||
|
|
||||||
|
if (pixels) {
|
||||||
|
const imageData = new ImageData(pixels, RESOLUTION, RESOLUTION)
|
||||||
|
|
||||||
|
if (OFFSCREEN_CANVAS) {
|
||||||
|
OFFSCREEN_CANVAS_CONTEXT_2D.putImageData(imageData, 0, 0)
|
||||||
|
OFFSCREEN_CANVAS.convertToBlob().then(blob => {
|
||||||
|
const decoded = URL.createObjectURL(blob)
|
||||||
|
CACHE.set(encoded, decoded)
|
||||||
|
postMessage({ encoded, decoded, imageData: null, error: null })
|
||||||
|
}).catch(error => {
|
||||||
|
postMessage({ encoded, decoded: null, imageData: null, error })
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
CACHE.set(encoded, imageData)
|
||||||
|
postMessage({ encoded, imageData, decoded: null, error: null })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
postMessage({ encoded, decoded: null, imageData: null, error: new Error('decode did not return any pixels') })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
postMessage({ encoded, decoded: null, imageData: null, error })
|
||||||
|
}
|
||||||
|
})
|
|
@ -13,10 +13,10 @@ test('shows sensitive images and videos', async t => {
|
||||||
const videoIdx = homeTimeline.findIndex(_ => _.content === 'secret video')
|
const videoIdx = homeTimeline.findIndex(_ => _.content === 'secret video')
|
||||||
|
|
||||||
await scrollToStatus(t, 1 + kittenIdx)
|
await scrollToStatus(t, 1 + kittenIdx)
|
||||||
await t.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).exists).notOk()
|
await t.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).getAttribute('src')).match(/^blob:http:\/\/localhost/)
|
||||||
.click($(`${getNthStatusSelector(1 + kittenIdx)} .status-sensitive-media-button`))
|
.click($(`${getNthStatusSelector(1 + kittenIdx)} .status-sensitive-media-button`))
|
||||||
.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).getAttribute('alt')).eql('kitten')
|
.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).getAttribute('alt')).eql('kitten')
|
||||||
.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).hasAttribute('src')).ok()
|
.expect($(`${getNthStatusSelector(1 + kittenIdx)} .status-media img`).getAttribute('src')).match(/^http:\/\//)
|
||||||
.hover(getNthStatus(1 + videoIdx))
|
.hover(getNthStatus(1 + videoIdx))
|
||||||
.expect($(`${getNthStatusSelector(1 + videoIdx)} .status-media .play-video-button`).exists).notOk()
|
.expect($(`${getNthStatusSelector(1 + videoIdx)} .status-media .play-video-button`).exists).notOk()
|
||||||
.click($(`${getNthStatusSelector(1 + videoIdx)} .status-sensitive-media-button`))
|
.click($(`${getNthStatusSelector(1 + videoIdx)} .status-sensitive-media-button`))
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { loginAsFoobar } from '../roles'
|
import { loginAsFoobar } from '../roles'
|
||||||
import {
|
import {
|
||||||
generalSettingsButton,
|
generalSettingsButton,
|
||||||
getNthStatus, getNthStatusMedia, getNthStatusSensitiveMediaButton, homeNavButton, markMediaSensitiveInput,
|
getNthStatus, getNthStatusMediaImg, getNthStatusSensitiveMediaButton, homeNavButton, markMediaSensitiveInput,
|
||||||
scrollToStatus, settingsNavButton, neverMarkMediaSensitiveInput
|
scrollToStatus, settingsNavButton, neverMarkMediaSensitiveInput
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
|
||||||
|
@ -14,11 +14,11 @@ async function checkSensitivityForStatus (t, idx, sensitive) {
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
await t
|
await t
|
||||||
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).ok()
|
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).ok()
|
||||||
.expect(getNthStatusMedia(1 + idx).exists).notOk()
|
.expect(getNthStatusMediaImg(1 + idx).getAttribute('src')).match(/^blob:http:\/\/localhost/)
|
||||||
} else {
|
} else {
|
||||||
await t
|
await t
|
||||||
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).notOk()
|
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).notOk()
|
||||||
.expect(getNthStatusMedia(1 + idx).exists).ok()
|
.expect(getNthStatusMediaImg(1 + idx).getAttribute('src')).match(/^http:\/\//)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import {
|
||||||
getNthFavorited,
|
getNthFavorited,
|
||||||
getNthStatus,
|
getNthStatus,
|
||||||
getNthStatusContent,
|
getNthStatusContent,
|
||||||
getNthStatusMedia,
|
getNthStatusMediaImg,
|
||||||
getNthStatusSensitiveMediaButton,
|
getNthStatusSensitiveMediaButton,
|
||||||
getNthStatusSpoiler,
|
getNthStatusSpoiler,
|
||||||
getUrl, modalDialog,
|
getUrl, modalDialog,
|
||||||
|
@ -104,11 +104,11 @@ test('Shortcut y shows/hides sensitive image', async t => {
|
||||||
await t
|
await t
|
||||||
.expect(isNthStatusActive(1 + idx)()).ok()
|
.expect(isNthStatusActive(1 + idx)()).ok()
|
||||||
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).ok()
|
.expect(getNthStatusSensitiveMediaButton(1 + idx).exists).ok()
|
||||||
.expect(getNthStatusMedia(1 + idx).exists).notOk()
|
.expect(getNthStatusMediaImg(1 + idx).getAttribute('src')).match(/^blob:http:\/\/localhost/)
|
||||||
.pressKey('y')
|
.pressKey('y')
|
||||||
.expect(getNthStatusMedia(1 + idx).exists).ok()
|
.expect(getNthStatusMediaImg(1 + idx).getAttribute('src')).match(/^http:\/\//)
|
||||||
.pressKey('y')
|
.pressKey('y')
|
||||||
.expect(getNthStatusMedia(1 + idx).exists).notOk()
|
.expect(getNthStatusMediaImg(1 + idx).getAttribute('src')).match(/^blob:http:\/\/localhost/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Shortcut f toggles favorite status', async t => {
|
test('Shortcut f toggles favorite status', async t => {
|
||||||
|
|
23
yarn.lock
23
yarn.lock
|
@ -1489,6 +1489,11 @@ bluebird@^3.5.5:
|
||||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.5.tgz#a8d0afd73251effbbd5fe384a77d73003c17a71f"
|
||||||
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
|
integrity sha512-5am6HnnfN+urzt4yfg7IgTbotDjIT/u8AJpEt0sIU9FtXfVeezXAPKswrG+xKUCOYAINpSdgZVDU6QFh+cuH3w==
|
||||||
|
|
||||||
|
blurhash@^1.1.3:
|
||||||
|
version "1.1.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/blurhash/-/blurhash-1.1.3.tgz#dc325af7da836d07a0861d830bdd63694382483e"
|
||||||
|
integrity sha512-yUhPJvXexbqbyijCIE/T2NCXcj9iNPhWmOKbPTuR/cm7Q5snXYIfnVnz6m7MWOXxODMz/Cr3UcVkRdHiuDVRDw==
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
||||||
version "4.11.8"
|
version "4.11.8"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
||||||
|
@ -4602,7 +4607,7 @@ loader-runner@^2.4.0:
|
||||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
|
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357"
|
||||||
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
|
integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==
|
||||||
|
|
||||||
loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0, loader-utils@^1.2.3:
|
||||||
version "1.2.3"
|
version "1.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
|
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7"
|
||||||
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
|
integrity sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==
|
||||||
|
@ -6600,6 +6605,14 @@ sax@^1.2.4, sax@~1.2.4:
|
||||||
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
|
||||||
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
|
||||||
|
|
||||||
|
schema-utils@^0.4.0:
|
||||||
|
version "0.4.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.7.tgz#ba74f597d2be2ea880131746ee17d0a093c68187"
|
||||||
|
integrity sha512-v/iwU6wvwGK8HbU9yi3/nhGzP0yGSuhQMzL6ySiec1FSrZZDkhm4noOSWzrNFo/jEc+SJY6jRTwuwbSXJPDUnQ==
|
||||||
|
dependencies:
|
||||||
|
ajv "^6.1.0"
|
||||||
|
ajv-keywords "^3.1.0"
|
||||||
|
|
||||||
schema-utils@^1.0.0:
|
schema-utils@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-1.0.0.tgz#0b79a93204d7b600d4b2850d1f66c2a34951c770"
|
||||||
|
@ -8044,6 +8057,14 @@ worker-farm@^1.7.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
errno "~0.1.7"
|
errno "~0.1.7"
|
||||||
|
|
||||||
|
worker-loader@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-2.0.0.tgz#45fda3ef76aca815771a89107399ee4119b430ac"
|
||||||
|
integrity sha512-tnvNp4K3KQOpfRnD20m8xltE3eWh89Ye+5oj7wXEEHKac1P4oZ6p9oTj8/8ExqoSBnk9nu5Pr4nKfQ1hn2APJw==
|
||||||
|
dependencies:
|
||||||
|
loader-utils "^1.0.0"
|
||||||
|
schema-utils "^0.4.0"
|
||||||
|
|
||||||
wrap-ansi@^2.0.0, wrap-ansi@^2.1.0:
|
wrap-ansi@^2.0.0, wrap-ansi@^2.1.0:
|
||||||
version "2.1.0"
|
version "2.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
|
||||||
|
|
Loading…
Reference in a new issue