From a7cecbc6c72130cba88425faa9d0fb7ad1ac0726 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 16 Feb 2025 12:59:35 -0800 Subject: [PATCH 01/21] implement meatball button --- src/features/quote_replies.js | 37 ++++++++++++++++++++++++++- src/main_world/test_parent_element.js | 13 ++++++++++ src/main_world/unbury_note_props.js | 16 ++++++++++++ src/utils/meatballs.js | 37 +++++++++++++++++++++++++-- src/utils/react_props.js | 9 +++++++ 5 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 src/main_world/test_parent_element.js create mode 100644 src/main_world/unbury_note_props.js diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index ae04031015..c663e2670d 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -8,10 +8,12 @@ import { notify } from '../utils/notifications.js'; import { getPreferences } from '../utils/preferences.js'; import { buildSvg } from '../utils/remixicon.js'; import { apiFetch, navigate } from '../utils/tumblr_helpers.js'; -import { userBlogs } from '../utils/user.js'; +import { userBlogNames, userBlogs } from '../utils/user.js'; +import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../utils/meatballs.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; +const meatballButtonId = 'quote_replies'; const dropdownButtonClass = 'xkit-quote-replies-dropdown'; const originalPostTagStorageKey = 'quick_tags.preferences.originalPostTag'; @@ -107,12 +109,43 @@ const quoteReply = async (tumblelogName, notificationProps) => { } }; +const processNoteProps = ([noteProps, parentNoteProps]) => { + if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { + return false; + } + if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { + return 'reply_to_comment'; + } + if (userBlogNames.includes(noteProps.blog.name)) { + return 'reply'; + } + if (noteProps.note.content.some(({ formatting }) => formatting?.some(({ type, blog }) => type === 'mention' && userBlogNames.includes(blog.name)))) { + return 'note_mention'; + } + return false; +}; + +const meatballButtonLabel = notePropsObjects => { + const mode = processNoteProps(notePropsObjects); + + return `Quote this reply (mode: ${mode})`; +}; + +const onMeatballButtonClicked = () => {}; + export const main = async function () { ({ [originalPostTagStorageKey]: originalPostTag } = await browser.storage.local.get(originalPostTagStorageKey)); ({ tagReplyingBlog, newTab } = await getPreferences('quote_replies')); pageModifications.register(notificationSelector, processNotifications); + registerReplyMeatballItem({ + id: meatballButtonId, + label: meatballButtonLabel, + notePropsFilter: notePropsObjects => console.log('notePropsObjects', notePropsObjects) && Boolean(processNoteProps(notePropsObjects)), + onclick: onMeatballButtonClicked + }); + const { [storageKey]: draftLocation } = await browser.storage.local.get(storageKey); browser.storage.local.remove(storageKey); @@ -123,6 +156,8 @@ export const main = async function () { export const clean = async function () { pageModifications.unregister(processNotifications); + unregisterReplyMeatballItem(meatballButtonId); + $(`.${buttonClass}`).remove(); }; diff --git a/src/main_world/test_parent_element.js b/src/main_world/test_parent_element.js new file mode 100644 index 0000000000..f88a2a47eb --- /dev/null +++ b/src/main_world/test_parent_element.js @@ -0,0 +1,13 @@ +export default function testParentElement (selector) { + const menuElement = this; + const reactKey = Object.keys(menuElement).find(key => key.startsWith('__reactFiber')); + let fiber = menuElement[reactKey]; + + while (fiber !== null) { + if (fiber.stateNode?.matches?.(selector)) { + return true; + } else { + fiber = fiber.return; + } + } +} diff --git a/src/main_world/unbury_note_props.js b/src/main_world/unbury_note_props.js new file mode 100644 index 0000000000..1ec6a28a03 --- /dev/null +++ b/src/main_world/unbury_note_props.js @@ -0,0 +1,16 @@ +export default function unburyNoteProps () { + const noteElement = this; + const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); + let fiber = noteElement[reactKey]; + + const results = {}; + while (fiber !== null) { + const props = fiber.memoizedProps || {}; + if (typeof props?.note?.replyId === 'string') { + // returns the last set of props corresponding to each replyId, which contains the most information + results[props.note.replyId] = props; + } + fiber = fiber.return; + } + return Object.values(results); +} diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index d7b0940d35..407cf51fb3 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -3,14 +3,15 @@ import { dom } from './dom.js'; import { postSelector } from './interface.js'; import { pageModifications } from './mutations.js'; import { inject } from './inject.js'; -import { blogData, timelineObject } from './react_props.js'; +import { blogData, notePropsObjects, timelineObject } from './react_props.js'; const postHeaderSelector = `${postSelector} article > header`; const blogHeaderSelector = `[style*="--blog-title-color"] > div > div > header, ${keyToCss('blogCardHeaderBar')}`; const meatballItems = { post: {}, - blog: {} + blog: {}, + reply: {} }; /** @@ -49,6 +50,24 @@ export const unregisterBlogMeatballItem = id => { $(`[data-xkit-blog-meatball-button="${id}"]`).remove(); }; +/** + * Add a custom button to post replies' meatball menus. + * @param {object} options - Destructured + * @param {string} options.id - Identifier for this button (must be unique) + * @param {string|Function} options.label - Button text to display. May be a function accepting the note component props data of the reply element being actioned on. + * @param {Function} options.onClick - Button click listener function + * @param {Function} [options.notePropsFilter] - Filter function, called with the note component props data of the reply element being actioned on. Must return true for button to be added. + */ +export const registerReplyMeatballItem = function ({ id, label, onClick, notePropsFilter }) { + meatballItems.reply[id] = { label, onClick, filter: notePropsFilter }; + pageModifications.trigger(addMeatballItems); +}; + +export const unregisterReplyMeatballItem = id => { + delete meatballItems.reply[id]; + $(`[data-xkit-reply-meatball-button="${id}"]`).remove(); +}; + const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMenu => { const inPostHeader = await inject('/main_world/test_header_element.js', [postHeaderSelector], meatballMenu); if (inPostHeader) { @@ -68,6 +87,20 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe reactData: await blogData(meatballMenu), reactDataKey: '__blogData' }); + return; + } + const inPostFooter = await inject('/main_world/test_parent_element.js', ['footer *'], meatballMenu); + if (inPostFooter) { + const __notePropsData = await notePropsObjects(meatballMenu); + + if (__notePropsData[0]?.note?.type === 'reply') { + addTypedMeatballItems({ + meatballMenu, + type: 'reply', + reactData: __notePropsData, + reactDataKey: '__notePropsData' + }); + } } }); diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 6a0a952de0..5befbd7644 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -18,6 +18,15 @@ export const notificationObject = weakMemoize(notificationElement => inject('/main_world/unbury_notification.js', [], notificationElement) ); +/** + * @param {Element} noteElement - An on-screen post note element + * @returns {Promise} - An array containing the element's buried note component props and, if it is a + * threaded reply, its parents' buried note component props values + */ +export const notePropsObjects = weakMemoize(noteElement => + inject('/main_world/unbury_note_props.js', [], noteElement) +); + /** * @param {Element} meatballMenu - An on-screen meatball menu element in a blog modal header or blog card * @returns {Promise} - The post's buried blog or blogSettings property. Some blog data fields, such as "followed," are not available in blog cards. From ae0d55003b94ae2ed9bcb097597f0cbd4837903a Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 6 Jan 2025 02:52:07 -0800 Subject: [PATCH 02/21] meatballs: fix onclick prop capitalization --- src/utils/meatballs.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index 407cf51fb3..8ffcb6fc48 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -55,11 +55,11 @@ export const unregisterBlogMeatballItem = id => { * @param {object} options - Destructured * @param {string} options.id - Identifier for this button (must be unique) * @param {string|Function} options.label - Button text to display. May be a function accepting the note component props data of the reply element being actioned on. - * @param {Function} options.onClick - Button click listener function + * @param {Function} options.onclick - Button click listener function * @param {Function} [options.notePropsFilter] - Filter function, called with the note component props data of the reply element being actioned on. Must return true for button to be added. */ -export const registerReplyMeatballItem = function ({ id, label, onClick, notePropsFilter }) { - meatballItems.reply[id] = { label, onClick, filter: notePropsFilter }; +export const registerReplyMeatballItem = function ({ id, label, onclick, notePropsFilter }) { + meatballItems.reply[id] = { label, onclick, filter: notePropsFilter }; pageModifications.trigger(addMeatballItems); }; From 1511fa683f687da2c57092638ef0b8fb567c42c0 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 6 Jan 2025 02:53:21 -0800 Subject: [PATCH 03/21] implement functionality --- src/features/quote_replies.js | 64 +++++++++++++++++++++++++++-------- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index c663e2670d..fbdf89f48a 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -10,6 +10,7 @@ import { buildSvg } from '../utils/remixicon.js'; import { apiFetch, navigate } from '../utils/tumblr_helpers.js'; import { userBlogNames, userBlogs } from '../utils/user.js'; import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../utils/meatballs.js'; +import { timelineObject } from '../utils/react_props.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; @@ -59,7 +60,7 @@ const processNotifications = notifications => notifications.forEach(async notifi const quoteReply = async (tumblelogName, notificationProps) => { const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; - const { type, targetPostId, targetPostSummary, targetTumblelogName, targetTumblelogUuid, timestamp } = notificationProps; + const { type, targetPostId, targetPostSummary, postUrl, targetTumblelogUuid, timestamp } = notificationProps; const { response } = await apiFetch( `/v2/blog/${targetTumblelogUuid}/post/${targetPostId}/notes/timeline`, @@ -71,15 +72,22 @@ const quoteReply = async (tumblelogName, notificationProps) => { if (!reply) throw new Error('No replies found on target post.'); if (Math.floor(reply.timestamp) !== timestamp) throw new Error('Reply not found.'); + const replyingBlogName = reply.blog.name; + const replyingBlogUuid = reply.blog.uuid; + + openQuoteReplyPost({ type, replyingBlogName, replyingBlogUuid, reply, postSummary: targetPostSummary, postUrl, targetBlogUuid: uuid, targetBlogName: tumblelogName }); +}; + +const openQuoteReplyPost = async ({ type, replyingBlogName, replyingBlogUuid, postSummary, postUrl, reply, targetBlogUuid, targetBlogName }) => { const verbiage = { reply: 'replied to your post', reply_to_comment: 'replied to you in a post', note_mention: 'mentioned you on a post' }[type]; - const text = `@${reply.blog.name} ${verbiage} \u201C${targetPostSummary.replace(/\n/g, ' ')}\u201D:`; + const text = `@${replyingBlogName} ${verbiage} \u201C${postSummary.replace(/\n/g, ' ')}\u201D:`; const formatting = [ - { start: 0, end: reply.blog.name.length + 1, type: 'mention', blog: { uuid: reply.blog.uuid } }, - { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: `https://${targetTumblelogName}.tumblr.com/post/${targetPostId}` } + { start: 0, end: replyingBlogName.length + 1, type: 'mention', blog: { uuid: replyingBlogUuid } }, + { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: postUrl } ]; const content = [ @@ -89,17 +97,17 @@ const quoteReply = async (tumblelogName, notificationProps) => { ]; const tags = [ ...originalPostTag ? [originalPostTag] : [], - ...tagReplyingBlog ? [reply.blog.name] : [] + ...tagReplyingBlog ? [replyingBlogName] : [] ].join(','); - const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); + const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${targetBlogUuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); - const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`; + const currentDraftLocation = `/edit/${targetBlogName}/${responseId}`; if (newTab) { await browser.storage.local.set({ [storageKey]: currentDraftLocation }); - const openedTab = window.open(`/blog/${tumblelogName}/drafts`); + const openedTab = window.open(`/blog/${targetBlogName}/drafts`); if (openedTab === null) { browser.storage.local.remove(storageKey); notify(displayText); @@ -114,13 +122,26 @@ const processNoteProps = ([noteProps, parentNoteProps]) => { return false; } if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { - return 'reply_to_comment'; + return { + type: 'reply_to_comment', + targetBlogName: parentNoteProps.note.blogName + }; } if (userBlogNames.includes(noteProps.blog.name)) { - return 'reply'; + return { + type: 'reply', + targetBlogName: noteProps.blog.name + }; } - if (noteProps.note.content.some(({ formatting }) => formatting?.some(({ type, blog }) => type === 'mention' && userBlogNames.includes(blog.name)))) { - return 'note_mention'; + for (const { formatting } of noteProps.note.content) { + for (const { type, blog } of formatting ?? []) { + if (type === 'mention' && userBlogNames.includes(blog.name)) { + return { + type: 'note_mention', + targetBlogName: blog.name + }; + } + } } return false; }; @@ -131,7 +152,20 @@ const meatballButtonLabel = notePropsObjects => { return `Quote this reply (mode: ${mode})`; }; -const onMeatballButtonClicked = () => {}; +const onMeatballButtonClicked = async ({ currentTarget }) => { + const [{ note: reply }] = currentTarget.__notePropsData; + + const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); + const targetBlogUuid = userBlogs.find(({ name }) => name === targetBlogName).uuid; + + const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); + + const replyingBlogName = reply.blogName; + const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) + .then(({ response: { blog: { uuid } } }) => uuid); + + openQuoteReplyPost({ type, replyingBlogName, replyingBlogUuid, reply, postSummary, postUrl, targetBlogUuid, targetBlogName }); +}; export const main = async function () { ({ [originalPostTagStorageKey]: originalPostTag } = await browser.storage.local.get(originalPostTagStorageKey)); @@ -142,8 +176,8 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, label: meatballButtonLabel, - notePropsFilter: notePropsObjects => console.log('notePropsObjects', notePropsObjects) && Boolean(processNoteProps(notePropsObjects)), - onclick: onMeatballButtonClicked + notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), + onclick: event => onMeatballButtonClicked(event).catch(showErrorModal) }); const { [storageKey]: draftLocation } = await browser.storage.local.get(storageKey); From 56e824afdc139fe394c0654f6bf3193b30049313 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 6 Jan 2025 02:54:08 -0800 Subject: [PATCH 04/21] remove debug label --- src/features/quote_replies.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index fbdf89f48a..d15a59e1c5 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -146,12 +146,6 @@ const processNoteProps = ([noteProps, parentNoteProps]) => { return false; }; -const meatballButtonLabel = notePropsObjects => { - const mode = processNoteProps(notePropsObjects); - - return `Quote this reply (mode: ${mode})`; -}; - const onMeatballButtonClicked = async ({ currentTarget }) => { const [{ note: reply }] = currentTarget.__notePropsData; @@ -175,7 +169,7 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, - label: meatballButtonLabel, + label: 'Quote this reply', notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), onclick: event => onMeatballButtonClicked(event).catch(showErrorModal) }); From 551bf9e7ae945e13e75aed354e3a9f92fb5a68e4 Mon Sep 17 00:00:00 2001 From: Marcus Date: Mon, 27 Jan 2025 06:49:20 -0800 Subject: [PATCH 05/21] misc cleanup --- src/features/quote_replies.js | 20 +++++++++++++------- src/main_world/test_parent_element.js | 6 +++--- src/main_world/unbury_note_props.js | 9 +++++---- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index d15a59e1c5..9984a6235e 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -14,7 +14,7 @@ import { timelineObject } from '../utils/react_props.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; -const meatballButtonId = 'quote_replies'; +const meatballButtonId = 'quote-replies'; const dropdownButtonClass = 'xkit-quote-replies-dropdown'; const originalPostTagStorageKey = 'quick_tags.preferences.originalPostTag'; @@ -72,10 +72,16 @@ const quoteReply = async (tumblelogName, notificationProps) => { if (!reply) throw new Error('No replies found on target post.'); if (Math.floor(reply.timestamp) !== timestamp) throw new Error('Reply not found.'); - const replyingBlogName = reply.blog.name; - const replyingBlogUuid = reply.blog.uuid; - - openQuoteReplyPost({ type, replyingBlogName, replyingBlogUuid, reply, postSummary: targetPostSummary, postUrl, targetBlogUuid: uuid, targetBlogName: tumblelogName }); + openQuoteReplyPost({ + type, + replyingBlogName: reply.blog.name, + replyingBlogUuid: reply.blog.uuid, + reply, + postSummary: targetPostSummary, + postUrl, + targetBlogUuid: uuid, + targetBlogName: tumblelogName + }); }; const openQuoteReplyPost = async ({ type, replyingBlogName, replyingBlogUuid, postSummary, postUrl, reply, targetBlogUuid, targetBlogName }) => { @@ -133,8 +139,8 @@ const processNoteProps = ([noteProps, parentNoteProps]) => { targetBlogName: noteProps.blog.name }; } - for (const { formatting } of noteProps.note.content) { - for (const { type, blog } of formatting ?? []) { + for (const { formatting = [] } of noteProps.note.content) { + for (const { type, blog } of formatting) { if (type === 'mention' && userBlogNames.includes(blog.name)) { return { type: 'note_mention', diff --git a/src/main_world/test_parent_element.js b/src/main_world/test_parent_element.js index f88a2a47eb..cd4fbbe39a 100644 --- a/src/main_world/test_parent_element.js +++ b/src/main_world/test_parent_element.js @@ -1,7 +1,7 @@ export default function testParentElement (selector) { - const menuElement = this; - const reactKey = Object.keys(menuElement).find(key => key.startsWith('__reactFiber')); - let fiber = menuElement[reactKey]; + const element = this; + const reactKey = Object.keys(element).find(key => key.startsWith('__reactFiber')); + let fiber = element[reactKey]; while (fiber !== null) { if (fiber.stateNode?.matches?.(selector)) { diff --git a/src/main_world/unbury_note_props.js b/src/main_world/unbury_note_props.js index 1ec6a28a03..13bd56c68b 100644 --- a/src/main_world/unbury_note_props.js +++ b/src/main_world/unbury_note_props.js @@ -3,14 +3,15 @@ export default function unburyNoteProps () { const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); let fiber = noteElement[reactKey]; - const results = {}; + const resultsByReplyId = {}; while (fiber !== null) { const props = fiber.memoizedProps || {}; if (typeof props?.note?.replyId === 'string') { - // returns the last set of props corresponding to each replyId, which contains the most information - results[props.note.replyId] = props; + // multiple sets of props correspond to each replyId; + // prefer the last set, as it contains the most information + resultsByReplyId[props.note.replyId] = props; } fiber = fiber.return; } - return Object.values(results); + return Object.values(resultsByReplyId); } From 6085540474d01dba680b12ae96edbaa8069697bd Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 27 Apr 2025 23:15:33 -0700 Subject: [PATCH 06/21] temporarily revert quote replies.js changes --- src/features/quote_replies.js | 87 ++++------------------------------- 1 file changed, 9 insertions(+), 78 deletions(-) diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index 9984a6235e..ae04031015 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -8,13 +8,10 @@ import { notify } from '../utils/notifications.js'; import { getPreferences } from '../utils/preferences.js'; import { buildSvg } from '../utils/remixicon.js'; import { apiFetch, navigate } from '../utils/tumblr_helpers.js'; -import { userBlogNames, userBlogs } from '../utils/user.js'; -import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../utils/meatballs.js'; -import { timelineObject } from '../utils/react_props.js'; +import { userBlogs } from '../utils/user.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; -const meatballButtonId = 'quote-replies'; const dropdownButtonClass = 'xkit-quote-replies-dropdown'; const originalPostTagStorageKey = 'quick_tags.preferences.originalPostTag'; @@ -60,7 +57,7 @@ const processNotifications = notifications => notifications.forEach(async notifi const quoteReply = async (tumblelogName, notificationProps) => { const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; - const { type, targetPostId, targetPostSummary, postUrl, targetTumblelogUuid, timestamp } = notificationProps; + const { type, targetPostId, targetPostSummary, targetTumblelogName, targetTumblelogUuid, timestamp } = notificationProps; const { response } = await apiFetch( `/v2/blog/${targetTumblelogUuid}/post/${targetPostId}/notes/timeline`, @@ -72,28 +69,15 @@ const quoteReply = async (tumblelogName, notificationProps) => { if (!reply) throw new Error('No replies found on target post.'); if (Math.floor(reply.timestamp) !== timestamp) throw new Error('Reply not found.'); - openQuoteReplyPost({ - type, - replyingBlogName: reply.blog.name, - replyingBlogUuid: reply.blog.uuid, - reply, - postSummary: targetPostSummary, - postUrl, - targetBlogUuid: uuid, - targetBlogName: tumblelogName - }); -}; - -const openQuoteReplyPost = async ({ type, replyingBlogName, replyingBlogUuid, postSummary, postUrl, reply, targetBlogUuid, targetBlogName }) => { const verbiage = { reply: 'replied to your post', reply_to_comment: 'replied to you in a post', note_mention: 'mentioned you on a post' }[type]; - const text = `@${replyingBlogName} ${verbiage} \u201C${postSummary.replace(/\n/g, ' ')}\u201D:`; + const text = `@${reply.blog.name} ${verbiage} \u201C${targetPostSummary.replace(/\n/g, ' ')}\u201D:`; const formatting = [ - { start: 0, end: replyingBlogName.length + 1, type: 'mention', blog: { uuid: replyingBlogUuid } }, - { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: postUrl } + { start: 0, end: reply.blog.name.length + 1, type: 'mention', blog: { uuid: reply.blog.uuid } }, + { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: `https://${targetTumblelogName}.tumblr.com/post/${targetPostId}` } ]; const content = [ @@ -103,17 +87,17 @@ const openQuoteReplyPost = async ({ type, replyingBlogName, replyingBlogUuid, po ]; const tags = [ ...originalPostTag ? [originalPostTag] : [], - ...tagReplyingBlog ? [replyingBlogName] : [] + ...tagReplyingBlog ? [reply.blog.name] : [] ].join(','); - const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${targetBlogUuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); + const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); - const currentDraftLocation = `/edit/${targetBlogName}/${responseId}`; + const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`; if (newTab) { await browser.storage.local.set({ [storageKey]: currentDraftLocation }); - const openedTab = window.open(`/blog/${targetBlogName}/drafts`); + const openedTab = window.open(`/blog/${tumblelogName}/drafts`); if (openedTab === null) { browser.storage.local.remove(storageKey); notify(displayText); @@ -123,63 +107,12 @@ const openQuoteReplyPost = async ({ type, replyingBlogName, replyingBlogUuid, po } }; -const processNoteProps = ([noteProps, parentNoteProps]) => { - if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { - return false; - } - if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { - return { - type: 'reply_to_comment', - targetBlogName: parentNoteProps.note.blogName - }; - } - if (userBlogNames.includes(noteProps.blog.name)) { - return { - type: 'reply', - targetBlogName: noteProps.blog.name - }; - } - for (const { formatting = [] } of noteProps.note.content) { - for (const { type, blog } of formatting) { - if (type === 'mention' && userBlogNames.includes(blog.name)) { - return { - type: 'note_mention', - targetBlogName: blog.name - }; - } - } - } - return false; -}; - -const onMeatballButtonClicked = async ({ currentTarget }) => { - const [{ note: reply }] = currentTarget.__notePropsData; - - const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); - const targetBlogUuid = userBlogs.find(({ name }) => name === targetBlogName).uuid; - - const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); - - const replyingBlogName = reply.blogName; - const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) - .then(({ response: { blog: { uuid } } }) => uuid); - - openQuoteReplyPost({ type, replyingBlogName, replyingBlogUuid, reply, postSummary, postUrl, targetBlogUuid, targetBlogName }); -}; - export const main = async function () { ({ [originalPostTagStorageKey]: originalPostTag } = await browser.storage.local.get(originalPostTagStorageKey)); ({ tagReplyingBlog, newTab } = await getPreferences('quote_replies')); pageModifications.register(notificationSelector, processNotifications); - registerReplyMeatballItem({ - id: meatballButtonId, - label: 'Quote this reply', - notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), - onclick: event => onMeatballButtonClicked(event).catch(showErrorModal) - }); - const { [storageKey]: draftLocation } = await browser.storage.local.get(storageKey); browser.storage.local.remove(storageKey); @@ -190,8 +123,6 @@ export const main = async function () { export const clean = async function () { pageModifications.unregister(processNotifications); - unregisterReplyMeatballItem(meatballButtonId); - $(`.${buttonClass}`).remove(); }; From 198f41ca174452aa3a953256876d2d59ef770540 Mon Sep 17 00:00:00 2001 From: Marcus Date: Sun, 27 Apr 2025 23:16:05 -0700 Subject: [PATCH 07/21] reimplement quote replies.js changes --- src/features/quote_replies.js | 90 +++++++++++++++++++++++++++++++++-- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index c9fa34f2c4..a8214386fd 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -8,10 +8,13 @@ import { notify } from '../utils/notifications.js'; import { getPreferences } from '../utils/preferences.js'; import { buildSvg } from '../utils/remixicon.js'; import { apiFetch, navigate } from '../utils/tumblr_helpers.js'; -import { userBlogs } from '../utils/user.js'; +import { userBlogNames, userBlogs } from '../utils/user.js'; +import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../utils/meatballs.js'; +import { timelineObject } from '../utils/react_props.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; +const meatballButtonId = 'quote-replies'; const dropdownButtonClass = 'xkit-quote-replies-dropdown'; export const styleElement = buildStyle(` @@ -90,7 +93,7 @@ const processNotifications = notifications => notifications.forEach(async notifi { click () { this.disabled = true; - quoteReply(tumblelogName, notificationProps) + quoteActivityReply(tumblelogName, notificationProps) .catch(showErrorModal) .finally(() => { this.disabled = false; }); } @@ -187,13 +190,81 @@ const processReply = async ({ type, timestamp, targetPostId, targetTumblelogName return { content, tags }; }; -const quoteReply = async (tumblelogName, notificationProps) => { - const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; - +const quoteActivityReply = async (tumblelogName, notificationProps) => { const { content, tags } = notificationProps.type === 'generic' ? await processGenericReply(notificationProps) : await processReply(notificationProps); + openQuoteReplyDraft(tumblelogName, content, tags); +}; + +const processNoteProps = ([noteProps, parentNoteProps]) => { + if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { + return false; + } + if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { + return { + type: 'reply_to_comment', + targetBlogName: parentNoteProps.note.blogName + }; + } + if (userBlogNames.includes(noteProps.blog.name)) { + return { + type: 'reply', + targetBlogName: noteProps.blog.name + }; + } + for (const { formatting = [] } of noteProps.note.content) { + for (const { type, blog } of formatting) { + if (type === 'mention' && userBlogNames.includes(blog.name)) { + return { + type: 'note_mention', + targetBlogName: blog.name + }; + } + } + } + return false; +}; + +const quoteNoteReply = async ({ currentTarget }) => { + const [{ note: reply }] = currentTarget.__notePropsData; + + const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); + + const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); + + const replyingBlogName = reply.blogName; + const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) + .then(({ response: { blog: { uuid } } }) => uuid); + + const verbiage = { + reply: 'replied to your post', + reply_to_comment: 'replied to you in a post', + note_mention: 'mentioned you on a post' + }[type]; + const text = `@${replyingBlogName} ${verbiage} \u201C${postSummary.replace(/\n/g, ' ')}\u201D:`; + const formatting = [ + { start: 0, end: replyingBlogName.length + 1, type: 'mention', blog: { uuid: replyingBlogUuid } }, + { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: postUrl } + ]; + + const content = [ + { type: 'text', text, formatting }, + Object.assign(reply.content[0], { subtype: 'indented' }), + { type: 'text', text: '\u200B' } + ]; + const tags = [ + ...originalPostTag ? [originalPostTag] : [], + ...tagReplyingBlog ? [replyingBlogName] : [] + ].join(','); + + openQuoteReplyDraft(targetBlogName, content, tags); +}; + +const openQuoteReplyDraft = async (tumblelogName, content, tags) => { + const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; + const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`; @@ -217,6 +288,13 @@ export const main = async function () { pageModifications.register(notificationSelector, processNotifications); + registerReplyMeatballItem({ + id: meatballButtonId, + label: 'Quote this reply', + notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), + onclick: event => quoteNoteReply(event).catch(showErrorModal) + }); + const { [storageKey]: draftLocation } = await browser.storage.local.get(storageKey); browser.storage.local.remove(storageKey); @@ -227,6 +305,8 @@ export const main = async function () { export const clean = async function () { pageModifications.unregister(processNotifications); + unregisterReplyMeatballItem(meatballButtonId); + $(`.${buttonClass}`).remove(); }; From 9c5a8999ab4d4e88eebfcf8aa27e00bdd8a73780 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Thu, 7 Aug 2025 19:46:02 -0400 Subject: [PATCH 08/21] partial refactor to api call --- src/features/quote_replies/index.js | 67 +++++++++++++++++----------- src/main_world/unbury_note_object.js | 14 ++++++ src/main_world/unbury_note_props.js | 17 ------- src/utils/meatballs.js | 10 ++--- src/utils/react_props.js | 7 ++- 5 files changed, 63 insertions(+), 52 deletions(-) create mode 100644 src/main_world/unbury_note_object.js delete mode 100644 src/main_world/unbury_note_props.js diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 9b15422c48..885a2e0f3c 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -198,39 +198,54 @@ const quoteActivityReply = async (tumblelogName, notificationProps) => { openQuoteReplyDraft(tumblelogName, content, tags); }; -const processNoteProps = ([noteProps, parentNoteProps]) => { - if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { - return false; - } - if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { - return { - type: 'reply_to_comment', - targetBlogName: parentNoteProps.note.blogName - }; - } - if (userBlogNames.includes(noteProps.blog.name)) { - return { - type: 'reply', - targetBlogName: noteProps.blog.name - }; - } - for (const { formatting = [] } of noteProps.note.content) { - for (const { type, blog } of formatting) { - if (type === 'mention' && userBlogNames.includes(blog.name)) { - return { - type: 'note_mention', - targetBlogName: blog.name - }; +const processNoteProps = async ({ replyUrl }) => { + try { + const [, blogName, postId, /* 'replies' */ , replyId] = new URL(replyUrl).pathname.split('/'); + let { response: { timeline } } = await apiFetch(`/v2/blog/${blogName}/post/${postId}/replies/${replyId}/permalink`); + const notes = []; + while (timeline) { + const note = timeline.elements[0]; + notes.unshift(note); + timeline = note.children; + } + const [note, parentNote] = notes; + + if (userBlogNames.includes(note.blogName) || noteProps.communityId) { + return false; + } + if (parentNote && userBlogNames.includes(parentNote.blogName)) { + return { + type: 'reply_to_comment', + targetBlogName: parentNote.blogName + }; + } + if (userBlogNames.includes(note.blog.name)) { + return { + type: 'reply', + targetBlogName: note.blog.name + }; + } + for (const { formatting = [] } of note.content) { + for (const { type, blog } of formatting) { + if (type === 'mention' && userBlogNames.includes(blog.name)) { + return { + type: 'note_mention', + targetBlogName: blog.name + }; + } } } + } catch (e) { + console.error('could not process note with reply url', replyUrl); + console.error(e); } return false; }; const quoteNoteReply = async ({ currentTarget }) => { - const [{ note: reply }] = currentTarget.__notePropsData; + const [{ note: reply }] = currentTarget.__noteObjectData; - const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); + const { type, targetBlogName } = await processNoteProps(currentTarget.__noteObjectData); const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); @@ -291,7 +306,7 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, label: 'Quote this reply', - notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), + notePropsFilter: async notePropsData => Boolean(await processNoteProps(notePropsData)), onclick: event => quoteNoteReply(event).catch(showErrorModal) }); diff --git a/src/main_world/unbury_note_object.js b/src/main_world/unbury_note_object.js new file mode 100644 index 0000000000..cafc2ab663 --- /dev/null +++ b/src/main_world/unbury_note_object.js @@ -0,0 +1,14 @@ +export default function unburyNoteObject () { + const noteElement = this; + const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); + let fiber = noteElement[reactKey]; + + while (fiber !== null) { + const { note } = fiber.memoizedProps || {}; + if (note !== undefined) { + return note; + } else { + fiber = fiber.return; + } + } +} diff --git a/src/main_world/unbury_note_props.js b/src/main_world/unbury_note_props.js deleted file mode 100644 index 13bd56c68b..0000000000 --- a/src/main_world/unbury_note_props.js +++ /dev/null @@ -1,17 +0,0 @@ -export default function unburyNoteProps () { - const noteElement = this; - const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); - let fiber = noteElement[reactKey]; - - const resultsByReplyId = {}; - while (fiber !== null) { - const props = fiber.memoizedProps || {}; - if (typeof props?.note?.replyId === 'string') { - // multiple sets of props correspond to each replyId; - // prefer the last set, as it contains the most information - resultsByReplyId[props.note.replyId] = props; - } - fiber = fiber.return; - } - return Object.values(resultsByReplyId); -} diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index 8ffcb6fc48..c8ec4107b3 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -3,7 +3,7 @@ import { dom } from './dom.js'; import { postSelector } from './interface.js'; import { pageModifications } from './mutations.js'; import { inject } from './inject.js'; -import { blogData, notePropsObjects, timelineObject } from './react_props.js'; +import { blogData, noteObject, timelineObject } from './react_props.js'; const postHeaderSelector = `${postSelector} article > header`; const blogHeaderSelector = `[style*="--blog-title-color"] > div > div > header, ${keyToCss('blogCardHeaderBar')}`; @@ -91,14 +91,14 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe } const inPostFooter = await inject('/main_world/test_parent_element.js', ['footer *'], meatballMenu); if (inPostFooter) { - const __notePropsData = await notePropsObjects(meatballMenu); + const __noteObjectData = await noteObject(meatballMenu); - if (__notePropsData[0]?.note?.type === 'reply') { + if (__noteObjectData?.type === 'reply') { addTypedMeatballItems({ meatballMenu, type: 'reply', - reactData: __notePropsData, - reactDataKey: '__notePropsData' + reactData: __noteObjectData, + reactDataKey: '__noteObjectData' }); } } diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 5befbd7644..4af169e702 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -20,11 +20,10 @@ export const notificationObject = weakMemoize(notificationElement => /** * @param {Element} noteElement - An on-screen post note element - * @returns {Promise} - An array containing the element's buried note component props and, if it is a - * threaded reply, its parents' buried note component props values + * @returns {Promise} - The element's buried note property */ -export const notePropsObjects = weakMemoize(noteElement => - inject('/main_world/unbury_note_props.js', [], noteElement) +export const noteObject = weakMemoize(noteElement => + inject('/main_world/unbury_note_object.js', [], noteElement) ); /** From 887a4d9844a0e5e5c3acd6601c8400aace3ff0dd Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Thu, 7 Aug 2025 20:15:17 -0400 Subject: [PATCH 09/21] Revert "partial refactor to api call" This reverts commit 9c5a8999ab4d4e88eebfcf8aa27e00bdd8a73780. --- src/features/quote_replies/index.js | 67 +++++++++++----------------- src/main_world/unbury_note_object.js | 14 ------ src/main_world/unbury_note_props.js | 17 +++++++ src/utils/meatballs.js | 10 ++--- src/utils/react_props.js | 7 +-- 5 files changed, 52 insertions(+), 63 deletions(-) delete mode 100644 src/main_world/unbury_note_object.js create mode 100644 src/main_world/unbury_note_props.js diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 885a2e0f3c..9b15422c48 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -198,54 +198,39 @@ const quoteActivityReply = async (tumblelogName, notificationProps) => { openQuoteReplyDraft(tumblelogName, content, tags); }; -const processNoteProps = async ({ replyUrl }) => { - try { - const [, blogName, postId, /* 'replies' */ , replyId] = new URL(replyUrl).pathname.split('/'); - let { response: { timeline } } = await apiFetch(`/v2/blog/${blogName}/post/${postId}/replies/${replyId}/permalink`); - const notes = []; - while (timeline) { - const note = timeline.elements[0]; - notes.unshift(note); - timeline = note.children; - } - const [note, parentNote] = notes; - - if (userBlogNames.includes(note.blogName) || noteProps.communityId) { - return false; - } - if (parentNote && userBlogNames.includes(parentNote.blogName)) { - return { - type: 'reply_to_comment', - targetBlogName: parentNote.blogName - }; - } - if (userBlogNames.includes(note.blog.name)) { - return { - type: 'reply', - targetBlogName: note.blog.name - }; - } - for (const { formatting = [] } of note.content) { - for (const { type, blog } of formatting) { - if (type === 'mention' && userBlogNames.includes(blog.name)) { - return { - type: 'note_mention', - targetBlogName: blog.name - }; - } +const processNoteProps = ([noteProps, parentNoteProps]) => { + if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { + return false; + } + if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { + return { + type: 'reply_to_comment', + targetBlogName: parentNoteProps.note.blogName + }; + } + if (userBlogNames.includes(noteProps.blog.name)) { + return { + type: 'reply', + targetBlogName: noteProps.blog.name + }; + } + for (const { formatting = [] } of noteProps.note.content) { + for (const { type, blog } of formatting) { + if (type === 'mention' && userBlogNames.includes(blog.name)) { + return { + type: 'note_mention', + targetBlogName: blog.name + }; } } - } catch (e) { - console.error('could not process note with reply url', replyUrl); - console.error(e); } return false; }; const quoteNoteReply = async ({ currentTarget }) => { - const [{ note: reply }] = currentTarget.__noteObjectData; + const [{ note: reply }] = currentTarget.__notePropsData; - const { type, targetBlogName } = await processNoteProps(currentTarget.__noteObjectData); + const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); @@ -306,7 +291,7 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, label: 'Quote this reply', - notePropsFilter: async notePropsData => Boolean(await processNoteProps(notePropsData)), + notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), onclick: event => quoteNoteReply(event).catch(showErrorModal) }); diff --git a/src/main_world/unbury_note_object.js b/src/main_world/unbury_note_object.js deleted file mode 100644 index cafc2ab663..0000000000 --- a/src/main_world/unbury_note_object.js +++ /dev/null @@ -1,14 +0,0 @@ -export default function unburyNoteObject () { - const noteElement = this; - const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); - let fiber = noteElement[reactKey]; - - while (fiber !== null) { - const { note } = fiber.memoizedProps || {}; - if (note !== undefined) { - return note; - } else { - fiber = fiber.return; - } - } -} diff --git a/src/main_world/unbury_note_props.js b/src/main_world/unbury_note_props.js new file mode 100644 index 0000000000..13bd56c68b --- /dev/null +++ b/src/main_world/unbury_note_props.js @@ -0,0 +1,17 @@ +export default function unburyNoteProps () { + const noteElement = this; + const reactKey = Object.keys(noteElement).find(key => key.startsWith('__reactFiber')); + let fiber = noteElement[reactKey]; + + const resultsByReplyId = {}; + while (fiber !== null) { + const props = fiber.memoizedProps || {}; + if (typeof props?.note?.replyId === 'string') { + // multiple sets of props correspond to each replyId; + // prefer the last set, as it contains the most information + resultsByReplyId[props.note.replyId] = props; + } + fiber = fiber.return; + } + return Object.values(resultsByReplyId); +} diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index c8ec4107b3..8ffcb6fc48 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -3,7 +3,7 @@ import { dom } from './dom.js'; import { postSelector } from './interface.js'; import { pageModifications } from './mutations.js'; import { inject } from './inject.js'; -import { blogData, noteObject, timelineObject } from './react_props.js'; +import { blogData, notePropsObjects, timelineObject } from './react_props.js'; const postHeaderSelector = `${postSelector} article > header`; const blogHeaderSelector = `[style*="--blog-title-color"] > div > div > header, ${keyToCss('blogCardHeaderBar')}`; @@ -91,14 +91,14 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe } const inPostFooter = await inject('/main_world/test_parent_element.js', ['footer *'], meatballMenu); if (inPostFooter) { - const __noteObjectData = await noteObject(meatballMenu); + const __notePropsData = await notePropsObjects(meatballMenu); - if (__noteObjectData?.type === 'reply') { + if (__notePropsData[0]?.note?.type === 'reply') { addTypedMeatballItems({ meatballMenu, type: 'reply', - reactData: __noteObjectData, - reactDataKey: '__noteObjectData' + reactData: __notePropsData, + reactDataKey: '__notePropsData' }); } } diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 4af169e702..5befbd7644 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -20,10 +20,11 @@ export const notificationObject = weakMemoize(notificationElement => /** * @param {Element} noteElement - An on-screen post note element - * @returns {Promise} - The element's buried note property + * @returns {Promise} - An array containing the element's buried note component props and, if it is a + * threaded reply, its parents' buried note component props values */ -export const noteObject = weakMemoize(noteElement => - inject('/main_world/unbury_note_object.js', [], noteElement) +export const notePropsObjects = weakMemoize(noteElement => + inject('/main_world/unbury_note_props.js', [], noteElement) ); /** From 2c16d8cca535d297c3284823dc3305f03142a4d5 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Thu, 7 Aug 2025 21:11:17 -0400 Subject: [PATCH 10/21] refactor: name parent note props explictly --- src/features/quote_replies/index.js | 6 +++--- src/main_world/unbury_note_props.js | 3 ++- src/utils/meatballs.js | 2 +- src/utils/react_props.js | 8 +++++++- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 9b15422c48..8928a767f4 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -198,7 +198,7 @@ const quoteActivityReply = async (tumblelogName, notificationProps) => { openQuoteReplyDraft(tumblelogName, content, tags); }; -const processNoteProps = ([noteProps, parentNoteProps]) => { +const processNotePropsData = ({ noteProps, parentNoteProps }) => { if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { return false; } @@ -230,7 +230,7 @@ const processNoteProps = ([noteProps, parentNoteProps]) => { const quoteNoteReply = async ({ currentTarget }) => { const [{ note: reply }] = currentTarget.__notePropsData; - const { type, targetBlogName } = processNoteProps(currentTarget.__notePropsData); + const { type, targetBlogName } = processNotePropsData(currentTarget.__notePropsData); const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); @@ -291,7 +291,7 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, label: 'Quote this reply', - notePropsFilter: notePropsData => Boolean(processNoteProps(notePropsData)), + notePropsFilter: notePropsData => Boolean(processNotePropsData(notePropsData)), onclick: event => quoteNoteReply(event).catch(showErrorModal) }); diff --git a/src/main_world/unbury_note_props.js b/src/main_world/unbury_note_props.js index 13bd56c68b..759b973cdc 100644 --- a/src/main_world/unbury_note_props.js +++ b/src/main_world/unbury_note_props.js @@ -13,5 +13,6 @@ export default function unburyNoteProps () { } fiber = fiber.return; } - return Object.values(resultsByReplyId); + const [noteProps, parentNoteProps] = Object.values(resultsByReplyId); + return { noteProps, parentNoteProps }; } diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index 8ffcb6fc48..ca7155b662 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -93,7 +93,7 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe if (inPostFooter) { const __notePropsData = await notePropsObjects(meatballMenu); - if (__notePropsData[0]?.note?.type === 'reply') { + if (__notePropsData?.noteProps?.note?.type === 'reply') { addTypedMeatballItems({ meatballMenu, type: 'reply', diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 5befbd7644..976eed1464 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -18,9 +18,15 @@ export const notificationObject = weakMemoize(notificationElement => inject('/main_world/unbury_notification.js', [], notificationElement) ); +/** + * @typedef NotePropsData + * @property {object} noteProps - A note element's buried note component props + * @property {object} [parentNoteProps] - A note element's parent reply's buried note component props, if it is a threaded reply + */ + /** * @param {Element} noteElement - An on-screen post note element - * @returns {Promise} - An array containing the element's buried note component props and, if it is a + * @returns {Promise} - An object containing the element's buried note component props and, if it is a * threaded reply, its parents' buried note component props values */ export const notePropsObjects = weakMemoize(noteElement => From 618d0450fee2e394600fe47493865a41a7449c52 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Fri, 8 Aug 2025 10:36:57 -0400 Subject: [PATCH 11/21] refactor: DRY is this really better though --- src/features/quote_replies/index.js | 49 +++++++++++------------------ 1 file changed, 19 insertions(+), 30 deletions(-) diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 8928a767f4..94ab3de33f 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -166,25 +166,32 @@ const processReply = async ({ type, timestamp, targetPostId, targetTumblelogName throw new Error('Reply not found.'); } + const { content: replyContent, blog: { name: replyingBlogName, uuid: replyingBlogUuid } } = reply; + const targetPostUrl = `https://${targetTumblelogName}.tumblr.com/post/${targetPostId}`; + + return createReplyPost({ type, replyingBlogName, replyingBlogUuid, targetPostSummary, targetPostUrl, replyContent }); +}; + +const createReplyPost = ({ type, replyingBlogName, replyingBlogUuid, targetPostSummary, targetPostUrl, replyContent }) => { const verbiage = { reply: 'replied to your post', reply_to_comment: 'replied to you in a post', note_mention: 'mentioned you on a post' }[type]; - const text = `@${reply.blog.name} ${verbiage} \u201C${targetPostSummary.replace(/\n/g, ' ')}\u201D:`; + const text = `@${replyingBlogName} ${verbiage} \u201C${targetPostSummary.replace(/\n/g, ' ')}\u201D:`; const formatting = [ - { start: 0, end: reply.blog.name.length + 1, type: 'mention', blog: { uuid: reply.blog.uuid } }, - { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: `https://${targetTumblelogName}.tumblr.com/post/${targetPostId}` } + { start: 0, end: replyingBlogName.length + 1, type: 'mention', blog: { uuid: replyingBlogUuid } }, + { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: targetPostUrl } ]; const content = [ { type: 'text', text, formatting }, - Object.assign(reply.content[0], { subtype: 'indented' }), + Object.assign(replyContent[0], { subtype: 'indented' }), { type: 'text', text: '\u200B' } ]; const tags = [ ...originalPostTag ? [originalPostTag] : [], - ...tagReplyingBlog ? [reply.blog.name] : [] + ...tagReplyingBlog ? [replyingBlogName] : [] ].join(','); return { content, tags }; @@ -228,37 +235,19 @@ const processNotePropsData = ({ noteProps, parentNoteProps }) => { }; const quoteNoteReply = async ({ currentTarget }) => { - const [{ note: reply }] = currentTarget.__notePropsData; + const { + noteProps: { + note: { blogName: replyingBlogName, content: replyContent } + } + } = currentTarget.__notePropsData; const { type, targetBlogName } = processNotePropsData(currentTarget.__notePropsData); - const { summary: postSummary, postUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); - - const replyingBlogName = reply.blogName; + const { summary: targetPostSummary, postUrl: targetPostUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) .then(({ response: { blog: { uuid } } }) => uuid); - const verbiage = { - reply: 'replied to your post', - reply_to_comment: 'replied to you in a post', - note_mention: 'mentioned you on a post' - }[type]; - const text = `@${replyingBlogName} ${verbiage} \u201C${postSummary.replace(/\n/g, ' ')}\u201D:`; - const formatting = [ - { start: 0, end: replyingBlogName.length + 1, type: 'mention', blog: { uuid: replyingBlogUuid } }, - { start: text.indexOf('\u201C'), end: text.length - 1, type: 'link', url: postUrl } - ]; - - const content = [ - { type: 'text', text, formatting }, - Object.assign(reply.content[0], { subtype: 'indented' }), - { type: 'text', text: '\u200B' } - ]; - const tags = [ - ...originalPostTag ? [originalPostTag] : [], - ...tagReplyingBlog ? [replyingBlogName] : [] - ].join(','); - + const { content, tags } = createReplyPost({ type, replyingBlogName, replyingBlogUuid, targetPostSummary, targetPostUrl, replyContent }); openQuoteReplyDraft(targetBlogName, content, tags); }; From f7990fe1c34b14d8f2c69c9578b7e695cee59f2d Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Fri, 8 Aug 2025 10:38:54 -0400 Subject: [PATCH 12/21] refactor: Renames --- src/features/quote_replies/index.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 94ab3de33f..a476f4efc3 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -102,7 +102,7 @@ const processNotifications = notifications => notifications.forEach(async notifi )); }); -const processGenericReply = async (notificationProps) => { +const createGenericActivityReplyPost = async (notificationProps) => { const { subtype: type, timestamp, @@ -120,7 +120,7 @@ const processGenericReply = async (notificationProps) => { ? bodyDescriptionContent.text.slice(summaryFormatting.start + 1, summaryFormatting.end - 1) : bodyDescriptionContent.text; - return await processReply({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary }); + return await createActivityReplyPost({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary }); } catch (exception) { console.error(exception); console.debug('[XKit] Falling back to generic quote content due to fetch/parse failure'); @@ -153,7 +153,7 @@ const processGenericReply = async (notificationProps) => { return { content, tags }; }; -const processReply = async ({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary }) => { +const createActivityReplyPost = async ({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary }) => { const { response } = await apiFetch( `/v2/blog/${targetTumblelogName}/post/${targetPostId}/notes/timeline`, { queryParams: { mode: 'replies', before_timestamp: `${timestamp + 1}000000` } } @@ -199,16 +199,16 @@ const createReplyPost = ({ type, replyingBlogName, replyingBlogUuid, targetPostS const quoteActivityReply = async (tumblelogName, notificationProps) => { const { content, tags } = notificationProps.type === 'generic' - ? await processGenericReply(notificationProps) - : await processReply(notificationProps); + ? await createGenericActivityReplyPost(notificationProps) + : await createActivityReplyPost(notificationProps); openQuoteReplyDraft(tumblelogName, content, tags); }; -const processNotePropsData = ({ noteProps, parentNoteProps }) => { - if (userBlogNames.includes(noteProps.note.blogName) || noteProps.communityId) { - return false; - } +const determineNoteReplyType = ({ noteProps, parentNoteProps }) => { + if (userBlogNames.includes(noteProps.note.blogName)) return false; + if (noteProps.communityId) return false; + if (parentNoteProps && userBlogNames.includes(parentNoteProps.note.blogName)) { return { type: 'reply_to_comment', @@ -241,7 +241,7 @@ const quoteNoteReply = async ({ currentTarget }) => { } } = currentTarget.__notePropsData; - const { type, targetBlogName } = processNotePropsData(currentTarget.__notePropsData); + const { type, targetBlogName } = determineNoteReplyType(currentTarget.__notePropsData); const { summary: targetPostSummary, postUrl: targetPostUrl } = await timelineObject(currentTarget.closest(keyToCss('meatballMenu'))); const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) @@ -280,7 +280,7 @@ export const main = async function () { registerReplyMeatballItem({ id: meatballButtonId, label: 'Quote this reply', - notePropsFilter: notePropsData => Boolean(processNotePropsData(notePropsData)), + notePropsFilter: notePropsData => Boolean(determineNoteReplyType(notePropsData)), onclick: event => quoteNoteReply(event).catch(showErrorModal) }); From 2dd72feba72167049e5923b3196214986e3413b6 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Fri, 8 Aug 2025 23:09:55 -0400 Subject: [PATCH 13/21] refactor: combine content and tags --- src/features/quote_replies/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index a476f4efc3..750fe411ca 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -198,11 +198,11 @@ const createReplyPost = ({ type, replyingBlogName, replyingBlogUuid, targetPostS }; const quoteActivityReply = async (tumblelogName, notificationProps) => { - const { content, tags } = notificationProps.type === 'generic' + const replyPost = notificationProps.type === 'generic' ? await createGenericActivityReplyPost(notificationProps) : await createActivityReplyPost(notificationProps); - openQuoteReplyDraft(tumblelogName, content, tags); + openQuoteReplyDraft(tumblelogName, replyPost); }; const determineNoteReplyType = ({ noteProps, parentNoteProps }) => { @@ -247,14 +247,14 @@ const quoteNoteReply = async ({ currentTarget }) => { const replyingBlogUuid = await apiFetch(`/v2/blog/${replyingBlogName}/info?fields[blogs]=uuid`) .then(({ response: { blog: { uuid } } }) => uuid); - const { content, tags } = createReplyPost({ type, replyingBlogName, replyingBlogUuid, targetPostSummary, targetPostUrl, replyContent }); - openQuoteReplyDraft(targetBlogName, content, tags); + const replyPost = createReplyPost({ type, replyingBlogName, replyingBlogUuid, targetPostSummary, targetPostUrl, replyContent }); + openQuoteReplyDraft(targetBlogName, replyPost); }; -const openQuoteReplyDraft = async (tumblelogName, content, tags) => { +const openQuoteReplyDraft = async (tumblelogName, replyPost) => { const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; - const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); + const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { state: 'draft', ...replyPost } }); const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`; From d075256560b4a787f91dd6b77eec6c07ccffb5d0 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Fri, 24 Oct 2025 20:53:20 -0700 Subject: [PATCH 14/21] update for new footer --- src/utils/meatballs.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index bef1838e8e..8d046d2e7a 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -89,8 +89,8 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe }); return; } - const inPostFooter = await inject('/main_world/test_parent_element.js', ['footer *'], meatballMenu); - if (inPostFooter) { + const inPostActivity = await inject('/main_world/test_parent_element.js', [`${keyToCss('postActivity')} *`], meatballMenu); + if (inPostActivity) { const __notePropsData = await notePropsObjects(meatballMenu); if (__notePropsData?.noteProps?.note?.type === 'reply') { From 48327ea4ab09ba4edf458e9e9cd089dca09b2b4b Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Mon, 3 Nov 2025 13:30:22 -0800 Subject: [PATCH 15/21] update --- src/utils/meatballs.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index f011109442..21a647bc59 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -52,11 +52,11 @@ export const unregisterBlogMeatballItem = id => { /** * Add a custom button to post replies' meatball menus. - * @param {object} options - Destructured - * @param {string} options.id - Identifier for this button (must be unique) - * @param {string|Function} options.label - Button text to display. May be a function accepting the note component props data of the reply element being actioned on. - * @param {Function} options.onclick - Button click listener function - * @param {Function} [options.notePropsFilter] - Filter function, called with the note component props data of the reply element being actioned on. Must return true for button to be added. + * @param {object} options Destructured + * @param {string} options.id Identifier for this button (must be unique) + * @param {string|Function} options.label Button text to display. May be a function accepting the note component props data of the reply element being actioned on. + * @param {Function} options.onclick Button click listener function + * @param {Function} [options.notePropsFilter] Filter function, called with the note component props data of the reply element being actioned on. Must return true for button to be added. */ export const registerReplyMeatballItem = function ({ id, label, onclick, notePropsFilter }) { meatballItems.reply[id] = { label, onclick, filter: notePropsFilter }; From 6b624dc8570a71d5480acce393632c78f1f5533d Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:10:34 -0800 Subject: [PATCH 16/21] use getClosestRenderedElement --- src/main_world/test_parent_element.js | 13 ------------- src/utils/meatballs.js | 3 +-- 2 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 src/main_world/test_parent_element.js diff --git a/src/main_world/test_parent_element.js b/src/main_world/test_parent_element.js deleted file mode 100644 index cd4fbbe39a..0000000000 --- a/src/main_world/test_parent_element.js +++ /dev/null @@ -1,13 +0,0 @@ -export default function testParentElement (selector) { - const element = this; - const reactKey = Object.keys(element).find(key => key.startsWith('__reactFiber')); - let fiber = element[reactKey]; - - while (fiber !== null) { - if (fiber.stateNode?.matches?.(selector)) { - return true; - } else { - fiber = fiber.return; - } - } -} diff --git a/src/utils/meatballs.js b/src/utils/meatballs.js index ccb975a3e5..844c9b9d3e 100644 --- a/src/utils/meatballs.js +++ b/src/utils/meatballs.js @@ -2,7 +2,6 @@ import { keyToCss } from './css_map.js'; import { dom } from './dom.js'; import { getClosestRenderedElement, postSelector } from './interface.js'; import { pageModifications } from './mutations.js'; -import { inject } from './inject.js'; import { blogData, notePropsObjects, timelineObject } from './react_props.js'; const postHeaderSelector = `${postSelector} :is(article > header, article > div > header)`; @@ -88,7 +87,7 @@ const addMeatballItems = meatballMenus => meatballMenus.forEach(async meatballMe }); return; } - const inPostActivity = await inject('/main_world/test_parent_element.js', [`${keyToCss('postActivity')} *`], meatballMenu); + const inPostActivity = Boolean(await getClosestRenderedElement(meatballMenu, `${keyToCss('postActivity')} *`)); if (inPostActivity) { const __notePropsData = await notePropsObjects(meatballMenu); From 5d48caddcf732db9bb4afb902d5dc0985ba984bd Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:12:59 -0800 Subject: [PATCH 17/21] fix commas --- dev/check-outdated-css-keys.js | 10 +++++----- src/utils/react_props.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/check-outdated-css-keys.js b/dev/check-outdated-css-keys.js index a368bebc35..87826b2f99 100755 --- a/dev/check-outdated-css-keys.js +++ b/dev/check-outdated-css-keys.js @@ -4,7 +4,7 @@ import fs from 'node:fs/promises'; const getCssMapKeys = async () => { const cssMapUrl = /(?<="cssMapUrl":")[^"]+.json(?=")/.exec( - await fetch('https://www.tumblr.com/').then(response => response.text()) + await fetch('https://www.tumblr.com/').then(response => response.text()), )[0]; const cssMap = await fetch(cssMapUrl).then(response => response.json()); return new Set(Object.keys(cssMap)); @@ -13,15 +13,15 @@ const getCssMapKeys = async () => { const getUsedCssKeys = async () => { const sourceFilePaths = await Array.fromAsync(fs.glob('src/**/*.js')); const sourceFileContents = await Promise.all( - sourceFilePaths.map(path => fs.readFile(path, 'utf8')) + sourceFilePaths.map(path => fs.readFile(path, 'utf8')), ); const keyToCssArgsStrings = sourceFileContents.flatMap(file => - [...file.matchAll(/(?<=keyToCss\()[a-zA-Z'\s,]+(?=\))/g)].map(match => match[0]) + [...file.matchAll(/(?<=keyToCss\()[a-zA-Z'\s,]+(?=\))/g)].map(match => match[0]), ); return new Set( keyToCssArgsStrings.flatMap(string => - [...string.matchAll(/(?<=')[a-zA-Z]+(?=')/g)].map(match => match[0]) - ) + [...string.matchAll(/(?<=')[a-zA-Z]+(?=')/g)].map(match => match[0]), + ), ); }; diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 2b8eacbbcf..090f4014d7 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -40,7 +40,7 @@ export const notificationObject = weakMemoize(notificationElement => * threaded reply, its parents' buried note component props values */ export const notePropsObjects = weakMemoize(noteElement => - inject('/main_world/unbury_note_props.js', [], noteElement) + inject('/main_world/unbury_note_props.js', [], noteElement), ); /** From 9ec059ec88c81514792b7b78bf5837fe4790b5ca Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:13:58 -0800 Subject: [PATCH 18/21] Revert "fix commas" This reverts commit 5d48caddcf732db9bb4afb902d5dc0985ba984bd. --- dev/check-outdated-css-keys.js | 10 +++++----- src/utils/react_props.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dev/check-outdated-css-keys.js b/dev/check-outdated-css-keys.js index 87826b2f99..a368bebc35 100755 --- a/dev/check-outdated-css-keys.js +++ b/dev/check-outdated-css-keys.js @@ -4,7 +4,7 @@ import fs from 'node:fs/promises'; const getCssMapKeys = async () => { const cssMapUrl = /(?<="cssMapUrl":")[^"]+.json(?=")/.exec( - await fetch('https://www.tumblr.com/').then(response => response.text()), + await fetch('https://www.tumblr.com/').then(response => response.text()) )[0]; const cssMap = await fetch(cssMapUrl).then(response => response.json()); return new Set(Object.keys(cssMap)); @@ -13,15 +13,15 @@ const getCssMapKeys = async () => { const getUsedCssKeys = async () => { const sourceFilePaths = await Array.fromAsync(fs.glob('src/**/*.js')); const sourceFileContents = await Promise.all( - sourceFilePaths.map(path => fs.readFile(path, 'utf8')), + sourceFilePaths.map(path => fs.readFile(path, 'utf8')) ); const keyToCssArgsStrings = sourceFileContents.flatMap(file => - [...file.matchAll(/(?<=keyToCss\()[a-zA-Z'\s,]+(?=\))/g)].map(match => match[0]), + [...file.matchAll(/(?<=keyToCss\()[a-zA-Z'\s,]+(?=\))/g)].map(match => match[0]) ); return new Set( keyToCssArgsStrings.flatMap(string => - [...string.matchAll(/(?<=')[a-zA-Z]+(?=')/g)].map(match => match[0]), - ), + [...string.matchAll(/(?<=')[a-zA-Z]+(?=')/g)].map(match => match[0]) + ) ); }; diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 090f4014d7..2b8eacbbcf 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -40,7 +40,7 @@ export const notificationObject = weakMemoize(notificationElement => * threaded reply, its parents' buried note component props values */ export const notePropsObjects = weakMemoize(noteElement => - inject('/main_world/unbury_note_props.js', [], noteElement), + inject('/main_world/unbury_note_props.js', [], noteElement) ); /** From 488c8be6ef2407531c8bacd931a0917eccc5b126 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:14:26 -0800 Subject: [PATCH 19/21] Reapply "fix commas" This reverts commit 9ec059ec88c81514792b7b78bf5837fe4790b5ca. --- src/utils/react_props.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 2b8eacbbcf..090f4014d7 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -40,7 +40,7 @@ export const notificationObject = weakMemoize(notificationElement => * threaded reply, its parents' buried note component props values */ export const notePropsObjects = weakMemoize(noteElement => - inject('/main_world/unbury_note_props.js', [], noteElement) + inject('/main_world/unbury_note_props.js', [], noteElement), ); /** From be247992b4b2bd456f963f8adab1ea9e7b5dcac7 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:17:11 -0800 Subject: [PATCH 20/21] organize imports --- src/features/quote_replies/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/quote_replies/index.js b/src/features/quote_replies/index.js index 7b9faa35d8..e6e410d963 100644 --- a/src/features/quote_replies/index.js +++ b/src/features/quote_replies/index.js @@ -2,15 +2,15 @@ import { keyToCss } from '../../utils/css_map.js'; import { dom } from '../../utils/dom.js'; import { inject } from '../../utils/inject.js'; import { buildStyle, displayInlineFlexUnlessDisabledAttr, notificationSelector } from '../../utils/interface.js'; +import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../../utils/meatballs.js'; import { showErrorModal } from '../../utils/modals.js'; import { pageModifications } from '../../utils/mutations.js'; import { notify } from '../../utils/notifications.js'; import { getPreferences } from '../../utils/preferences.js'; +import { timelineObject } from '../../utils/react_props.js'; import { buildSvg } from '../../utils/remixicon.js'; import { apiFetch, navigate } from '../../utils/tumblr_helpers.js'; import { userBlogNames, userBlogs } from '../../utils/user.js'; -import { registerReplyMeatballItem, unregisterReplyMeatballItem } from '../../utils/meatballs.js'; -import { timelineObject } from '../../utils/react_props.js'; const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; From dbf2d5d927ade158b2f39a6fc8954e09c77f1266 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Sun, 15 Feb 2026 16:18:10 -0800 Subject: [PATCH 21/21] remove weakMemoize --- src/utils/react_props.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/utils/react_props.js b/src/utils/react_props.js index 576188fd70..89b38ac175 100644 --- a/src/utils/react_props.js +++ b/src/utils/react_props.js @@ -41,9 +41,10 @@ export const notificationObject = notificationElement => { * @returns {Promise} An object containing the element's buried note component props and, if it is a * threaded reply, its parents' buried note component props values */ -export const notePropsObjects = weakMemoize(noteElement => - inject('/main_world/unbury_note_props.js', [], noteElement), -); +export const notePropsObjects = noteElement => { + noteElement.notePropsObjectsPromise ??= inject('/main_world/unbury_note_props.js', [], noteElement); + return noteElement.notePropsObjectsPromise; +}; /** * @param {Element} meatballMenu An on-screen meatball menu element in a blog modal header or blog card