diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 8a7f121a4..3761ed140 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -29,7 +29,6 @@ jobs: env: WEB_EXT_API_KEY: ${{ secrets.WEB_EXT_API_KEY }} WEB_EXT_API_SECRET: ${{ secrets.WEB_EXT_API_SECRET }} - WEB_EXT_ID: ${{ secrets.WEB_EXT_ID }} chrome: name: Chrome runs-on: ubuntu-latest diff --git a/src/features/collapsed_queue.js b/src/features/collapsed_queue.js index dbeaf1667..1d6561a57 100644 --- a/src/features/collapsed_queue.js +++ b/src/features/collapsed_queue.js @@ -2,16 +2,17 @@ import { filterPostElements } from '../utils/interface.js'; import { getPreferences } from '../utils/preferences.js'; import { onNewPosts } from '../utils/mutations.js'; import { keyToCss } from '../utils/css_map.js'; +import { anyQueueTimelineFilter, anyDraftsTimelineFilter } from '../utils/timeline_id.js'; const excludeClass = 'xkit-collapsed-queue-done'; const wrapperClass = 'xkit-collapsed-queue-wrapper'; const containerClass = 'xkit-collapsed-queue-container'; const footerSelector = keyToCss('footerWrapper'); -let timelineRegex; +let timeline; const processPosts = async function (postElements) { - filterPostElements(postElements, { excludeClass, timeline: timelineRegex }).forEach(async postElement => { + filterPostElements(postElements, { excludeClass, timeline }).forEach(async postElement => { const headerElement = postElement.querySelector('header'); const footerElement = postElement.querySelector(footerSelector); @@ -30,11 +31,10 @@ export const main = async function () { const { runInQueue, runInDrafts } = await getPreferences('collapsed_queue'); if (![runInQueue, runInDrafts].some(Boolean)) return; - const regexGroup = [ - ...runInQueue ? ['queue'] : [], - ...runInDrafts ? ['draft'] : [] - ].join('|'); - timelineRegex = new RegExp(`/v2/blog/[^/]+/posts/(${regexGroup})`); + timeline = [ + runInQueue && anyQueueTimelineFilter, + runInDrafts && anyDraftsTimelineFilter + ].filter(Boolean); onNewPosts.addListener(processPosts); }; diff --git a/src/features/mutual_checker.js b/src/features/mutual_checker.js index ef5577bfd..cb6614c62 100644 --- a/src/features/mutual_checker.js +++ b/src/features/mutual_checker.js @@ -7,6 +7,7 @@ import { onNewPosts, onNewNotifications, pageModifications } from '../utils/muta import { dom } from '../utils/dom.js'; import { getPreferences } from '../utils/preferences.js'; import { translate } from '../utils/language_data.js'; +import { followingTimelineSelector } from '../utils/timeline_id.js'; const mutualIconClass = 'xkit-mutual-icon'; const hiddenAttribute = 'data-mutual-checker-hidden'; @@ -34,7 +35,7 @@ const styleElement = buildStyle(` margin-right: 0.5ch; } - [data-timeline="/v2/timeline/dashboard"] [${hiddenAttribute}] article { + ${followingTimelineSelector} [${hiddenAttribute}] article { display: none; } diff --git a/src/features/no_recommended/hide_recommended_posts.js b/src/features/no_recommended/hide_recommended_posts.js index cbc3a6ad0..4db701d31 100644 --- a/src/features/no_recommended/hide_recommended_posts.js +++ b/src/features/no_recommended/hide_recommended_posts.js @@ -1,11 +1,12 @@ import { buildStyle, getTimelineItemWrapper, filterPostElements, postSelector } from '../../utils/interface.js'; import { onNewPosts } from '../../utils/mutations.js'; import { timelineObject } from '../../utils/react_props.js'; +import { followingTimelineFilter } from '../../utils/timeline_id.js'; const excludeClass = 'xkit-no-recommended-posts-done'; const hiddenAttribute = 'data-no-recommended-posts-hidden'; const unHiddenAttribute = 'data-no-recommended-posts-many'; -const timeline = /\/v2\/timeline\/dashboard/; +const timeline = followingTimelineFilter; const includeFiltered = true; const styleElement = buildStyle(` diff --git a/src/features/seen_posts.js b/src/features/seen_posts.js index 7de74b98e..8b16ecedc 100644 --- a/src/features/seen_posts.js +++ b/src/features/seen_posts.js @@ -2,9 +2,10 @@ import { filterPostElements, getTimelineItemWrapper, postSelector } from '../uti import { getPreferences } from '../utils/preferences.js'; import { onNewPosts } from '../utils/mutations.js'; import { keyToCss } from '../utils/css_map.js'; +import { followingTimelineFilter, followingTimelineSelector } from '../utils/timeline_id.js'; const excludeAttribute = 'data-seen-posts-done'; -const timeline = '/v2/timeline/dashboard'; +const timeline = followingTimelineFilter; const includeFiltered = true; const dimAttribute = 'data-seen-posts-seen'; @@ -45,7 +46,7 @@ const markAsSeen = (element) => { }; const lengthenTimelines = () => - [...document.querySelectorAll(`[data-timeline="${timeline}"]`)].forEach(timelineElement => { + [...document.querySelectorAll(followingTimelineSelector)].forEach(timelineElement => { if (!timelineElement.querySelector(keyToCss('manualPaginatorButtons'))) { timelineElement.classList.add(lengthenedClass); } diff --git a/src/features/show_originals.js b/src/features/show_originals.js index 383e7c9e0..39f29eefb 100644 --- a/src/features/show_originals.js +++ b/src/features/show_originals.js @@ -5,12 +5,12 @@ import { onNewPosts } from '../utils/mutations.js'; import { keyToCss } from '../utils/css_map.js'; import { translate } from '../utils/language_data.js'; import { userBlogs } from '../utils/user.js'; +import { followingTimelineFilter, anyBlogTimelineFilter, blogTimelineFilter, blogSubsTimelineFilter, timelineSelector } from '../utils/timeline_id.js'; const hiddenAttribute = 'data-show-originals-hidden'; const lengthenedClass = 'xkit-show-originals-lengthened'; const controlsClass = 'xkit-show-originals-controls'; -const blogTimelineRegex = /^\/v2\/blog\/[a-z0-9-]{1,32}\/posts$/; const channelSelector = `${keyToCss('bar')} ~ *`; const storageKey = 'show_originals.savedModes'; @@ -65,21 +65,20 @@ const addControls = async (timelineElement, location) => { }; const getLocation = timelineElement => { - const { timeline, which } = timelineElement.dataset; - - const isBlog = blogTimelineRegex.test(timeline) && !timelineElement.matches(channelSelector); + const isBlog = + anyBlogTimelineFilter(timelineElement) && !timelineElement.matches(channelSelector); const on = { - dashboard: timeline === '/v2/timeline/dashboard', - disabled: isBlog && disabledBlogs.some(name => timeline === `/v2/blog/${name}/posts`), + dashboard: followingTimelineFilter(timelineElement), + disabled: isBlog && disabledBlogs.some(name => blogTimelineFilter(name)(timelineElement)), peepr: isBlog, - blogSubscriptions: timeline.includes('blog_subscriptions') || which === 'blog_subscriptions' + blogSubscriptions: blogSubsTimelineFilter(timelineElement) }; return Object.keys(on).find(location => on[location]); }; const processTimelines = async () => { - [...document.querySelectorAll('[data-timeline]')].forEach(async timelineElement => { + [...document.querySelectorAll(timelineSelector)].forEach(async timelineElement => { const location = getLocation(timelineElement); const currentControls = [...timelineElement.children] diff --git a/src/features/tag_tracking_plus.js b/src/features/tag_tracking_plus.js index c79732e36..1469d67f2 100644 --- a/src/features/tag_tracking_plus.js +++ b/src/features/tag_tracking_plus.js @@ -6,6 +6,7 @@ import { onNewPosts, pageModifications } from '../utils/mutations.js'; import { dom } from '../utils/dom.js'; import { addSidebarItem, removeSidebarItem } from '../utils/sidebar.js'; import { getPreferences } from '../utils/preferences.js'; +import { tagTimelineFilter } from '../utils/timeline_id.js'; const storageKey = 'tag_tracking_plus.trackedTagTimestamps'; let timestamps; @@ -116,7 +117,7 @@ const processPosts = async function (postElements) { const currentTag = decodeURIComponent(encodedCurrentTag); if (!trackedTags.includes(currentTag)) return; - const timeline = new RegExp(`/v2/hubs/${encodedCurrentTag}/timeline`); + const timeline = tagTimelineFilter(currentTag); let updated = false; diff --git a/src/features/tweaks/caught_up_line.js b/src/features/tweaks/caught_up_line.js index a6e982b3f..b0ab92ee7 100644 --- a/src/features/tweaks/caught_up_line.js +++ b/src/features/tweaks/caught_up_line.js @@ -1,6 +1,7 @@ import { keyToCss } from '../../utils/css_map.js'; import { buildStyle, getTimelineItemWrapper } from '../../utils/interface.js'; import { pageModifications } from '../../utils/mutations.js'; +import { followingTimelineSelector } from '../../utils/timeline_id.js'; const hiddenAttribute = 'data-tweaks-caught-up-line-title'; const borderAttribute = 'data-tweaks-caught-up-line-border'; @@ -23,7 +24,7 @@ const styleElement = buildStyle(` `); const listTimelineObjectSelector = keyToCss('listTimelineObject'); -const tagChicletCarouselLinkSelector = `[data-timeline="/v2/timeline/dashboard"] ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; +const tagChicletCarouselLinkSelector = `${followingTimelineSelector} ${listTimelineObjectSelector} ${keyToCss('tagChicletLink')}`; const createCaughtUpLine = tagChicletCarouselItems => tagChicletCarouselItems .map(getTimelineItemWrapper) diff --git a/src/features/tweaks/hide_liked_posts.js b/src/features/tweaks/hide_liked_posts.js index e452fa76b..65f43d23f 100644 --- a/src/features/tweaks/hide_liked_posts.js +++ b/src/features/tweaks/hide_liked_posts.js @@ -1,8 +1,9 @@ import { onNewPosts } from '../../utils/mutations.js'; import { buildStyle, getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; import { isMyPost, timelineObject } from '../../utils/react_props.js'; +import { followingTimelineFilter } from '../../utils/timeline_id.js'; -const timeline = /\/v2\/timeline\/dashboard/; +const timeline = followingTimelineFilter; const hiddenAttribute = 'data-tweaks-hide-liked-posts-hidden'; const styleElement = buildStyle(`[${hiddenAttribute}] article { display: none; }`); diff --git a/src/features/tweaks/hide_my_posts.js b/src/features/tweaks/hide_my_posts.js index c572fd5ac..6b10b4923 100644 --- a/src/features/tweaks/hide_my_posts.js +++ b/src/features/tweaks/hide_my_posts.js @@ -1,9 +1,10 @@ import { onNewPosts } from '../../utils/mutations.js'; import { buildStyle, getTimelineItemWrapper, filterPostElements } from '../../utils/interface.js'; import { isMyPost } from '../../utils/react_props.js'; +import { followingTimelineFilter } from '../../utils/timeline_id.js'; const excludeClass = 'xkit-tweaks-hide-my-posts-done'; -const timeline = /\/v2\/timeline\/dashboard/; +const timeline = followingTimelineFilter; const hiddenAttribute = 'data-tweaks-hide-my-posts-hidden'; const styleElement = buildStyle(`[${hiddenAttribute}] article { display: none; }`); diff --git a/src/utils/inject.js b/src/utils/inject.js index 5f214ee6d..6bf9ec994 100644 --- a/src/utils/inject.js +++ b/src/utils/inject.js @@ -1,4 +1,8 @@ /** + * Runs a script in the page's "main" execution environment and returns its result. + * This permits access to variables exposed by the Tumblr web platform that are normally inaccessible + * in the content script sandbox. + * See the src/main_world directory and [../main_world/index.js](../main_world/index.js). * @param {string} path - Absolute path of script to inject (will be fed to `runtime.getURL()`) * @param {Array} [args] - Array of arguments to pass to the script * @param {Element} [target] - Target element; will be accessible as the `this` value in the injected function. diff --git a/src/utils/interface.js b/src/utils/interface.js index 757653e90..3eb252ca5 100644 --- a/src/utils/interface.js +++ b/src/utils/interface.js @@ -1,5 +1,6 @@ import { keyToCss } from './css_map.js'; import { dom } from './dom.js'; +import { timelineSelector } from './timeline_id.js'; export const postSelector = '[tabindex="-1"][data-id]'; export const blogViewSelector = '[style*="--blog-title-color"] *'; @@ -19,7 +20,9 @@ const targetWrapperSelector = keyToCss( * @returns {Element | null} The timeline item wrapper */ export const getTimelineItemWrapper = element => - element.closest(cellSelector) || element.closest(listTimelineObjectSelector); + (element.closest('[data-timeline-id]') && element.closest(listTimelineObjectSelector)?.parentElement) || + element.closest(cellSelector) || + element.closest(listTimelineObjectSelector); /** * @param {Element} element Element within a popover wrapper @@ -35,7 +38,7 @@ export const getPopoverWrapper = element => { /** * @typedef {object} PostFilterOptions * @property {string} [excludeClass] - Classname to exclude and add - * @property {RegExp|string} [timeline] - Filter results to matching [data-timeline] children + * @property {Function|Function[]} [timeline] - Filter results to matching timeline element children * @property {boolean} [noBlogView] - Whether to exclude posts in the blog view modal * @property {boolean} [includeFiltered] - Whether to include filtered posts */ @@ -51,10 +54,15 @@ export const filterPostElements = function (postElements, { excludeClass, timeli .map(element => element.closest(postSelector)) .filter(Boolean); - if (timeline instanceof RegExp) { - postElements = postElements.filter(postElement => timeline.test(postElement.closest('[data-timeline]')?.dataset.timeline)); - } else if (timeline) { - postElements = postElements.filter(postElement => timeline === postElement.closest('[data-timeline]')?.dataset.timeline); + if (timeline) { + const timelineFilters = [timeline].flat().filter(Boolean); + postElements = postElements.filter(postElement => { + const timelineElement = postElement.closest(timelineSelector); + return ( + timelineElement && + timelineFilters.some(timelineFilter => timelineFilter(timelineElement)) + ); + }); } if (noBlogView) { diff --git a/src/utils/modals.js b/src/utils/modals.js index 1a2d077a8..3449ee9db 100644 --- a/src/utils/modals.js +++ b/src/utils/modals.js @@ -47,7 +47,12 @@ export const showErrorModal = exception => { showModal({ title: 'Something went wrong.', message: [ - [exception.body?.errors?.[0]?.detail, exception.errors?.[0]?.detail, exception.message] + [ + exception.body?.errors?.[0]?.detail, + exception.errors?.[0]?.detail, + exception.message, + browser.runtime?.id === undefined && 'Please refresh this browser tab!' + ] .filter(Boolean) .join('\n\n') ], diff --git a/src/utils/timeline_id.js b/src/utils/timeline_id.js new file mode 100644 index 000000000..368b1d466 --- /dev/null +++ b/src/utils/timeline_id.js @@ -0,0 +1,43 @@ +const createSelector = (...components) => `:is(${components.filter(Boolean).join(', ')})`; + +export const timelineSelector = ':is([data-timeline], [data-timeline-id])'; + +const anyBlog = '[a-z0-9-]{1,32}'; + +export const followingTimelineFilter = ({ dataset: { timeline, timelineId } }) => + timeline === '/v2/timeline/dashboard' || + timelineId === '/dashboard/following'; + +export const followingTimelineSelector = createSelector( + `[data-timeline="${'/v2/timeline/dashboard'}"]`, + `[data-timeline-id="${'/dashboard/following'}"]` +); + +// includes "channel" user blog view page +export const anyBlogTimelineFilter = ({ dataset: { timeline, timelineId } }) => + timeline?.match(`/v2/blog/${anyBlog}/posts`) || + timelineId?.match(`peepr-posts-${anyBlog}-undefined-undefined-undefined-undefined-undefined-undefined`) || + timelineId?.match(`blog-view-${anyBlog}`); + +// includes "channel" user blog view page +export const blogTimelineFilter = blog => + ({ dataset: { timeline, timelineId } }) => + timeline === `/v2/blog/${blog}/posts` || + timelineId === `peepr-posts-${blog}-undefined-undefined-undefined-undefined-undefined-undefined` || + timelineId === `blog-view-${blog}`; + +export const blogSubsTimelineFilter = ({ dataset: { timeline, which, timelineId } }) => + timeline === '/v2/timeline?which=blog_subscriptions' || + which === 'blog_subscriptions' || + timelineId === '/dashboard/blog_subs'; + +export const anyDraftsTimelineFilter = ({ dataset: { timeline, timelineId } }) => + timeline?.match(`/v2/blog/${anyBlog}/posts/draft`); + +export const anyQueueTimelineFilter = ({ dataset: { timeline, timelineId } }) => + timeline?.match(`/v2/blog/${anyBlog}/posts/queue`); + +export const tagTimelineFilter = tag => + ({ dataset: { timeline, timelineId } }) => + timeline === `/v2/hubs/${encodeURIComponent(tag)}/timeline` || + timelineId?.startsWith(`hubsTimeline-${tag}-recent-`);