Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions src/features/classic_footer/feature.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,12 @@
"class_name": "ri-layout-bottom-line",
"color": "white",
"background_color": "#395875"
},
"preferences": {
"noReblogMenu": {
"type": "checkbox",
"label": "Turn reblog buttons back into links",
"default": true
}
}
}
131 changes: 127 additions & 4 deletions src/features/classic_footer/index.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { keyToCss } from '../../utils/css_map.js';
import { dom } from '../../utils/dom.js';
import { a, button, span } from '../../utils/dom.js';
import { buildStyle, postSelector } from '../../utils/interface.js';
import { pageModifications } from '../../utils/mutations.js';
import { getPreferences } from '../../utils/preferences.js';
import { timelineObject } from '../../utils/react_props.js';

const noteCountClass = 'xkit-classic-footer-note-count';
const reblogLinkClass = 'xkit-classic-footer-reblog-link';

const postOrRadarSelector = `:is(${postSelector}, aside ${keyToCss('radar')})`;
const postOwnerControlsSelector = `${postOrRadarSelector} ${keyToCss('postOwnerControls')}`;
const footerContentSelector = `${postOrRadarSelector} article footer ${keyToCss('footerContent')}`;
const engagementControlsSelector = `${footerContentSelector} ${keyToCss('engagementControls')}`;
const replyButtonSelector = `${engagementControlsSelector} button:has(svg use[href="#managed-icon__ds-reply-outline-24"])`;
const reblogButtonSelector = `${engagementControlsSelector} button:has(svg use:is([href="#managed-icon__ds-reblog-24"], [href="#managed-icon__ds-queue-add-24"]))`;
const quickActionsSelector = 'svg[style="--icon-color-primary: var(--brand-blue);"], svg[style="--icon-color-primary: var(--brand-purple);"]';
const closeNotesButtonSelector = `${postOrRadarSelector} ${keyToCss('postActivity')} [role="tablist"] button:has(svg use[href="#managed-icon__ds-ui-x-20"])`;
const reblogMenuPortalSelector = 'div[id^="portal/"]:has(div[role="menu"] a[role="menuitem"][href^="/reblog/"])';

const locale = document.documentElement.lang;
const noteCountFormat = new Intl.NumberFormat(locale);
Expand Down Expand Up @@ -68,6 +73,46 @@ export const styleElement = buildStyle(`
text-overflow: ellipsis;
white-space: nowrap;
}

.${reblogLinkClass} {
display: flex;
padding: 8px;
border-radius: 9999px;

color: var(--content-fg-secondary);
}
.${reblogLinkClass}:hover {
background-color: var(--brand-green-tint);
color: var(--brand-green);
}
.${reblogLinkClass}:focus-visible {
outline: 2px solid var(--brand-green);
outline-offset: -2px;
}
.${reblogLinkClass}:active {
background-color: var(--brand-green-tint-strong);
color: var(--brand-green);
}

@container (width: 260px) {
.${noteCountClass}, .${reblogLinkClass} {
padding: 6px;
}
}

span:has(svg[style="--icon-color-primary: var(--brand-green);"]) > .${reblogLinkClass} {
color: var(--brand-green);
}
span:has(${quickActionsSelector}) > .${reblogLinkClass} {
display: none;
}

.${reblogLinkClass} ~ :is(${reblogButtonSelector}):not(:has(${quickActionsSelector})) {
display: none;
}
body:has(.${reblogLinkClass}) > ${reblogMenuPortalSelector}:not(:has([role="menu"][aria-labelledby])) {
display: none;
}
`);

const onNoteCountClick = (event) => {
Expand All @@ -84,20 +129,98 @@ const processPosts = (postElements) => postElements.forEach(async postElement =>
postElement.querySelector(`.${noteCountClass}`)?.remove();

const { noteCount } = await timelineObject(postElement);
const noteCountButton = dom('button', { class: noteCountClass }, { click: onNoteCountClick }, [
dom('span', null, null, [noteCountFormat.format(noteCount)]),
` ${noteCount === 1 ? 'note' : 'notes'}`
const noteCountButton = button({ class: noteCountClass, click: onNoteCountClick }, [
span({}, [noteCountFormat.format(noteCount)]), ` ${noteCount === 1 ? 'note' : 'notes'}`
]);

const engagementControls = postElement.querySelector(engagementControlsSelector);
engagementControls?.before(noteCountButton);
});

const getReblogMenuItem = async (reblogButton, href) => {
const reblogMenuItemSelector = `${reblogMenuPortalSelector} a[href^="${href}"]`;

return document.querySelector(reblogMenuItemSelector) ?? new Promise(resolve => {
// Start observing the document body for the relevant reblog menu.
const mutationObserver = new MutationObserver(mutations => {
const addedNodes = mutations.flatMap(({ addedNodes }) => [...addedNodes]);
const addedElements = addedNodes.filter(addedNode => addedNode instanceof Element);

for (const addedElement of addedElements) {
const reblogMenuItem = addedElement.querySelector(reblogMenuItemSelector);
if (reblogMenuItem) resolve(reblogMenuItem);
}
});
mutationObserver.observe(document.body, { childList: true });

// Open the reblog menu for the observer to find.
reblogButton.click();

// Disconnect the observer after 5 seconds. If we've gone this long without
// finding the menu item, anything we do cannot be considered to have been
// triggered by user input, so we should give up and do nothing at all.
setTimeout(() => mutationObserver.disconnect(), 5000);
});
};

const onReblogLinkClick = (event) => {
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;

event.preventDefault();

const reblogButton = event.currentTarget.parentElement.querySelector(reblogButtonSelector);
const href = event.currentTarget.getAttribute('href');

getReblogMenuItem(reblogButton, href).then(reblogMenuItem => reblogMenuItem.click());
};

const processReblogButtons = (reblogButtons) => reblogButtons.forEach(async reblogButton => {
const { blogName, canReblog, idString, reblogKey } = await timelineObject(reblogButton);

if (reblogButton.matches(`.${reblogLinkClass} ~ :scope`)) {
$(reblogButton).siblings(`.${reblogLinkClass}`).remove();
}

if (!canReblog) return;

const reblogLink = a({
'aria-label': reblogButton.getAttribute('aria-label'),
class: reblogLinkClass,
click: onReblogLinkClick,
href: `/reblog/${blogName}/${idString}/${reblogKey}`
}, [
buildStyle(`${reblogMenuPortalSelector}:has([aria-labelledby="${reblogButton.id}"]) { display: none; }`),
reblogButton.firstElementChild.cloneNode(true)]
);

reblogButton.before(reblogLink);
});

const restoreReblogButtons = () => {
pageModifications.unregister(processReblogButtons);
$(`.${reblogLinkClass}`).remove();
};

export const onStorageChanged = async function (changes) {
const { 'classic_footer.preferences.noReblogMenu': noReblogMenuChanges } = changes;
if (noReblogMenuChanges && noReblogMenuChanges.oldValue === undefined) return;

const { newValue: noReblogMenu } = noReblogMenuChanges;
noReblogMenu
? pageModifications.register(reblogButtonSelector, processReblogButtons)
: restoreReblogButtons();
};

export const main = async function () {
pageModifications.register(`${postOrRadarSelector} article`, processPosts);

const { noReblogMenu } = await getPreferences('classic_footer');
if (noReblogMenu) pageModifications.register(reblogButtonSelector, processReblogButtons);
};

export const clean = async function () {
pageModifications.unregister(processPosts);
$(`.${noteCountClass}`).remove();

restoreReblogButtons();
};
28 changes: 4 additions & 24 deletions src/features/quick_reblog/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -195,22 +195,8 @@ footer:is(.published, .queue, .draft) :is(button:not([role]), a[href^="/reblog/"
position: relative;
}

/* Cut-out for tick in 2025 post footer, "split notes count" variant */
footer:is(.published, .queue, .draft) a[href^="/reblog/"] svg:has(use[href="#managed-icon__ds-reblog-24"]) {
-webkit-mask-image: radial-gradient(
calc(14.7px / 2) calc(18px / 2) at bottom 5px right 4px,
transparent 99%,
white 100%
);
mask-image: radial-gradient(
calc(14.7px / 2) calc(18px / 2) at bottom 5px right 4px,
transparent 99%,
white 100%
);
}

/* Cut-out for tick in 2025 post footer, "single action" variant */
footer:is(.published, .queue, .draft) button svg:has(use[href="#managed-icon__ds-reblog-24"]) {
/* Cut-out for tick in 2025 post footer */
footer:is(.published, .queue, .draft) :is(a[href^="/reblog/"], button) svg:has(use[href="#managed-icon__ds-reblog-24"]) {
-webkit-mask-image: radial-gradient(
calc(14.7px / 2) calc(18px / 2) at bottom 4px left 21px,
transparent 99%,
Expand Down Expand Up @@ -246,14 +232,8 @@ footer:is(.published, .queue, .draft) a[href^="/reblog/"]:has(use[href="#managed
right: -5px;
}

/* 2025 post footer, "split notes count" variant */
footer:is(.published, .queue, .draft) a[href^="/reblog/"]:has(use[href="#managed-icon__ds-reblog-24"])::after {
bottom: 4px;
right: 5px;
}

/* 2025 post footer, "single action" variant */
footer:is(.published, .queue, .draft) button:has(use[href="#managed-icon__ds-reblog-24"])::after {
/* 2025 post footer */
footer:is(.published, .queue, .draft) :is(a[href^="/reblog/"], button):has(use[href="#managed-icon__ds-reblog-24"])::after {
bottom: 4px;
left: 21px;
}
Expand Down