11import { keyToCss } from '../../utils/css_map.js' ;
2- import { dom } from '../../utils/dom.js' ;
2+ import { a , button , span } from '../../utils/dom.js' ;
33import { buildStyle , postSelector } from '../../utils/interface.js' ;
44import { pageModifications } from '../../utils/mutations.js' ;
5+ import { getPreferences } from '../../utils/preferences.js' ;
56import { timelineObject } from '../../utils/react_props.js' ;
67
78const noteCountClass = 'xkit-classic-footer-note-count' ;
9+ const reblogLinkClass = 'xkit-classic-footer-reblog-link' ;
810
911const postOrRadarSelector = `:is(${ postSelector } , aside ${ keyToCss ( 'radar' ) } )` ;
1012const postOwnerControlsSelector = `${ postOrRadarSelector } ${ keyToCss ( 'postOwnerControls' ) } ` ;
1113const footerContentSelector = `${ postOrRadarSelector } article footer ${ keyToCss ( 'footerContent' ) } ` ;
1214const engagementControlsSelector = `${ footerContentSelector } ${ keyToCss ( 'engagementControls' ) } ` ;
1315const 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);"]' ;
1418const 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
1621const locale = document . documentElement . lang ;
1722const 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
73118const 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+
96214export 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
100221export const clean = async function ( ) {
101222 pageModifications . unregister ( processPosts ) ;
102223 $ ( `.${ noteCountClass } ` ) . remove ( ) ;
224+
225+ restoreReblogButtons ( ) ;
103226} ;
0 commit comments