From b75a8ba873b43b55b1cf7a07893e51a91648e125 Mon Sep 17 00:00:00 2001 From: marcustyphoon Date: Tue, 7 May 2024 18:19:34 -0700 Subject: [PATCH] Mutual Checker: Icons in blog cards; icon tooltips (#889) Co-authored-by: alleycatboy <118627996+alleycatboy@users.noreply.github.com> Co-authored-by: April Sylph <28949509+AprilSylph@users.noreply.github.com> --- src/scripts/mutual_checker.css | 12 --- src/scripts/mutual_checker.js | 131 ++++++++++++++++++++++++--------- 2 files changed, 95 insertions(+), 48 deletions(-) delete mode 100644 src/scripts/mutual_checker.css diff --git a/src/scripts/mutual_checker.css b/src/scripts/mutual_checker.css deleted file mode 100644 index 81108b4ff..000000000 --- a/src/scripts/mutual_checker.css +++ /dev/null @@ -1,12 +0,0 @@ -svg.xkit-mutual-icon { - vertical-align: text-bottom; - - height: 1.23em; - margin-top: 0; - margin-left: 0; - margin-right: 0.5ch; -} - -[data-timeline="/v2/timeline/dashboard"] [data-mutual-checker-hidden] article { - display: none; -} diff --git a/src/scripts/mutual_checker.js b/src/scripts/mutual_checker.js index e0df7abcc..a3cea79ce 100644 --- a/src/scripts/mutual_checker.js +++ b/src/scripts/mutual_checker.js @@ -1,28 +1,57 @@ import { buildStyle, getTimelineItemWrapper, filterPostElements, getPopoverWrapper } from '../util/interface.js'; -import { notificationObject, timelineObject } from '../util/react_props.js'; +import { blogData, notificationObject, timelineObject } from '../util/react_props.js'; import { apiFetch } from '../util/tumblr_helpers.js'; import { primaryBlogName } from '../util/user.js'; import { keyToCss } from '../util/css_map.js'; -import { onNewPosts, onNewNotifications } from '../util/mutations.js'; +import { onNewPosts, onNewNotifications, pageModifications } from '../util/mutations.js'; import { dom } from '../util/dom.js'; import { getPreferences } from '../util/preferences.js'; +import { translate } from '../util/language_data.js'; const mutualIconClass = 'xkit-mutual-icon'; const hiddenAttribute = 'data-mutual-checker-hidden'; const mutualsClass = 'from-mutual'; const postAttributionSelector = `header ${keyToCss('attribution')} a:not(${keyToCss('reblogAttribution', 'rebloggedFromName')} *)`; -const styleElement = buildStyle(`${keyToCss('notification')}:not([data-mutuals]) { display: none !important; }`); +const onlyMutualsStyleElement = buildStyle(`${keyToCss('notification')}: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'; const following = {}; -const mutuals = {}; +const followingYou = {}; let showOnlyMutuals; let showOnlyMutualNotifications; -let icon; + +const styleElement = buildStyle(` + svg.xkit-mutual-icon { + vertical-align: text-bottom; + + height: 1.125rem; + margin-top: 0; + margin-left: 0; + margin-right: 0.5ch; + } + + [data-timeline="/v2/timeline/dashboard"] [${hiddenAttribute}] article { + display: none; + } + + ${keyToCss('blogCardBlogLink')} { + display: flex; + } + + ${keyToCss('blogCardBlogLink')} svg.xkit-mutual-icon { + position: relative; + top: 3px; + + box-sizing: border-box; + flex: none; + height: 1.5rem; + padding: 0.1875rem 0; + } +`); const processNotifications = (notificationElements) => { notificationElements.forEach(async notificationElement => { @@ -50,72 +79,102 @@ const addIcons = function (postElements) { const blogName = postAttribution.textContent.trim(); if (!blogName) return; - if (following[blogName] === undefined) { - const { blog } = await timelineObject(postElement); - if (blogName === blog.name) { - following[blogName] = Promise.resolve(blog.followed && !blog.isMember); - } else { - following[blogName] = apiFetch(`/v2/blog/${blogName}/info`) - .then(({ response: { blog: { followed } } }) => followed) - .catch(() => Promise.resolve(false)); - } - } - - const followingBlog = await following[blogName]; + const followingBlog = await getIsFollowing(blogName, postElement); if (!followingBlog) { return; } - if (mutuals[blogName] === undefined) { - mutuals[blogName] = apiFetch(`/v2/blog/${primaryBlogName}/followed_by`, { queryParams: { query: blogName } }) - .then(({ response: { followedBy } }) => followedBy) - .catch(() => Promise.resolve(false)); - } - - const isMutual = await mutuals[blogName]; + const isMutual = await getIsFollowingYou(blogName); if (isMutual) { postElement.classList.add(mutualsClass); const iconTarget = getPopoverWrapper(postAttribution) ?? postAttribution; - iconTarget?.before(icon.cloneNode(true)); + iconTarget?.before(createIcon(blogName)); } else if (showOnlyMutuals) { getTimelineItemWrapper(postElement)?.setAttribute(hiddenAttribute, ''); } }); }; +const addBlogCardIcons = blogCardLinks => + blogCardLinks.forEach(async blogCardLink => { + const blogName = blogCardLink.querySelector(keyToCss('blogLinkShort'))?.textContent || blogCardLink?.textContent; + if (!blogName) return; + + const followingBlog = await getIsFollowing(blogName, blogCardLink); + if (!followingBlog) return; + + const isMutual = await getIsFollowingYou(blogName); + if (isMutual) { + blogCardLink.before(createIcon(blogName, getComputedStyle(blogCardLink).color)); + } + }); + +const getIsFollowing = async (blogName, element) => { + const blog = await blogData(element) ?? (await timelineObject(element))?.blog; + + if (following[blogName] === undefined) { + if (blogName === blog?.name) { + following[blogName] = Promise.resolve(blog.followed && !blog.isMember); + } else { + following[blogName] = apiFetch(`/v2/blog/${blogName}/info`) + .then(({ response: { blog: { followed } } }) => followed) + .catch(() => Promise.resolve(false)); + } + } + return following[blogName]; +}; + +const getIsFollowingYou = (blogName) => { + if (followingYou[blogName] === undefined) { + followingYou[blogName] = apiFetch(`/v2/blog/${primaryBlogName}/followed_by`, { queryParams: { query: blogName } }) + .then(({ response: { followedBy } }) => followedBy) + .catch(() => Promise.resolve(false)); + } + return followingYou[blogName]; +}; + export const main = async function () { if (primaryBlogName === undefined) return; + document.documentElement.append(styleElement); ({ showOnlyMutuals, showOnlyMutualNotifications } = await getPreferences('mutual_checker')); following[primaryBlogName] = Promise.resolve(false); + onNewPosts.addListener(addIcons); + pageModifications.register(`${keyToCss('blogCard')} ${keyToCss('blogCardBlogLink')} > a`, addBlogCardIcons); + + if (showOnlyMutualNotifications) { + document.documentElement.append(onlyMutualsStyleElement); + onNewNotifications.addListener(processNotifications); + } +}; + +const createIcon = (blogName, color = 'rgb(var(--black))') => { const today = new Date(); const aprilFools = (today.getMonth() === 3 && today.getDate() === 1); - icon = dom('svg', { + const icon = dom('svg', { xmlns: 'http://www.w3.org/2000/svg', class: mutualIconClass, viewBox: '0 0 1000 1000', - fill: aprilFools ? '#00b8ff' : 'rgb(var(--black))' + fill: aprilFools ? '#00b8ff' : color }, null, [ + dom('title', { xmlns: 'http://www.w3.org/2000/svg' }, null, [ + translate('{{blogNameLink /}} follows you!').replace('{{blogNameLink /}}', blogName) + ]), dom('path', { xmlns: 'http://www.w3.org/2000/svg', d: aprilFools ? aprilFoolsPath : regularPath }) ]); - - onNewPosts.addListener(addIcons); - if (showOnlyMutualNotifications) { - document.documentElement.append(styleElement); - onNewNotifications.addListener(processNotifications); - } + return icon; }; export const clean = async function () { onNewPosts.removeListener(addIcons); + pageModifications.unregister(addBlogCardIcons); if (showOnlyMutualNotifications) { - styleElement.remove(); + onlyMutualsStyleElement.remove(); onNewNotifications.removeListener(processNotifications); } + styleElement.remove(); $(`.${mutualsClass}`).removeClass(mutualsClass); $(`[${hiddenAttribute}]`).removeAttr(hiddenAttribute); $(`.${mutualIconClass}`).remove(); }; - -export const stylesheet = true;