Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 56 additions & 65 deletions src/features/accesskit/disable_gifs.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ import { buildStyle, postSelector } from '../../utils/interface.js';
import { getPreferences } from '../../utils/preferences.js';
import { memoize } from '../../utils/memoize.js';

const canvasClass = 'xkit-paused-gif-placeholder';
const pausedPosterAttribute = 'data-paused-gif-use-poster';
const pausedContentVar = '--xkit-paused-gif-content';
const pausedBackgroundImageVar = '--xkit-paused-gif-background-image';
const hoverContainerAttribute = 'data-paused-gif-hover-container';
const labelAttribute = 'data-paused-gif-label';
const hoverFixAttribute = 'data-paused-gif-hover-fix';
const containerClass = 'xkit-paused-gif-container';

let loadingMode;

const hovered = `:is(:hover, [${hoverContainerAttribute}]:hover *)`;
const parentHovered = `:is(:hover > *, [${hoverContainerAttribute}]:hover *)`;

export const styleElement = buildStyle(`
[${labelAttribute}]::after {
Expand Down Expand Up @@ -45,16 +45,17 @@ export const styleElement = buildStyle(`
transform: translateY(-50%);
}

.${canvasClass} {
position: absolute;
visibility: visible;
[${labelAttribute}]${hovered}::after,
[${pausedPosterAttribute}]:not(${hovered}) > div > ${keyToCss('knightRiderLoader')} {
display: none;
}

background-color: rgb(var(--white));
${keyToCss('blogCard')} ${keyToCss('headerImage')}${keyToCss('small')}[${labelAttribute}]::after {
font-size: 0.8rem;
top: calc(140px - 1em - 2.2ch);
}

.${canvasClass}${parentHovered},
[${labelAttribute}]${hovered}::after,
[${pausedPosterAttribute}]:not(${hovered}) > div > ${keyToCss('knightRiderLoader')} {
img:is([${pausedPosterAttribute}], [style*="${pausedContentVar}"]):not(${hovered}) ~ div > ${keyToCss('knightRiderLoader')} {
display: none;
}
${keyToCss('background')}[${labelAttribute}]::after {
Expand All @@ -72,9 +73,17 @@ ${keyToCss('background')}[${labelAttribute}]::after {
display: none;
}

img[style*="${pausedContentVar}"]:not(${hovered}) {
content: var(${pausedContentVar});
}
[style*="${pausedBackgroundImageVar}"]:not(${hovered}) {
background-image: var(${pausedBackgroundImageVar}) !important;
}

[${hoverFixAttribute}] {
position: relative;
pointer-events: auto !important;
}
`);

const addLabel = (element, inside = false) => {
Expand All @@ -88,27 +97,6 @@ const addLabel = (element, inside = false) => {
}
};

/**
* Fetches the selected image and tests if it is animated. On older browsers without ImageDecoder
* support, GIF images are assumed to be animated and WebP images are assumed to not be animated.
*/
const isAnimated = memoize(async sourceUrl => {
const response = await fetch(sourceUrl, { headers: { Accept: 'image/webp,*/*' } });
const contentType = response.headers.get('Content-Type');

if (typeof ImageDecoder === 'function' && await ImageDecoder.isTypeSupported(contentType)) {
const decoder = new ImageDecoder({
type: contentType,
data: response.body,
preferAnimation: true
});
await decoder.decode();
return decoder.tracks.selectedTrack.animated;
} else {
return !sourceUrl.endsWith('.webp');
}
});

/**
* Fetches the selected image, tests if it is animated, and returns a blob URL with the paused image
* if it is. This may be a small memory or storage leak, as the resulting blob URL will be valid until
Expand Down Expand Up @@ -148,49 +136,33 @@ const createPausedUrlIfAnimated = memoize(async sourceUrl => {
return URL.createObjectURL(blob);
});

const pauseGif = async function (gifElement) {
if (gifElement.currentSrc.endsWith('.webp') && !(await isAnimated(gifElement.currentSrc))) return;

const image = new Image();
image.src = gifElement.currentSrc;
image.onload = () => {
if (gifElement.parentNode && gifElement.parentNode.querySelector(`.${canvasClass}`) === null) {
const canvas = document.createElement('canvas');
canvas.width = image.naturalWidth;
canvas.height = image.naturalHeight;
canvas.className = gifElement.className;
canvas.classList.add(canvasClass);
canvas.setAttribute('style', gifElement.getAttribute('style'));
canvas.getContext('2d').drawImage(image, 0, 0);
gifElement.after(canvas);
addLabel(gifElement);
}
};
};

const processGifs = function (gifElements) {
gifElements.forEach(gifElement => {
gifElements.forEach(async gifElement => {
if (gifElement.closest(`${keyToCss('avatarImage', 'subAvatarImage')}, .block-editor-writing-flow`)) return;
const pausedGifElements = [...gifElement.parentNode.querySelectorAll(`.${canvasClass}`)];
if (pausedGifElements.length) {
gifElement.after(...pausedGifElements);
return;
}

gifElement.decoding = 'sync';

const posterElement = gifElement.parentElement.querySelector(keyToCss('poster'));
if (posterElement) {
gifElement.parentElement.setAttribute(pausedPosterAttribute, loadingMode);
addLabel(posterElement);
return;
}

if (gifElement.complete && gifElement.currentSrc) {
pauseGif(gifElement);
} else {
gifElement.onload = () => pauseGif(gifElement);
const sourceUrl = gifElement.currentSrc ||
await new Promise(resolve => gifElement.addEventListener('load', () => resolve(gifElement.currentSrc), { once: true }));

gifElement.style.setProperty(pausedContentVar, 'linear-gradient(transparent, transparent)');
const pausedUrl = await createPausedUrlIfAnimated(sourceUrl).catch(() => undefined);
if (!pausedUrl) {
gifElement.style.removeProperty(pausedContentVar);
return;
}

gifElement.style.setProperty(pausedContentVar, `url(${pausedUrl})`);
}
addLabel(gifElement);

gifElement.closest(keyToCss(
'albumImage', // post audio element
'imgLink' // trending tag: https://www.tumblr.com/explore/trending
))?.setAttribute(hoverFixAttribute, '');
});
};

Expand Down Expand Up @@ -218,6 +190,11 @@ const processBackgroundGifs = function (gifBackgroundElements) {
sourceValue.replace(sourceUrlRegex, `url("${pausedUrl}")`)
);
addLabel(gifBackgroundElement, true);

gifBackgroundElement.closest(keyToCss(
'media', // old activity item: "liked your post", "reblogged your post", "mentioned you in a post"
'activityMedia' // new activity item: "replied to your post", "replied to you in a post"
))?.setAttribute(hoverFixAttribute, '');
});
};

Expand Down Expand Up @@ -257,11 +234,20 @@ export const main = async function () {
${
'figure' // post image/imageset; recommended blog carousel entry; blog view sidebar "more like this"; post in grid view; blog card modal post entry
},
${
'main.labs' // labs settings header: https://www.tumblr.com/settings/labs
},
${keyToCss(
'linkCard', // post link element
'albumImage', // post audio element
'messageImage', // direct message attached image
'messagePost', // direct message linked post
'typeaheadRow', // modal search dropdown entry
'tagImage', // search page sidebar related tags, recommended tag carousel entry: https://www.tumblr.com/search/gif, https://www.tumblr.com/explore/recommended-for-you
'headerBanner', // blog view header
'headerImage', // modal blog card header, activity page "biggest fans" header
'topPost', // activity page top post
'colorfulListItemWrapper', // trending tag: https://www.tumblr.com/explore/trending
'takeoverBanner' // advertisement
)}
) img:is([srcset*=".gif"], [src*=".gif"], [srcset*=".webp"], [src*=".webp"]):not(${keyToCss('poster')})
Expand All @@ -270,6 +256,8 @@ export const main = async function () {

const gifBackgroundImage = `
${keyToCss(
'media', // old activity item: "liked your post", "reblogged your post", "mentioned you in a post"
'activityMedia', // new activity item: "replied to your post", "replied to you in a post"
'communityHeaderImage', // search page tags section header: https://www.tumblr.com/search/gif?v=tag
'bannerImage', // tagged page sidebar header: https://www.tumblr.com/tagged/gif
'tagChicletWrapper', // "trending" / "your tags" timeline carousel entry: https://www.tumblr.com/dashboard/trending, https://www.tumblr.com/dashboard/hubs
Expand All @@ -281,6 +269,7 @@ export const main = async function () {
const hoverableElement = [
`${keyToCss('listTimelineObject')} ${keyToCss('carouselWrapper')} ${keyToCss('postCard')}`, // recommended blog carousel entry
`div:has(> a${keyToCss('cover')}):has(${keyToCss('communityCategoryImage')})`, // tumblr communities browse page entry: https://www.tumblr.com/communities/browse
`${keyToCss('gridTimelineObject')}` // likes page or patio grid view post: https://www.tumblr.com/likes
].join(', ');
pageModifications.register(hoverableElement, processHoverableElements);

Expand All @@ -304,10 +293,12 @@ export const clean = async function () {
wrapper.replaceWith(...wrapper.children)
);

$(`.${canvasClass}`).remove();
$(`[${labelAttribute}]`).removeAttr(labelAttribute);
$(`[${pausedPosterAttribute}]`).removeAttr(pausedPosterAttribute);
$(`[${hoverContainerAttribute}]`).removeAttr(hoverContainerAttribute);
$(`[${hoverFixAttribute}]`).removeAttr(hoverFixAttribute);
[...document.querySelectorAll(`img[style*="${pausedContentVar}"]`)]
.forEach(element => element.style.removeProperty(pausedContentVar));
[...document.querySelectorAll(`[style*="${pausedBackgroundImageVar}"]`)]
.forEach(element => element.style.removeProperty(pausedBackgroundImageVar));
};