vshih / Jira next-gen - Dedupe notifications

// ==UserScript==
// @name			Jira next-gen - Dedupe notifications
// @namespace		https://blog.vicshih.com/2022/04/jira-next-gen-dedupe-notifications.html
// @version			0.2
// @description		Jira next-gen - Hide duplicate notifications for issues.
// @author			Victor Shih
// @match			https://*.atlassian.net/*
// @icon			https://www.google.com/s2/favicons?sz=64&domain=atlassian.net
// @grant			none
// @license			GPL-3.0-or-later
// ==/UserScript==

/* jshint esversion: 6 */

(function () {
	'use strict';


	const articleSelector = '[data-testid="notification-item-container"]';


	function triggerGroup(event) {
		if (localStorage.dedupe != 'true' || event.detail.dispatched) { return }

		const theButton = event.currentTarget;
		const theHref = theButton.closest(articleSelector).querySelector('a').getAttribute('href');

		// Trigger all other (hidden) buttons with the same href.
		const feed = document.querySelector('[role="feed"]');
		for (let article of feed.querySelectorAll(articleSelector)) {
			const button = article.querySelector('[data-testid="read-state-indicator"]'),
				href = article.querySelector('a').getAttribute('href').split('?')[0];

			if (button != theButton && href == theHref) {
				button.dispatchEvent(new CustomEvent(
					'click',
					{bubbles: true, detail: {dispatched: true}}
				));
			}
		}
	}


	function handleCheckDedupe(event, checked) {
		if (event) {
			checked = event.currentTarget.checked;
			localStorage.dedupe = checked;
		}

		const feed = document.querySelector('[role="feed"]');
		/* TODO debounce
		if (feed.getAttribute('deduped')) { return }
		feed.setAttribute('deduped', true);
		*/

		const articles = feed.querySelectorAll(articleSelector);

		if (checked) {
			let hrefs = new Set();

			for (let article of articles) {
				const button = article.querySelector('[data-testid="read-state-indicator"]'),
					href = article.querySelector('a').getAttribute('href').split('?')[0];

				if (hrefs.has(href)) {
					// Hide the (parent) node.
					article.parentNode.style.display = 'none';
				} else {
					hrefs.add(href);
				}

				if (!button.getAttribute('dedupe-handler')) {
					button.setAttribute('dedupe-handler', true);
					button.addEventListener('click', triggerGroup, {passive: true});
				}
			}
		} else {
			for (let article of articles) {
				// Just show every article's direct-parent.
				article.parentNode.style.display = '';
			}
		}
	}


	function poll() {
		let h1 = document.querySelector('#notification-list-header-label');

		if (!h1) {
			// Notifications not open.
			return;
		}

		const checked = localStorage.dedupe == 'true';

		if (h1.nextElementSibling.getAttribute('for') != 'dedupe') {
			// Inject dedupe checkbox.
			let label = document.createElement('label');
			label.setAttribute('for', 'dedupe');
			label.innerHTML = `Dedupe <input id="dedupe" type="checkbox" ${checked ? 'checked' : ''}>`;
			label.style.marginRight = '15px';
			h1.after(label);

			// Register handler.
			const dedupe = document.querySelector('#dedupe');
			dedupe.addEventListener('change', handleCheckDedupe, {passive: true});
		}

		handleCheckDedupe(null, checked);
	}


	setInterval(poll, 250);
})();