diff --git a/src/features/mutual_checker/index.js b/src/features/mutual_checker/index.js index a5cd241f1d..d7ed8f6ce8 100644 --- a/src/features/mutual_checker/index.js +++ b/src/features/mutual_checker/index.js @@ -6,7 +6,8 @@ import { onNewPosts, onNewNotifications, pageModifications } from '../../utils/m import { getPreferences } from '../../utils/preferences.js'; import { blogData, notificationObject, timelineObject } from '../../utils/react_props.js'; import { buildSvg } from '../../utils/remixicon.js'; -import { followingTimelineSelector } from '../../utils/timeline_id.js'; +import { followingTimelineFilter } from '../../utils/timeline_id.js'; +import { timelineTabs } from '../../utils/timeline_tabs.js'; import { apiFetch } from '../../utils/tumblr_helpers.js'; import { primaryBlogName, userBlogNames } from '../../utils/user.js'; @@ -38,7 +39,7 @@ const styleElement = buildStyle(` isolation: isolate; } - ${followingTimelineSelector} [${hiddenAttribute}] { + .xkit-timeline-controls[data-mode="mutual-checker-only-mutuals"] ~ div > [${hiddenAttribute}] { content: linear-gradient(transparent, transparent); height: 0; } @@ -73,6 +74,8 @@ const alreadyProcessed = postElement => postElement.querySelector(`.${mutualIconClass}`); const addIcons = function (postElements) { + timelineTabs.process(); + filterPostElements(postElements, { includeFiltered: true }).forEach(async postElement => { if (alreadyProcessed(postElement)) return; @@ -146,6 +149,9 @@ export const main = async function () { onNewPosts.addListener(addIcons); pageModifications.register(`${keyToCss('blogCard')} ${keyToCss('blogCardBlogLink')} > a`, addBlogCardIcons); + if (showOnlyMutuals) { + timelineTabs.register({ id: 'mutual-checker-only-mutuals', label: 'From Mutuals', timelineFilter: followingTimelineFilter }); + } if (showOnlyMutualNotifications) { document.documentElement.append(onlyMutualsStyleElement); onNewNotifications.addListener(processNotifications); @@ -185,4 +191,6 @@ export const clean = async function () { $(`.${mutualsClass}`).removeClass(mutualsClass); $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); $(`.${mutualIconClass}`).remove(); + + timelineTabs.unregister('mutual-checker-only-mutuals'); }; diff --git a/src/features/show_originals/index.css b/src/features/show_originals/index.css index 6540868345..409791ad5d 100644 --- a/src/features/show_originals/index.css +++ b/src/features/show_originals/index.css @@ -1,39 +1,4 @@ -[data-show-originals="on"] ~ div > [data-show-originals-hidden] { +.xkit-timeline-controls[data-mode="show-originals"] ~ div > [data-show-originals-hidden] { content: linear-gradient(transparent, transparent); height: 0; } - -.xkit-show-originals-lengthened { - min-height: 100vh; -} - -.xkit-show-originals-controls { - color: var(--blog-title-color, rgb(var(--white-on-dark))); - display: flex; - font-weight: 700; - margin-bottom: 20px; -} - -.xkit-show-originals-controls > a { - flex: 1; - padding: 14px 16px; - text-align: center; - text-decoration: none; - text-transform: capitalize; -} - -.xkit-show-originals-controls > a { - cursor: pointer; -} - -.xkit-show-originals-controls > a[data-mode="disabled"] { - cursor: not-allowed; - opacity: 0.65; -} - -[data-show-originals="on"].xkit-show-originals-controls > a[data-mode="on"], -[data-show-originals="off"].xkit-show-originals-controls > a[data-mode="off"], -.xkit-show-originals-controls > a[data-mode="disabled"] { - box-shadow: inset 0 -3px 0 var(--blog-link-color, rgb(var(--deprecated-accent))); - color: var(--blog-link-color, rgb(var(--deprecated-accent))); -} diff --git a/src/features/show_originals/index.js b/src/features/show_originals/index.js index 5a2fb49bda..7897fcf186 100644 --- a/src/features/show_originals/index.js +++ b/src/features/show_originals/index.js @@ -1,5 +1,4 @@ import { keyToCss } from '../../utils/css_map.js'; -import { a, div } from '../../utils/dom.js'; import { filterPostElements, getTimelineItemWrapper } from '../../utils/interface.js'; import { translate } from '../../utils/language_data.js'; import { onNewPosts } from '../../utils/mutations.js'; @@ -10,11 +9,11 @@ import { anyBlogPostsTimelineFilter, blogPostsTimelineFilter, blogSubsTimelineFilter, - timelineSelector, anyCommunityTimelineFilter, communitiesTimelineFilter, blogpackTimelineFilter, } from '../../utils/timeline_id.js'; +import { timelineTabs } from '../../utils/timeline_tabs.js'; import { userBlogs } from '../../utils/user.js'; const hiddenAttribute = 'data-show-originals-hidden'; @@ -23,7 +22,7 @@ const controlsClass = 'xkit-show-originals-controls'; const channelSelector = `${keyToCss('bar')} ~ *`; -const storageKey = 'show_originals.savedModes'; +// const storageKey = 'show_originals.savedModes'; const includeFiltered = true; let showOwnReblogs; @@ -32,45 +31,6 @@ let showReblogsOfNotFollowing; let whitelist; let disabledBlogs; -const lengthenTimeline = async (timeline) => { - if (!timeline.querySelector(keyToCss('manualPaginatorButtons'))) { - timeline.classList.add(lengthenedClass); - } -}; - -const createButton = (buttonText, onclick, mode) => - a({ 'data-mode': mode, click: onclick }, [buttonText]); - -const addControls = async (timelineElement, location) => { - const controls = div({ class: controlsClass }); - controls.dataset.location = location; - - timelineElement.prepend(controls); - - const handleClick = async ({ currentTarget: { dataset: { mode } } }) => { - controls.dataset.showOriginals = mode; - - const { [storageKey]: savedModes = {} } = await browser.storage.local.get(storageKey); - savedModes[location] = mode; - browser.storage.local.set({ [storageKey]: savedModes }); - }; - - const onButton = createButton(translate('Original Posts'), handleClick, 'on'); - const offButton = createButton(translate('All posts'), handleClick, 'off'); - const disabledButton = createButton(translate('All posts'), null, 'disabled'); - - if (location === 'disabled') { - controls.append(disabledButton); - } else { - controls.append(onButton, offButton); - - lengthenTimeline(timelineElement); - const { [storageKey]: savedModes = {} } = await browser.storage.local.get(storageKey); - const mode = savedModes[location] ?? 'on'; - controls.dataset.showOriginals = mode; - } -}; - const getLocation = timelineElement => { const isBlog = anyBlogPostsTimelineFilter(timelineElement) && !timelineElement.matches(channelSelector); @@ -86,22 +46,14 @@ const getLocation = timelineElement => { return Object.keys(on).find(location => on[location]); }; -const processTimelines = async () => { - [...document.querySelectorAll(timelineSelector)].forEach(async timelineElement => { - const location = getLocation(timelineElement); - - const currentControls = [...timelineElement.children] - .find(element => element.matches(`.${controlsClass}`)); - - if (currentControls?.dataset?.location !== location) { - currentControls?.remove(); - if (location) addControls(timelineElement, location); - } - }); +const timelineTabFilter = timelineElement => { + const location = getLocation(timelineElement); + if (location === 'disabled') return 'disabled'; + return Boolean(location); }; const processPosts = async function (postElements) { - processTimelines(); + timelineTabs.process(); filterPostElements(postElements, { includeFiltered }) .forEach(async postElement => { @@ -135,6 +87,7 @@ export const main = async function () { disabledBlogs = [...whitelist, ...showOwnReblogs ? nonGroupUserBlogs : []]; onNewPosts.addListener(processPosts); + timelineTabs.register({ id: 'show-originals', label: translate('Original Posts'), timelineFilter: timelineTabFilter }); }; export const clean = async function () { @@ -143,6 +96,8 @@ export const clean = async function () { $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); $(`.${lengthenedClass}`).removeClass(lengthenedClass); $(`.${controlsClass}`).remove(); + + timelineTabs.unregister('show-originals'); }; export const stylesheet = true; diff --git a/src/utils/timeline_tabs.js b/src/utils/timeline_tabs.js new file mode 100644 index 0000000000..e1d455e00e --- /dev/null +++ b/src/utils/timeline_tabs.js @@ -0,0 +1,135 @@ +import { keyToCss } from './css_map.js'; +import { a, div } from './dom.js'; +import { buildStyle } from './interface.js'; +import { translate } from './language_data.js'; +import { timelineSelector } from './timeline_id.js'; + +const controlsClass = 'xkit-timeline-controls'; +const lengthenedClass = 'xkit-timeline-controls-lengthened'; + +const cachedIdStringAttribute = 'xkit-timeline-controls-cached-id'; + +// Remove outdated elements when loading module +$(`.${controlsClass}`).remove(); + +const styleElement = buildStyle(` +.${lengthenedClass} { + min-height: 100vh; +} + +.${controlsClass} { + color: var(--blog-title-color, rgb(var(--white-on-dark))); + display: flex; + font-weight: 700; + margin-bottom: 20px; +} + +.${controlsClass} > a { + flex: 1; + padding: 14px 16px; + text-align: center; + text-decoration: none; + text-transform: capitalize; + cursor: pointer; +} + +.${controlsClass} > a.disabled { + cursor: not-allowed; + opacity: 0.65; +} + +.${controlsClass} > a.active { + box-shadow: inset 0 -3px 0 var(--blog-link-color, rgb(var(--deprecated-accent))); + color: var(--blog-link-color, rgb(var(--deprecated-accent))); +} +`); +document.documentElement.append(styleElement); + +export const timelineTabs = Object.freeze({ + registered: new Map(), + + register (options) { + if (this.registered.has(options.id) === false) { + this.registered.set(options.id, options); + this.clean(); + this.process(); + } + }, + + unregister (id) { + this.registered.delete(id); + this.clean(); + this.process(); + }, + + process () { + [...document.querySelectorAll(timelineSelector)].forEach(async timelineElement => { + const { dataset: { timeline, timelineId } } = timelineElement; + const idString = timelineId + ? `timelineId:${timelineId}` + : timeline + ? `timeline:${timeline}` + : undefined; + const cachedIdString = timelineElement.getAttribute(cachedIdStringAttribute); + + if (idString !== cachedIdString) { + timelineElement.setAttribute(cachedIdStringAttribute, idString); + timelineElement.querySelector(`.${controlsClass}`)?.remove(); + + const controls = div({ class: controlsClass }); + + const onclick = async ({ currentTarget }) => { + const { dataset: { mode } } = currentTarget; + if (!currentTarget.classList.contains('disabled')) { + controls.dataset.mode = mode; + + buttons.forEach(button => button.classList.toggle('active', button.dataset.mode === mode)); + + // const { [storageKey]: savedModes = {} } = await browser.storage.local.get(storageKey); + // savedModes[location] = mode; + // browser.storage.local.set({ [storageKey]: savedModes }); + } + }; + + const createButton = ({ id, label, shown }) => + a({ 'data-mode': id, click: onclick, class: shown === 'disabled' ? 'disabled' : '' }, [label]); + + const buttons = [...this.registered.values()] + .map(({ id, timelineFilter, label }) => ({ + id, + label, + shown: timelineFilter(timelineElement), + })) + .filter(({ shown }) => shown) + .sort((a, b) => a.id.localeCompare(b.id)) + .map(createButton); + + if (buttons.length) { + buttons.push(createButton({ id: '', label: translate('All posts'), shown: true })); + + // temp + buttons.at(-1).click(); + + controls.replaceChildren(...buttons); + timelineElement.prepend(controls); + + if (!timelineElement.querySelector(keyToCss('manualPaginatorButtons'))) { + timelineElement.classList.add(lengthenedClass); + } + + // const { [storageKey]: savedModes = {} } = await browser.storage.local.get(storageKey); + // const mode = savedModes[location] ?? 'on'; + // controls.dataset.showOriginals = mode; + } + } + }); + }, + + clean () { + [...document.querySelectorAll(timelineSelector)].forEach(async timelineElement => { + timelineElement.removeAttribute(cachedIdStringAttribute); + timelineElement.querySelector(`.${controlsClass}`)?.remove(); + }); + }, + +});