diff --git a/src/features/mutual_checker.js b/src/features/mutual_checker.js index cb6614c62..ee43df7e9 100644 --- a/src/features/mutual_checker.js +++ b/src/features/mutual_checker.js @@ -1,4 +1,4 @@ -import { buildStyle, getTimelineItemWrapper, filterPostElements, getPopoverWrapper } from '../utils/interface.js'; +import { buildStyle, getTimelineItemWrapper, filterPostElements, getPopoverWrapper, notificationSelector } from '../utils/interface.js'; import { blogData, notificationObject, timelineObject } from '../utils/react_props.js'; import { apiFetch } from '../utils/tumblr_helpers.js'; import { primaryBlogName } from '../utils/user.js'; @@ -14,7 +14,7 @@ const hiddenAttribute = 'data-mutual-checker-hidden'; const mutualsClass = 'from-mutual'; const postAttributionSelector = `header ${keyToCss('attribution')} a:not(${keyToCss('reblogAttribution', 'rebloggedFromName')} *)`; -const onlyMutualsStyleElement = buildStyle(`${keyToCss('notification')}:not([data-mutuals]) { display: none !important; }`); +const onlyMutualsStyleElement = buildStyle(`${notificationSelector}:not([data-mutuals]) { display: none !important; }`); const regularPath = 'M593 500q0-45-22.5-64.5T500 416t-66.5 19-18.5 65 18.5 64.5T500 583t70.5-19 22.5-64zm-90 167q-44 0-83.5 18.5t-63 51T333 808v25h334v-25q0-39-22-71.5t-59.5-51T503 667zM166 168l14-90h558l12-78H180q-8 0-51 63l-42 63v209q-19 3-52 3t-33-3q-1 1 0 27 3 53 0 53l32-2q35-1 53 2v258H2l-3 40q-2 41 3 41 42 0 64-1 7-1 21 1v246h756q25 0 42-13 14-10 22-27 5-13 8-28l1-13V275q0-47-3-63-5-24-22.5-34T832 168H166zm667 752H167V754q17 0 38.5-6.5T241 730q16-12 16-26 0-21-33-28-19-4-57-4-3 0-1-51 2-37 1-36V421q88 0 90-48 1-20-33-30-24-6-57-6-4 0-2-44l2-43h635q14 0 22.5 11t8.5 26v543q0 5 4 26 5 30 5 42 1 22-9 22z'; const aprilFoolsPath = 'M858 352q-6-14-8-35-2-12-4-38-3-38-6-54-7-28-22-43t-43-22q-16-3-54-6-26-2-38-4-21-2-34.5-8T619 124q-9-7-28-24-29-25-44-34-24-16-47-16t-47 16q-15 9-44 34-19 17-28 24-16 12-29.5 18t-34.5 8q-12 2-38 4-38 3-54 6-28 7-43 22t-22 43q-3 16-6 54-2 26-4 38-2 21-8 34.5T124 381q-7 9-24 28-25 29-34 44-16 24-16 47t16 47q9 15 34 44 17 19 24 28 12 16 18 29.5t8 34.5q2 12 4 38 3 38 6 54 7 28 22 43t43 22q16 3 54 6 26 2 38 4 21 2 34.5 8t29.5 18q9 7 28 24 29 25 44 34 24 16 47 16t47-16q15-9 44-34 19-17 28-24 16-12 29.5-18t34.5-8q12-2 38-4 38-3 54-6 28-7 43-22t22-43q3-16 6-54 2-26 4-38 2-21 8-34.5t18-29.5q7-9 24-28 25-29 34-44 16-24 16-47t-16-47q-9-15-34-44-17-19-24-28-12-16-18-29zm-119 62L550 706q-10 17-26.5 27T488 745l-11 1q-34 0-59-24L271 584q-26-25-27-60.5t23.5-61.5 60.5-27.5 62 23.5l71 67 132-204q20-30 55-38t65 11.5 37.5 54.5-11.5 65z'; @@ -57,11 +57,9 @@ const styleElement = buildStyle(` const processNotifications = (notificationElements) => { notificationElements.forEach(async notificationElement => { const notification = await notificationObject(notificationElement); - if (notification) { - const { mutuals } = notification; - if (mutuals) { - notificationElement.dataset.mutuals = mutuals; - } + + if (notification?.mutuals || notification?.title?.relationshipLabel === 'mutuals') { + notificationElement.setAttribute('data-mutuals', ''); } }); }; diff --git a/src/features/notificationblock.js b/src/features/notificationblock.js index e3ec8b09d..594e4c337 100644 --- a/src/features/notificationblock.js +++ b/src/features/notificationblock.js @@ -25,7 +25,11 @@ const processNotifications = (notificationElements) => { const notification = await notificationObject(notificationElement); if (notification !== undefined) { const { targetRootPostId, targetPostId } = notification; - notificationElement.dataset.targetRootPostId = targetRootPostId || targetPostId; + + // available on "replied to your post" notifications, which appear to always target the root post + const blockablePostId = notification.actions?.longTap?.meta?.postId; + + notificationElement.dataset.targetRootPostId = targetRootPostId || targetPostId || blockablePostId || ''; } }); }; diff --git a/src/features/quote_replies.css b/src/features/quote_replies.css deleted file mode 100644 index 43e041056..000000000 --- a/src/features/quote_replies.css +++ /dev/null @@ -1,41 +0,0 @@ -button.xkit-quote-replies { - position: relative; - align-self: center; - transform: translateY(-2px); - - display: inline-flex; - align-items: center; - margin: 0 6px; - - cursor: pointer; -} - -button.xkit-quote-replies svg { - width: 21.5px; - height: 21.5px; - - fill: rgb(var(--blue)); - transition: all .25s ease-out .4s; -} - -button.xkit-quote-replies:disabled svg { - fill: rgba(var(--black), 0.65); - transition-property: none; -} - -button.xkit-quote-replies-dropdown { - align-self: flex-start; - margin: 10px 0 0; -} - -@media (hover: hover) { - button.xkit-quote-replies svg { - opacity: 0; - transform: scale(0); - } - - :is(:hover, :focus-within) > button.xkit-quote-replies svg { - opacity: 1; - transform: scale(1); - } -} diff --git a/src/features/quote_replies.js b/src/features/quote_replies.js index ae0403101..f8d9dd5c7 100644 --- a/src/features/quote_replies.js +++ b/src/features/quote_replies.js @@ -2,7 +2,7 @@ import { keyToCss } from '../utils/css_map.js'; import { dom } from '../utils/dom.js'; import { inject } from '../utils/inject.js'; import { showErrorModal } from '../utils/modals.js'; -import { notificationSelector } from '../utils/interface.js'; +import { buildStyle, notificationSelector } from '../utils/interface.js'; import { pageModifications } from '../utils/mutations.js'; import { notify } from '../utils/notifications.js'; import { getPreferences } from '../utils/preferences.js'; @@ -14,9 +14,53 @@ const storageKey = 'quote_replies.draftLocation'; const buttonClass = 'xkit-quote-replies'; const dropdownButtonClass = 'xkit-quote-replies-dropdown'; +export const styleElement = buildStyle(` +button.xkit-quote-replies { + position: relative; + align-self: center; + transform: translateY(-2px); + + display: inline-flex; + align-items: center; + margin: 0 6px; + + cursor: pointer; +} + +button.xkit-quote-replies svg { + width: 21.5px; + height: 21.5px; + + fill: rgb(var(--blue)); + transition: all .25s ease-out .4s; +} + +button.xkit-quote-replies:disabled svg { + fill: rgba(var(--black), 0.65); + transition-property: none; +} + +button.xkit-quote-replies-dropdown { + align-self: flex-start; + margin: 10px 0 0; +} + +@media (hover: hover) { + button.xkit-quote-replies svg { + opacity: 0; + transform: scale(0); + } + + ${notificationSelector}:is(:hover, :focus-within) button.xkit-quote-replies svg { + opacity: 1; + transform: scale(1); + } +} +`); + const originalPostTagStorageKey = 'quick_tags.preferences.originalPostTag'; -const activitySelector = `${keyToCss('notification')} > ${keyToCss('activity')}`; +const activitySelector = `:is(${keyToCss('notification')} > ${keyToCss('activity')}, ${keyToCss('activityContent')})`; const dropdownSelector = '[role="tabpanel"] *'; @@ -25,15 +69,16 @@ let tagReplyingBlog; let newTab; const processNotifications = notifications => notifications.forEach(async notification => { - const { notification: notificationProps, tumblelogName } = await inject( - '/main_world/get_notification_props.js', - [], - notification - ); + const [notificationProps, tumblelogName] = await Promise.all([ + inject('/main_world/get_notification_props.js', [], notification), + inject('/main_world/get_tumblelogname_prop.js', [], notification) + ]); - if (!['reply', 'reply_to_comment', 'note_mention'].includes(notificationProps.type)) return; + if (!['reply', 'reply_to_comment', 'note_mention'].includes(notificationProps.type === 'generic' ? notificationProps.subtype : notificationProps.type)) return; if (notificationProps.community) return; + const quoteReply = notificationProps.type === 'generic' ? quoteGenericReply : quoteLegacyReply; + const activityElement = notification.querySelector(activitySelector); if (!activityElement) return; @@ -55,7 +100,45 @@ const processNotifications = notifications => notifications.forEach(async notifi )); }); -const quoteReply = async (tumblelogName, notificationProps) => { +const quoteGenericReply = async (tumblelogName, notificationProps) => { + const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; + + const { + title: { textContent: titleContent }, + body: { content: [bodyDescriptionContent, bodyQuoteContent] }, + actions + } = notificationProps; + + const replyingBlog = titleContent.formatting.find(({ type }) => type === 'mention').blog; + + const toConvertToLink = bodyDescriptionContent.formatting.find(({ type }) => type === 'semantic_color'); + + const content = [ + { + type: 'text', + text: `@${replyingBlog.name}`, + formatting: [ + { start: 0, end: replyingBlog.name.length + 1, type: 'mention', blog: { uuid: replyingBlog.uuid } }] + }, + { + type: 'text', + text: bodyDescriptionContent.text, + formatting: [ + { start: toConvertToLink.start, end: toConvertToLink.end, type: 'link', url: actions.tap.href } + ] + }, + bodyQuoteContent, + { type: 'text', text: '\u200B' } + ]; + const tags = [ + ...originalPostTag ? [originalPostTag] : [], + ...tagReplyingBlog ? [replyingBlog.name] : [] + ].join(','); + + createDraft({ uuid, content, tags, tumblelogName }); +}; + +const quoteLegacyReply = async (tumblelogName, notificationProps) => { const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid; const { type, targetPostId, targetPostSummary, targetTumblelogName, targetTumblelogUuid, timestamp } = notificationProps; @@ -90,6 +173,10 @@ const quoteReply = async (tumblelogName, notificationProps) => { ...tagReplyingBlog ? [reply.blog.name] : [] ].join(','); + createDraft({ uuid, content, tags, tumblelogName }); +}; + +const createDraft = async ({ uuid, content, tags, tumblelogName }) => { const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } }); const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`; @@ -135,5 +222,3 @@ export const onStorageChanged = async function (changes) { ({ tagReplyingBlog, newTab } = await getPreferences('quote_replies')); } }; - -export const stylesheet = true; diff --git a/src/features/quote_replies.json b/src/features/quote_replies.json index 20cbf46fd..95196ed6e 100644 --- a/src/features/quote_replies.json +++ b/src/features/quote_replies.json @@ -1,6 +1,7 @@ { "title": "Quote Replies", "description": "Reply to reply notifications", + "note": "This feature may be only partially functional.", "icon": { "class_name": "ri-chat-quote-line", "color": "white", diff --git a/src/main_world/get_notification_props.js b/src/main_world/get_notification_props.js index 3322f1385..b9b39b214 100644 --- a/src/main_world/get_notification_props.js +++ b/src/main_world/get_notification_props.js @@ -6,7 +6,7 @@ export default function getNotificationProps () { while (fiber !== null) { const props = fiber.memoizedProps || {}; if (props?.notification !== undefined) { - return props; + return props.notification; } else { fiber = fiber.return; } diff --git a/src/main_world/get_tumblelogname_prop.js b/src/main_world/get_tumblelogname_prop.js new file mode 100644 index 000000000..8ef12501c --- /dev/null +++ b/src/main_world/get_tumblelogname_prop.js @@ -0,0 +1,14 @@ +export default function getTumblelogNameProp () { + const notificationElement = this; + const reactKey = Object.keys(notificationElement).find(key => key.startsWith('__reactFiber')); + let fiber = notificationElement[reactKey]; + + while (fiber !== null) { + const props = fiber.memoizedProps || {}; + if (props?.tumblelogName !== undefined) { + return props.tumblelogName; + } else { + fiber = fiber.return; + } + } +} diff --git a/src/utils/interface.js b/src/utils/interface.js index 86b67680b..1dfe202fd 100644 --- a/src/utils/interface.js +++ b/src/utils/interface.js @@ -4,7 +4,7 @@ import { timelineSelector } from './timeline_id.js'; export const postSelector = '[tabindex="-1"][data-id]'; export const blogViewSelector = '[style*="--blog-title-color"] *'; -export const notificationSelector = `${keyToCss('notification')}[role="listitem"]`; +export const notificationSelector = `:is(${keyToCss('notification')}[role="listitem"], ${keyToCss('activityItem')})`; const listTimelineObjectSelector = keyToCss('listTimelineObject'); const cellSelector = keyToCss('cell');