Skip to content
12 changes: 5 additions & 7 deletions src/features/mutual_checker.js
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand Down Expand Up @@ -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', '');
}
});
};
Expand Down
6 changes: 5 additions & 1 deletion src/features/notificationblock.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '';
}
});
};
Expand Down
41 changes: 0 additions & 41 deletions src/features/quote_replies.css

This file was deleted.

126 changes: 112 additions & 14 deletions src/features/quote_replies.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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"] *';

Expand All @@ -25,14 +69,14 @@ 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;
if (notificationProps.actions?.tap?.href && new URL(notificationProps.actions.tap.href).pathname.startsWith('/communities/')) return;

const activityElement = notification.querySelector(activitySelector);
if (!activityElement) return;
Expand All @@ -55,12 +99,58 @@ 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 processGenericReply = async (notificationProps) => {
const {
subtype: type,
timestamp,
title: { textContent: titleContent },
body: { content: [bodyDescriptionContent, bodyQuoteContent] },
actions
} = notificationProps;
const summaryFormatting = bodyDescriptionContent.formatting.find(({ type }) => type === 'semantic_color');

try {
const [, targetTumblelogName, targetPostId] =
/^\/@?([a-z0-9-]{1,32})\/([0-9]{1,20})\//.exec(new URL(actions.tap.href).pathname);

const targetPostSummary = bodyDescriptionContent.text.slice(summaryFormatting.start + 1, summaryFormatting.end - 1);

return await processReply({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary });
} catch (exception) {
console.log(exception);
console.log('falling back to generic quote content due to fetch/parse failure');
}

const replyingBlog = titleContent.formatting.find(({ type }) => type === 'mention').blog;

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: summaryFormatting.start, end: summaryFormatting.end, type: 'link', url: actions.tap.href }
]
},
bodyQuoteContent,
{ type: 'text', text: '\u200B' }
];
const tags = [
...originalPostTag ? [originalPostTag] : [],
...tagReplyingBlog ? [replyingBlog.name] : []
].join(',');

return { content, tags };
};

const processReply = async ({ type, timestamp, targetPostId, targetTumblelogName, targetPostSummary }) => {
const { response } = await apiFetch(
`/v2/blog/${targetTumblelogUuid}/post/${targetPostId}/notes/timeline`,
`/v2/blog/${targetTumblelogName}/post/${targetPostId}/notes/timeline`,
{ queryParams: { mode: 'replies', before_timestamp: `${timestamp + 1}000000` } }
);

Expand Down Expand Up @@ -90,6 +180,16 @@ const quoteReply = async (tumblelogName, notificationProps) => {
...tagReplyingBlog ? [reply.blog.name] : []
].join(',');

return { content, tags };
};

const quoteReply = async (tumblelogName, notificationProps) => {
const uuid = userBlogs.find(({ name }) => name === tumblelogName).uuid;

const { content, tags } = notificationProps.type === 'generic'
? await processGenericReply(notificationProps)
: await processReply(notificationProps);

const { response: { id: responseId, displayText } } = await apiFetch(`/v2/blog/${uuid}/posts`, { method: 'POST', body: { content, state: 'draft', tags } });

const currentDraftLocation = `/edit/${tumblelogName}/${responseId}`;
Expand Down Expand Up @@ -135,5 +235,3 @@ export const onStorageChanged = async function (changes) {
({ tagReplyingBlog, newTab } = await getPreferences('quote_replies'));
}
};

export const stylesheet = true;
2 changes: 1 addition & 1 deletion src/main_world/get_notification_props.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
14 changes: 14 additions & 0 deletions src/main_world/get_tumblelogname_prop.js
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
2 changes: 1 addition & 1 deletion src/utils/interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down