Skip to content

Commit cafb84e

Browse files
feat(Classic Footer): "Turn reblog buttons back into links" (#1931)
Co-authored-by: marcustyphoon <[email protected]>
1 parent 4566bca commit cafb84e

File tree

3 files changed

+138
-28
lines changed

3 files changed

+138
-28
lines changed

src/features/classic_footer/feature.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,12 @@
55
"class_name": "ri-layout-bottom-line",
66
"color": "white",
77
"background_color": "#395875"
8+
},
9+
"preferences": {
10+
"noReblogMenu": {
11+
"type": "checkbox",
12+
"label": "Turn reblog buttons back into links",
13+
"default": true
14+
}
815
}
916
}

src/features/classic_footer/index.js

Lines changed: 127 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import { keyToCss } from '../../utils/css_map.js';
2-
import { dom } from '../../utils/dom.js';
2+
import { a, button, span } from '../../utils/dom.js';
33
import { buildStyle, postSelector } from '../../utils/interface.js';
44
import { pageModifications } from '../../utils/mutations.js';
5+
import { getPreferences } from '../../utils/preferences.js';
56
import { timelineObject } from '../../utils/react_props.js';
67

78
const noteCountClass = 'xkit-classic-footer-note-count';
9+
const reblogLinkClass = 'xkit-classic-footer-reblog-link';
810

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

1621
const locale = document.documentElement.lang;
1722
const noteCountFormat = new Intl.NumberFormat(locale);
@@ -68,6 +73,46 @@ export const styleElement = buildStyle(`
6873
text-overflow: ellipsis;
6974
white-space: nowrap;
7075
}
76+
77+
.${reblogLinkClass} {
78+
display: flex;
79+
padding: 8px;
80+
border-radius: 9999px;
81+
82+
color: var(--content-fg-secondary);
83+
}
84+
.${reblogLinkClass}:hover {
85+
background-color: var(--brand-green-tint);
86+
color: var(--brand-green);
87+
}
88+
.${reblogLinkClass}:focus-visible {
89+
outline: 2px solid var(--brand-green);
90+
outline-offset: -2px;
91+
}
92+
.${reblogLinkClass}:active {
93+
background-color: var(--brand-green-tint-strong);
94+
color: var(--brand-green);
95+
}
96+
97+
@container (width: 260px) {
98+
.${noteCountClass}, .${reblogLinkClass} {
99+
padding: 6px;
100+
}
101+
}
102+
103+
span:has(svg[style="--icon-color-primary: var(--brand-green);"]) > .${reblogLinkClass} {
104+
color: var(--brand-green);
105+
}
106+
span:has(${quickActionsSelector}) > .${reblogLinkClass} {
107+
display: none;
108+
}
109+
110+
.${reblogLinkClass} ~ :is(${reblogButtonSelector}):not(:has(${quickActionsSelector})) {
111+
display: none;
112+
}
113+
body:has(.${reblogLinkClass}) > ${reblogMenuPortalSelector}:not(:has([role="menu"][aria-labelledby])) {
114+
display: none;
115+
}
71116
`);
72117

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

86131
const { noteCount } = await timelineObject(postElement);
87-
const noteCountButton = dom('button', { class: noteCountClass }, { click: onNoteCountClick }, [
88-
dom('span', null, null, [noteCountFormat.format(noteCount)]),
89-
` ${noteCount === 1 ? 'note' : 'notes'}`
132+
const noteCountButton = button({ class: noteCountClass, click: onNoteCountClick }, [
133+
span({}, [noteCountFormat.format(noteCount)]), ` ${noteCount === 1 ? 'note' : 'notes'}`
90134
]);
91135

92136
const engagementControls = postElement.querySelector(engagementControlsSelector);
93137
engagementControls?.before(noteCountButton);
94138
});
95139

140+
const getReblogMenuItem = async (reblogButton, href) => {
141+
const reblogMenuItemSelector = `${reblogMenuPortalSelector} a[href^="${href}"]`;
142+
143+
return document.querySelector(reblogMenuItemSelector) ?? new Promise(resolve => {
144+
// Start observing the document body for the relevant reblog menu.
145+
const mutationObserver = new MutationObserver(mutations => {
146+
const addedNodes = mutations.flatMap(({ addedNodes }) => [...addedNodes]);
147+
const addedElements = addedNodes.filter(addedNode => addedNode instanceof Element);
148+
149+
for (const addedElement of addedElements) {
150+
const reblogMenuItem = addedElement.querySelector(reblogMenuItemSelector);
151+
if (reblogMenuItem) resolve(reblogMenuItem);
152+
}
153+
});
154+
mutationObserver.observe(document.body, { childList: true });
155+
156+
// Open the reblog menu for the observer to find.
157+
reblogButton.click();
158+
159+
// Disconnect the observer after 5 seconds. If we've gone this long without
160+
// finding the menu item, anything we do cannot be considered to have been
161+
// triggered by user input, so we should give up and do nothing at all.
162+
setTimeout(() => mutationObserver.disconnect(), 5000);
163+
});
164+
};
165+
166+
const onReblogLinkClick = (event) => {
167+
if (event.ctrlKey || event.metaKey || event.altKey || event.shiftKey) return;
168+
169+
event.preventDefault();
170+
171+
const reblogButton = event.currentTarget.parentElement.querySelector(reblogButtonSelector);
172+
const href = event.currentTarget.getAttribute('href');
173+
174+
getReblogMenuItem(reblogButton, href).then(reblogMenuItem => reblogMenuItem.click());
175+
};
176+
177+
const processReblogButtons = (reblogButtons) => reblogButtons.forEach(async reblogButton => {
178+
const { blogName, canReblog, idString, reblogKey } = await timelineObject(reblogButton);
179+
180+
if (reblogButton.matches(`.${reblogLinkClass} ~ :scope`)) {
181+
$(reblogButton).siblings(`.${reblogLinkClass}`).remove();
182+
}
183+
184+
if (!canReblog) return;
185+
186+
const reblogLink = a({
187+
'aria-label': reblogButton.getAttribute('aria-label'),
188+
class: reblogLinkClass,
189+
click: onReblogLinkClick,
190+
href: `/reblog/${blogName}/${idString}/${reblogKey}`
191+
}, [
192+
buildStyle(`${reblogMenuPortalSelector}:has([aria-labelledby="${reblogButton.id}"]) { display: none; }`),
193+
reblogButton.firstElementChild.cloneNode(true)]
194+
);
195+
196+
reblogButton.before(reblogLink);
197+
});
198+
199+
const restoreReblogButtons = () => {
200+
pageModifications.unregister(processReblogButtons);
201+
$(`.${reblogLinkClass}`).remove();
202+
};
203+
204+
export const onStorageChanged = async function (changes) {
205+
const { 'classic_footer.preferences.noReblogMenu': noReblogMenuChanges } = changes;
206+
if (noReblogMenuChanges && noReblogMenuChanges.oldValue === undefined) return;
207+
208+
const { newValue: noReblogMenu } = noReblogMenuChanges;
209+
noReblogMenu
210+
? pageModifications.register(reblogButtonSelector, processReblogButtons)
211+
: restoreReblogButtons();
212+
};
213+
96214
export const main = async function () {
97215
pageModifications.register(`${postOrRadarSelector} article`, processPosts);
216+
217+
const { noReblogMenu } = await getPreferences('classic_footer');
218+
if (noReblogMenu) pageModifications.register(reblogButtonSelector, processReblogButtons);
98219
};
99220

100221
export const clean = async function () {
101222
pageModifications.unregister(processPosts);
102223
$(`.${noteCountClass}`).remove();
224+
225+
restoreReblogButtons();
103226
};

src/features/quick_reblog/index.css

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -195,22 +195,8 @@ footer:is(.published, .queue, .draft) :is(button:not([role]), a[href^="/reblog/"
195195
position: relative;
196196
}
197197

198-
/* Cut-out for tick in 2025 post footer, "split notes count" variant */
199-
footer:is(.published, .queue, .draft) a[href^="/reblog/"] svg:has(use[href="#managed-icon__ds-reblog-24"]) {
200-
-webkit-mask-image: radial-gradient(
201-
calc(14.7px / 2) calc(18px / 2) at bottom 5px right 4px,
202-
transparent 99%,
203-
white 100%
204-
);
205-
mask-image: radial-gradient(
206-
calc(14.7px / 2) calc(18px / 2) at bottom 5px right 4px,
207-
transparent 99%,
208-
white 100%
209-
);
210-
}
211-
212-
/* Cut-out for tick in 2025 post footer, "single action" variant */
213-
footer:is(.published, .queue, .draft) button svg:has(use[href="#managed-icon__ds-reblog-24"]) {
198+
/* Cut-out for tick in 2025 post footer */
199+
footer:is(.published, .queue, .draft) :is(a[href^="/reblog/"], button) svg:has(use[href="#managed-icon__ds-reblog-24"]) {
214200
-webkit-mask-image: radial-gradient(
215201
calc(14.7px / 2) calc(18px / 2) at bottom 4px left 21px,
216202
transparent 99%,
@@ -246,14 +232,8 @@ footer:is(.published, .queue, .draft) a[href^="/reblog/"]:has(use[href="#managed
246232
right: -5px;
247233
}
248234

249-
/* 2025 post footer, "split notes count" variant */
250-
footer:is(.published, .queue, .draft) a[href^="/reblog/"]:has(use[href="#managed-icon__ds-reblog-24"])::after {
251-
bottom: 4px;
252-
right: 5px;
253-
}
254-
255-
/* 2025 post footer, "single action" variant */
256-
footer:is(.published, .queue, .draft) button:has(use[href="#managed-icon__ds-reblog-24"])::after {
235+
/* 2025 post footer */
236+
footer:is(.published, .queue, .draft) :is(a[href^="/reblog/"], button):has(use[href="#managed-icon__ds-reblog-24"])::after {
257237
bottom: 4px;
258238
left: 21px;
259239
}

0 commit comments

Comments
 (0)