diff --git a/src/features/notificationblock.js b/src/features/notificationblock.js index 1a7d1426de..dce72cfe70 100644 --- a/src/features/notificationblock.js +++ b/src/features/notificationblock.js @@ -1,19 +1,51 @@ import { buildStyle } from '../utils/interface.js'; import { registerMeatballItem, unregisterMeatballItem } from '../utils/meatballs.js'; import { onNewNotifications } from '../utils/mutations.js'; -import { showModal, hideModal, modalCancelButton } from '../utils/modals.js'; +import { showModal, hideModal, modalCancelButton, modalCompleteButton } from '../utils/modals.js'; import { dom } from '../utils/dom.js'; -import { userBlogNames } from '../utils/user.js'; -import { apiFetch } from '../utils/tumblr_helpers.js'; +import { userBlogNames, userBlogs } from '../utils/user.js'; +import { apiFetch, navigate } from '../utils/tumblr_helpers.js'; import { notificationObject } from '../utils/react_props.js'; const storageKey = 'notificationblock.blockedPostTargetIDs'; +const uuidsStorageKey = 'notificationblock.uuids'; +const toOpenStorageKey = 'notificationblock.toOpen'; const meatballButtonBlockId = 'notificationblock-block'; const meatballButtonBlockLabel = 'Block notifications'; const meatballButtonUnblockId = 'notificationblock-unblock'; const meatballButtonUnblockLabel = 'Unblock notifications'; +const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); + let blockedPostTargetIDs; +let uuids = {}; + +const userBlogsToSearch = userBlogs + .filter(({ posts }) => posts) + .sort((a, b) => b.posts - a.posts); + +const findUuid = async id => { + if (uuids[id]) return; + uuids[id] = false; + for (const { uuid } of userBlogsToSearch) { + const delay = sleep(500); + try { + await apiFetch(`/v2/blog/${uuid}/posts/${id}`); + uuids[id] = uuid; + break; + } catch (e) { + await delay; + } + } + await browser.storage.local.set({ [uuidsStorageKey]: uuids }); +}; + +const backgroundFindUuids = async () => { + const idsMissingUuids = blockedPostTargetIDs.filter(id => uuids[id] === undefined).reverse(); + for (const id of idsMissingUuids) { + await findUuid(id); + } +}; const styleElement = buildStyle(); const buildCss = () => `:is(${blockedPostTargetIDs.map(rootId => `[data-target-root-post-id="${rootId}"]`).join(', ') @@ -38,10 +70,11 @@ const muteNotificationsMessage = [ ]; const onButtonClicked = async function ({ currentTarget }) { - const { id, rebloggedRootId, blog: { uuid } } = currentTarget.__timelineObjectData; + const { id, rebloggedRootId, blog: { uuid }, rebloggedRootUuid } = currentTarget.__timelineObjectData; const { response: { muted } } = await apiFetch(`/v2/blog/${uuid}/posts/${id}`); const rootId = rebloggedRootId || id; + const rootUuid = rebloggedRootUuid || uuid; const shouldBlockNotifications = blockedPostTargetIDs.includes(rootId) === false; const title = shouldBlockNotifications @@ -61,8 +94,18 @@ const onButtonClicked = async function ({ currentTarget }) { ? 'red' : 'blue'; const saveNotificationPreference = shouldBlockNotifications - ? () => { blockedPostTargetIDs.push(rootId); browser.storage.local.set({ [storageKey]: blockedPostTargetIDs }); } - : () => browser.storage.local.set({ [storageKey]: blockedPostTargetIDs.filter(blockedId => blockedId !== rootId) }); + ? () => { + blockedPostTargetIDs.push(rootId); + browser.storage.local.set({ [storageKey]: blockedPostTargetIDs }); + uuids[rootId] = rootUuid; + browser.storage.local.set({ [uuidsStorageKey]: uuids }); + } + : () => { + blockedPostTargetIDs = blockedPostTargetIDs.filter(blockedId => blockedId !== rootId); + browser.storage.local.set({ [storageKey]: blockedPostTargetIDs }); + delete uuids[rootId]; + browser.storage.local.set({ [uuidsStorageKey]: uuids }); + }; showModal({ title, @@ -94,7 +137,13 @@ const unblockPostFilter = async ({ id, rebloggedRootId }) => { }; export const onStorageChanged = (changes, areaName) => { - if (Object.keys(changes).includes(storageKey)) { + const { [storageKey]: blockedPostChanges, [uuidsStorageKey]: uuidsChanges } = changes; + + if (uuidsChanges) { + ({ newValue: uuids } = uuidsChanges); + } + + if (blockedPostChanges) { blockedPostTargetIDs = changes[storageKey].newValue; styleElement.textContent = buildCss(); } @@ -102,12 +151,42 @@ export const onStorageChanged = (changes, areaName) => { export const main = async function () { ({ [storageKey]: blockedPostTargetIDs = [] } = await browser.storage.local.get(storageKey)); + ({ [uuidsStorageKey]: uuids = {} } = await browser.storage.local.get(uuidsStorageKey)); + + backgroundFindUuids(); + styleElement.textContent = buildCss(); document.documentElement.append(styleElement); onNewNotifications.addListener(processNotifications); registerMeatballItem({ id: meatballButtonBlockId, label: meatballButtonBlockLabel, onclick: onButtonClicked, postFilter: blockPostFilter }); registerMeatballItem({ id: meatballButtonUnblockId, label: meatballButtonUnblockLabel, onclick: onButtonClicked, postFilter: unblockPostFilter }); + + const { [toOpenStorageKey]: toOpen } = await browser.storage.local.get(toOpenStorageKey); + if (toOpen) { + browser.storage.local.remove(toOpenStorageKey); + + const { blockedPostID } = toOpen; + try { + showModal({ + title: 'NotificationBlock', + message: [`Searching for post ${blockedPostID} on your blogs. Please wait...`] + }); + await findUuid(blockedPostID); + if (!uuids[blockedPostID]) { + throw new Error(); + } + const { response: { blog: { name } } } = await apiFetch(`/v2/blog/${uuids[blockedPostID]}/info`); + hideModal(); + navigate(`/${name}/${blockedPostID}`); + } catch (e) { + showModal({ + title: 'NotificationBlock', + message: [`Failed to find and open post ${blockedPostID}! It may not be one of your original posts.`], + buttons: [modalCompleteButton] + }); + } + } }; export const clean = async function () { diff --git a/src/features/notificationblock.json b/src/features/notificationblock.json index b0969fa4c6..12ccf68e67 100644 --- a/src/features/notificationblock.json +++ b/src/features/notificationblock.json @@ -7,5 +7,12 @@ "background_color": "#f5223c" }, "help": "https://github.com/AprilSylph/XKit-Rewritten/wiki/Features#notificationblock", - "relatedTerms": [ "activity", "mute notifications", "notes" ] + "relatedTerms": [ "activity", "mute notifications", "notes" ], + "preferences": { + "manageBlockedPosts": { + "type": "iframe", + "label": "Manage posts with blocked notifications", + "src": "/features/notificationblock/options/index.html" + } + } } diff --git a/src/features/notificationblock/options/index.css b/src/features/notificationblock/options/index.css new file mode 100644 index 0000000000..55e45b4d48 --- /dev/null +++ b/src/features/notificationblock/options/index.css @@ -0,0 +1,69 @@ +:root { + --black: 21, 20, 25; + --white: 255, 255, 255; + --grey: 207, 207, 216; + --accent: 10, 132, 255; +} + +@media (prefers-color-scheme: dark) { + :root { + --black: 251, 251, 254; + --white: 66, 65, 77; + --grey: 91, 91, 102; + --accent: 54, 213, 255; + } +} + +html { + font-size: 14px; + scrollbar-color: rgb(var(--grey)) transparent; + scrollbar-width: thin; + overflow-y: hidden; +} + +body { + background-color: rgb(var(--white)); + color: rgb(var(--black)); + font-family: "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; + font-size: 100%; + -webkit-user-select: none; + user-select: none; +} + +header { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + margin-bottom: 1em; +} + +#notification-blocked-count { + font-weight: bold; +} + +#notification-blocked-posts { + padding: 0; + border-bottom: 1px solid rgb(var(--grey)); + margin: 0; +} + +.notification-blocked-post { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + padding: 1ch 0; + border-top: 1px solid rgb(var(--grey)); +} + +.notification-blocked-post button { + padding: 0; + border: none; + + appearance: none; + background-color: transparent; + color: rgb(var(--accent)); + cursor: pointer; + font-weight: bold; +} diff --git a/src/features/notificationblock/options/index.html b/src/features/notificationblock/options/index.html new file mode 100644 index 0000000000..50cac9bb45 --- /dev/null +++ b/src/features/notificationblock/options/index.html @@ -0,0 +1,28 @@ + + + + + + XKit: Manage posts with blocked notifications + + + + + + + + + +
+
+ +
+ +
+ + diff --git a/src/features/notificationblock/options/index.js b/src/features/notificationblock/options/index.js new file mode 100644 index 0000000000..eaef18b918 --- /dev/null +++ b/src/features/notificationblock/options/index.js @@ -0,0 +1,50 @@ +const postsBlockedCount = document.getElementById('notification-blocked-count'); +const blockedPostList = document.getElementById('notification-blocked-posts'); +const blockedPostTemplate = document.getElementById('notification-blocked-post'); + +const storageKey = 'notificationblock.blockedPostTargetIDs'; +const toOpenStorageKey = 'notificationblock.toOpen'; + +const unblockPost = async function ({ currentTarget }) { + let { [storageKey]: blockedPostRootIDs = [] } = await browser.storage.local.get(storageKey); + + blockedPostRootIDs = blockedPostRootIDs.filter(id => id !== currentTarget.dataset.postId); + await browser.storage.local.set({ [storageKey]: blockedPostRootIDs }); + + currentTarget.remove(); +}; + +const renderBlocked = async function () { + const { [storageKey]: blockedPostRootIDs = [] } = await browser.storage.local.get(storageKey); + + postsBlockedCount.textContent = `${blockedPostRootIDs.length} ${blockedPostRootIDs.length === 1 ? 'post' : 'posts'} with blocked notifications`; + blockedPostList.textContent = ''; + + for (const blockedPostID of blockedPostRootIDs) { + const templateClone = blockedPostTemplate.content.cloneNode(true); + const anchorElement = templateClone.querySelector('a'); + const spanElement = templateClone.querySelector('span'); + const unblockButton = templateClone.querySelector('button'); + + spanElement.textContent = blockedPostID; + unblockButton.dataset.postId = blockedPostID; + unblockButton.addEventListener('click', unblockPost); + + anchorElement.addEventListener('click', async () => { + await browser.storage.local.set({ + [toOpenStorageKey]: { blockedPostID } + }); + window.open('https://www.tumblr.com/'); + }); + + blockedPostList.append(templateClone); + } +}; + +browser.storage.onChanged.addListener((changes, areaName) => { + if (areaName === 'local' && Object.keys(changes).includes(storageKey)) { + renderBlocked(); + } +}); + +renderBlocked();