diff --git a/src/features/no_recommended/hide_recommended_community_posts.js b/src/features/no_recommended/hide_recommended_community_posts.js index b6f2ab6d5c..7ebe349cb8 100644 --- a/src/features/no_recommended/hide_recommended_community_posts.js +++ b/src/features/no_recommended/hide_recommended_community_posts.js @@ -1,23 +1,20 @@ -import { buildStyle, filterPostElements, getTimelineItemWrapper } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { filterPostElements } from '../../utils/interface.js'; import { onNewPosts } from '../../utils/mutations.js'; import { timelineObject } from '../../utils/react_props.js'; import { forYouTimelineFilter, searchPostsTimelineFilter } from '../../utils/timeline_id.js'; import { joinedCommunityUuids } from '../../utils/user.js'; -const hiddenAttribute = 'data-no-recommended-community-posts-hidden'; const timeline = [forYouTimelineFilter, searchPostsTimelineFilter]; const includeFiltered = true; -export const styleElement = buildStyle(`[${hiddenAttribute}] { - content: linear-gradient(transparent, transparent); - height: 0; -}`); +const { hidePost, showPosts } = createPostHideFunctions({ id: 'no-recommended-community-posts' }); const processPosts = postElements => filterPostElements(postElements, { timeline, includeFiltered }).forEach(async postElement => { const { community } = await timelineObject(postElement); if (community && !joinedCommunityUuids.includes(community.uuid)) { - getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); + hidePost(postElement); } }); @@ -27,6 +24,5 @@ export const main = async function () { export const clean = async function () { onNewPosts.removeListener(processPosts); - - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + showPosts(); }; diff --git a/src/features/postblock/index.css b/src/features/postblock/index.css deleted file mode 100644 index 0eb6c48963..0000000000 --- a/src/features/postblock/index.css +++ /dev/null @@ -1,56 +0,0 @@ -[data-postblock-hidden], .xkit-postblock-hidden-post-controls ~ div [data-xkit-postblock-hidden-controlled] { - content: linear-gradient(transparent, transparent); - height: 0; -} - -.xkit-postblock-hidden-post-controls { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: var(--space-m); - border-radius: 8px; - margin-bottom: var(--space-m); - - background-color: var(--content-panel); - background-image: linear-gradient(var(--content-tint), var(--content-tint)); - color: var(--content-fg-secondary); - - font-family: var(--font-family-modern); - font-size: 1rem; - font-weight: 350; - line-height: 1.5rem; -} - -.xkit-postblock-hidden-post-controls button { - flex-shrink: 0; - padding: 10px 16px; - border-radius: 9999px; - - font-family: var(--font-family-modern); - font-size: 1rem; - font-weight: 500; - line-height: 1.5rem; -} - -.xkit-postblock-hidden-post-controls button:hover { - background-color: var(--content-tint); - color: var(--content-fg); -} - -.xkit-postblock-hidden-post-controls button:active { - background-color: var(--content-tint-strong); -} - -.xkit-postblock-hidden-post-controls button:focus-visible { - outline: 2px solid var(--content-ui-focus); - outline-offset: 2px; -} - -/* Palettes for Tumblr font override compatibility */ -:root[style*="--font-family-modern"] .xkit-postblock-hidden-post-controls { - font-weight: normal; -} -:root[style*="--font-family-modern"] .xkit-postblock-hidden-post-controls button { - font-weight: bold; -} diff --git a/src/features/postblock/index.js b/src/features/postblock/index.js index 202ba3c7c9..5d1b40e0cd 100644 --- a/src/features/postblock/index.js +++ b/src/features/postblock/index.js @@ -1,10 +1,10 @@ import { dom } from '../../utils/dom.js'; -import { getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { filterPostElements } from '../../utils/interface.js'; import { registerMeatballItem, unregisterMeatballItem } from '../../utils/meatballs.js'; import { showModal, hideModal, modalCancelButton } from '../../utils/modals.js'; import { onNewPosts, pageModifications } from '../../utils/mutations.js'; import { timelineObject } from '../../utils/react_props.js'; -import { postPermalinkTimelineFilter, timelineSelector } from '../../utils/timeline_id.js'; import { navigate } from '../../utils/tumblr_helpers.js'; const meatballButtonBlockId = 'postblock-block'; @@ -12,27 +12,20 @@ const meatballButtonBlockLabel = 'Block this post'; const meatballButtonUnblockId = 'postblock-unblock'; const meatballButtonUnblockLabel = 'Unblock this post'; -const hiddenAttribute = 'data-postblock-hidden'; -const controlsClass = 'xkit-postblock-hidden-post-controls'; -const controlledHiddenAttribute = 'data-xkit-postblock-hidden-controlled'; const storageKey = 'postblock.blockedPostRootIDs'; const blogUuidsStorageKey = 'postblock.blockedPostBlogUUIDs'; -// Remove outdated elements when loading module -$(`.${controlsClass}`).remove(); - let blogUuids = {}; -const addPermalinkPageControls = timelineElement => { - const controlsElement = dom('div', { class: controlsClass }, null, [ - 'This post is hidden by PostBlock.', - dom('button', null, { click: () => controlsElement.remove() }, 'View post'), - ]); - timelineElement.prepend(controlsElement); -}; - let blockedPostRootIDs = []; +const { hidePost, showPost, showPosts } = createPostHideFunctions({ + id: 'postblock', + permalinkPageControls: { + message: 'This post is hidden by PostBlock.', + }, +}); + const saveUuidPair = (postId, blogUuid) => { if (blockedPostRootIDs.includes(postId) && !blogUuids[postId]) { blogUuids[postId] = blogUuid; @@ -55,15 +48,9 @@ const processPosts = postElements => const rootID = rebloggedRootId || postID; if (blockedPostRootIDs.includes(rootID)) { - const timelineElement = postElement.closest(timelineSelector); - if (postPermalinkTimelineFilter(postID)(timelineElement)) { - getTimelineItemWrapper(postElement).setAttribute(controlledHiddenAttribute, ''); - addPermalinkPageControls(timelineElement); - } else { - getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); - } + hidePost(postElement); } else { - getTimelineItemWrapper(postElement).removeAttribute(hiddenAttribute); + showPost(postElement); } saveUuidPair(id, uuid); @@ -144,8 +131,5 @@ export const clean = async function () { unregisterMeatballItem(meatballButtonUnblockId); onNewPosts.removeListener(processPosts); - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); - $(`.${controlsClass}`).remove(); + showPosts(); }; - -export const stylesheet = true; diff --git a/src/features/tweaks/hide_blocked_blogs.js b/src/features/tweaks/hide_blocked_blogs.js index 7b6bcb9868..35e3d01f9b 100644 --- a/src/features/tweaks/hide_blocked_blogs.js +++ b/src/features/tweaks/hide_blocked_blogs.js @@ -1,14 +1,18 @@ -import { buildStyle, getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { filterPostElements } from '../../utils/interface.js'; import { onNewPosts } from '../../utils/mutations.js'; import { isMyPost, timelineObject } from '../../utils/react_props.js'; import { blogTimelineFilter, timelineSelector } from '../../utils/timeline_id.js'; -const hiddenAttribute = 'data-xkit-tweaks-hide-blocked-blogs-hidden'; -export const styleElement = buildStyle(` -[${hiddenAttribute}] { - content: linear-gradient(transparent, transparent); - height: 0; -}`); +const { hidePost, showPosts } = createPostHideFunctions({ + id: 'tweaks-hide-blocked-blogs', + + // Only applied to posts hidden by a blocked blog in the trail. + // Posts *authored by* blocked blogs aren't hidden with this util (see isTimelineExempt below). + permalinkPageControls: { + message: 'This post contains a blocked blog.', + }, +}); const processPosts = (postElements) => { filterPostElements(postElements, { includeFiltered: true }).forEach(async postElement => { @@ -32,7 +36,7 @@ const processPosts = (postElements) => { continue; } - getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); + hidePost(postElement); break; } }); @@ -44,5 +48,5 @@ export const main = async function () { export const clean = async function () { onNewPosts.removeListener(processPosts); - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + showPosts(); }; diff --git a/src/features/tweaks/hide_filtered_posts.js b/src/features/tweaks/hide_filtered_posts.js index 2919488831..d39730feb5 100644 --- a/src/features/tweaks/hide_filtered_posts.js +++ b/src/features/tweaks/hide_filtered_posts.js @@ -1,17 +1,18 @@ import { keyToCss } from '../../utils/css_map.js'; -import { buildStyle, getTimelineItemWrapper } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { getTimelineItemWrapper } from '../../utils/interface.js'; import { pageModifications } from '../../utils/mutations.js'; -const hiddenAttribute = 'data-tweaks-hide-filtered-posts-hidden'; -export const styleElement = buildStyle(` -[${hiddenAttribute}] { - content: linear-gradient(transparent, transparent); - height: 0; -}`); +const { hidePost, showPosts } = createPostHideFunctions({ + id: 'tweaks-hide-filtered-posts', + permalinkPageControls: { + message: 'This post contains filtered tags or content.', + }, +}); const hideFilteredPosts = filteredScreens => filteredScreens .map(getTimelineItemWrapper) - .forEach(timelineItem => timelineItem.setAttribute(hiddenAttribute, '')); + .forEach(hidePost); export const main = async function () { const filteredScreenSelector = `article ${keyToCss('filteredScreen')}`; @@ -20,6 +21,5 @@ export const main = async function () { export const clean = async function () { pageModifications.unregister(hideFilteredPosts); - - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + showPosts(); }; diff --git a/src/features/tweaks/hide_liked_posts.js b/src/features/tweaks/hide_liked_posts.js index 6b600e2fc3..4635025449 100644 --- a/src/features/tweaks/hide_liked_posts.js +++ b/src/features/tweaks/hide_liked_posts.js @@ -1,23 +1,19 @@ -import { buildStyle, getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { filterPostElements } from '../../utils/interface.js'; import { onNewPosts } from '../../utils/mutations.js'; import { isMyPost, timelineObject } from '../../utils/react_props.js'; import { followingTimelineFilter } from '../../utils/timeline_id.js'; const timeline = followingTimelineFilter; -const hiddenAttribute = 'data-tweaks-hide-liked-posts-hidden'; -export const styleElement = buildStyle(` -[${hiddenAttribute}] { - content: linear-gradient(transparent, transparent); - height: 0; -}`); +const { hidePost, showPosts } = createPostHideFunctions({ id: 'tweaks-hide-liked-posts' }); const processPosts = async function (postElements) { filterPostElements(postElements, { timeline }).forEach(async postElement => { const { liked } = await timelineObject(postElement); const myPost = await isMyPost(postElement); - if (liked && !myPost) getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); + if (liked && !myPost) hidePost(postElement); }); }; @@ -27,6 +23,5 @@ export const main = async function () { export const clean = async function () { onNewPosts.removeListener(processPosts); - - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + showPosts(); }; diff --git a/src/features/tweaks/hide_my_posts.js b/src/features/tweaks/hide_my_posts.js index f930ee7791..7da34189ac 100644 --- a/src/features/tweaks/hide_my_posts.js +++ b/src/features/tweaks/hide_my_posts.js @@ -1,4 +1,5 @@ -import { buildStyle, getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; +import { createPostHideFunctions } from '../../utils/hide_posts.js'; +import { filterPostElements } from '../../utils/interface.js'; import { onNewPosts } from '../../utils/mutations.js'; import { isMyPost } from '../../utils/react_props.js'; import { followingTimelineFilter } from '../../utils/timeline_id.js'; @@ -6,19 +7,12 @@ import { followingTimelineFilter } from '../../utils/timeline_id.js'; const excludeClass = 'xkit-tweaks-hide-my-posts-done'; const timeline = followingTimelineFilter; -const hiddenAttribute = 'data-tweaks-hide-my-posts-hidden'; -export const styleElement = buildStyle(` -[${hiddenAttribute}] { - content: linear-gradient(transparent, transparent); - height: 0; -}`); +const { hidePost, showPosts } = createPostHideFunctions({ id: 'tweaks-hide-my-posts' }); const processPosts = async function (postElements) { filterPostElements(postElements, { excludeClass, timeline }).forEach(async postElement => { - const myPost = await isMyPost(postElement); - - if (myPost) { - getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); + if (await isMyPost(postElement)) { + hidePost(postElement); } }); }; @@ -31,5 +25,5 @@ export const clean = async function () { onNewPosts.removeListener(processPosts); $(`.${excludeClass}`).removeClass(excludeClass); - $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + showPosts(); }; diff --git a/src/utils/hide_posts.js b/src/utils/hide_posts.js new file mode 100644 index 0000000000..e5ca971dd6 --- /dev/null +++ b/src/utils/hide_posts.js @@ -0,0 +1,141 @@ +import { button, div } from './dom.js'; +import { buildStyle, getTimelineItemWrapper } from './interface.js'; +import { anyPostPermalinkTimelineFilter, timelineSelector } from './timeline_id.js'; + +const controlsClass = 'xkit-hidden-post-controls'; + +// Remove outdated elements when loading module +$(`.${controlsClass}`).remove(); + +const styleElement = buildStyle(` +.${controlsClass} { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: var(--space-m); + border-radius: 8px; + margin-bottom: var(--space-m); + + background-color: var(--content-panel); + background-image: linear-gradient(var(--content-tint), var(--content-tint)); + color: var(--content-fg-secondary); + + font-family: var(--font-family-modern); + font-size: 1rem; + font-weight: 350; + line-height: 1.5rem; +} + +.${controlsClass} + .${controlsClass} { + display: none; +} + +.${controlsClass} button { + flex-shrink: 0; + padding: 10px 16px; + border-radius: 9999px; + + font-family: var(--font-family-modern); + font-size: 1rem; + font-weight: 500; + line-height: 1.5rem; +} + +.${controlsClass} button:hover { + background-color: var(--content-tint); + color: var(--content-fg); +} + +.${controlsClass} button:active { + background-color: var(--content-tint-strong); +} + +.${controlsClass} button:focus-visible { + outline: 2px solid var(--content-ui-focus); + outline-offset: 2px; +} + +/* Palettes for Tumblr font override compatibility */ +:root[style*="--font-family-modern"] .${controlsClass} { + font-weight: normal; +} +:root[style*="--font-family-modern"] .${controlsClass} button { + font-weight: bold; +} +`); +document.documentElement.append(styleElement); + +/** + * @typedef {object} PostHideFunctions + * @property {(postElement: Element) => void} hidePost Hides a post. + * @property {(postElement: Element) => void} showPost Shows a post that was previously hidden. + * @property {() => void} showPosts Shows all posts hidden by this post hiding instance. + */ + +/** + * @typedef {object} PermalinkPageOptions + * @property {string} message Message to display in permalink page controls (e.g. "This post contains a blocked blog!") + */ + +/** + * @param {object} options Destructured + * @param {string} options.id Identifier for this post hiding instance (must be unique) + * @param {PermalinkPageOptions} [options.permalinkPageControls] If specified, single posts on permalink pages are hidden with an informative, dismissable UI + * @returns {PostHideFunctions} Functions to hide/show posts + */ +export const createPostHideFunctions = ({ id, permalinkPageControls }) => { + const hiddenAttribute = `data-xkit-${id}-hidden`; + + const controlledHiddenAttribute = `data-xkit-${id}-hidden-controlled`; + const controlsAttribute = `data-xkit-${id}-hidden-controls`; + + styleElement.textContent += ` + [${hiddenAttribute}], [${controlsAttribute}] ~ div [${controlledHiddenAttribute}] { + content: linear-gradient(transparent, transparent); + height: 0; + } + `; + + const addPermalinkPageControls = (postElement, timelineElement) => { + const timelineItemWrapper = getTimelineItemWrapper(postElement); + if (timelineItemWrapper.getAttribute(controlledHiddenAttribute) !== '') { + timelineItemWrapper.setAttribute(controlledHiddenAttribute, ''); + + const { message } = permalinkPageControls; + const controlsElement = div({ class: controlsClass, [controlsAttribute]: id }, [ + message, + button({ click: () => controlsElement.remove() }, ['View post']), + ]); + timelineElement.prepend(controlsElement); + } + }; + + const hidePost = postElement => { + const timelineElement = postElement.closest(timelineSelector); + const onPermalinkPage = anyPostPermalinkTimelineFilter(timelineElement); + + if (onPermalinkPage) { + if (permalinkPageControls) { + addPermalinkPageControls(postElement, timelineElement); + } else { + // do nothing; avoid hiding single post and making permalink page look broken + } + } else { + getTimelineItemWrapper(postElement).setAttribute(hiddenAttribute, ''); + } + }; + const showPost = postElement => { + getTimelineItemWrapper(postElement).removeAttribute(hiddenAttribute); + getTimelineItemWrapper(postElement).removeAttribute(controlledHiddenAttribute); + postElement.closest(timelineSelector)?.querySelector(`[${controlsAttribute}]`)?.remove(); + }; + const showPosts = () => { + $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); + $(`[${controlledHiddenAttribute}]`).removeAttr(controlledHiddenAttribute); + $(`[${controlsAttribute}]`).remove(); + }; + showPosts(); + + return { hidePost, showPost, showPosts }; +};