Greasy Fork is available in English.

pixiv ツリー型コメント

Deletes stamps on comment sections. In addition, changes the comment section’s default linear mode to threaded mode on comment sections in group pages.

// ==UserScript==
// @name        pixiv ツリー型コメント
// @name:ja     pixiv スタンプを削除
// @name:en     pixiv Stamp Eraser
// @description Deletes stamps on comment sections. In addition, changes the comment section’s default linear mode to threaded mode on comment sections in group pages.
// @description:ja コメント欄のスタンプを削除します。また、グループ機能において、返信コメントを返信先の下に移動します。
// @namespace   https://greasyfork.org/users/137
// @version     5.1.0
// @match       https://www.pixiv.net/*
// @require     https://greasyfork.org/scripts/17896/code/start-script.js?version=112958
// @license     MPL-2.0
// @contributionURL https://www.amazon.co.jp/registry/wishlist/E7PJ5C3K7AM2
// @compatible  Edge
// @compatible  Firefox 推奨 (Recommended)
// @compatible  Opera
// @compatible  Chrome
// @grant       dummy
// @noframes
// @run-at      document-start
// @icon        https://www.pixiv.net/favicon.ico
// @author      100の人
// @homepageURL https://greasyfork.org/scripts/5291
// ==/UserScript==

'use strict';

class StampEraser
{
	constructor()
	{
		const root = document.getElementById('root');
		if (!root) {
			return;
		}

		new MutationObserver(mutations => {
			for (const mutation of mutations) {
				const commentListParentSelector = 'main h2 ~ div:nth-of-type(2)';
				if (!mutation.target.matches(`${commentListParentSelector},
					${commentListParentSelector} > ul, ${commentListParentSelector} > ul *`)) {
					continue;
				}

				for (const element of mutation.addedNodes) {
					switch (element.localName) {
						case 'ul':
							this.hideStampComments(element);
							break;
						case 'li':
							this.hideIfStamp(element);
							break;
					}
				}
			}
		}).observe(root, {childList: true, subtree: true});
	}

	/**
	 * コメントリストからスタンプを削除します。
	 * @access private
	 * @param {HTMLLIElement} comment
	 */
	hideStampComments(commentList)
	{
		for (const comment of Array.from(commentList.children)) {
			this.hideIfStamp(comment);
		}
	}

	/**
	 * スタンプコメント、または絵文字単体のコメントであれば削除します。
	 * @access private
	 * @param {HTMLLIElement} comment
	 */
	hideIfStamp(comment)
	{
		let ul, emoji;
		if (!comment.hidden
			&& (!(ul = comment.getElementsByTagName('ul')[0]) || ul.hidden)
			&& (comment.querySelector('div[style*="/stamp/"]')
				|| (emoji = comment.querySelector('img[src*="/emoji/"]'))
					&& !emoji.previousSibling && !emoji.nextSibling)) {
			// 返信が存在しない、かつスタンプコメントか絵文字単体のコメントであれば
			if (comment.matches('ul ul li')) {
				// 返信コメントであれば
				const list = comment.parentElement;
				const parentComment = list.closest('li');
				if (comment.parentElement.querySelectorAll('li:not([hidden])').length === 1) {
					// 他に返信が存在しなければ
					list.hidden = true;
					this.hideIfStamp(parentComment);
				} else {
					comment.hidden = true;
				}
				return;
			}

			comment.hidden = true;
		}
	}
}

function main()
{
	document.head.insertAdjacentHTML('beforeend', `<style>
		/*====================================
			グループのコメント欄
		*/
		/*------------------------------------
			区切り線
		*/
		#page-group #timeline li.post div.comment div.post,
		#page-group #timeline li.post div.comment div.post ~ div.post {
			border-top: dashed 1px #DEE0E8;
			border-bottom: none;
		}
		#page-group #timeline li.post div.comment div.post::before {
			content: "";
			position: absolute;
			top: 0;
			left: 0;
			right: 0;
			border-top: dashed 1px #FFFFFF;
		}
		#page-group #timeline li.post div.comment {
			border-bottom: dashed 1px #DEE0E8;
		}
		/*------------------------------------
			最初のコメント
		*/
		#page-group #timeline li.post div.comment > div.post:first-of-type,
		#page-group #timeline li.post div.comment > .tree:first-of-type > div.post:first-of-type,
		#page-group #timeline li.post div.comment > div.post:first-child::before,
		#page-group #timeline li.post div.comment > .tree:first-child > div.post:first-of-type::before {
			/* 一番上のコメント */
			/* 「以前のコメントを見る」ボタンがある時は、白色の破線は消さない */
			border-top: none;
		}
		/*------------------------------------
			返信コメント
		*/
		#page-group #timeline li.post div.comment .tree > :nth-of-type(n+2) {
			margin-left: 2em;
		}
		#page-group #timeline li.post div.comment .tree .postbody {
			width: initial;
		}
	</style>`);

	/**
	 * コメントリスト要素。
	 * @type {HTMLDivElement}
	 */
	const commentList = document.getElementById('timeline');

	/**
	 * {@link MutationObserver#observe}の第2引数に指定するオブジェクト。
	 * @type {MutationObserverInit}
	 */
	const observerOptions = {
		childList: true,
		subtree: true,
	};

	moveAllReplyComments();

	// コメントの増減を監視する
	new MutationObserver(function (mutations, observer) {
		const firstMutationRecord = mutations[0];
		const firstAddedNode = firstMutationRecord.addedNodes[0];
		if (firstAddedNode) {
			// コメントが増えていれば
			// 監視を一旦停止して返信コメントを移動する
			observer.disconnect();
			moveAllReplyComments();
			observer.observe(commentList, observerOptions);
		}
	}).observe(commentList, observerOptions);

	/**
	 * すべての返信コメントを返信先コメントの下に移動します。
	 */
	function moveAllReplyComments()
	{
		const repliedUserNames = document.querySelectorAll('.body > p > a');
		for (let i = repliedUserNames.length - 1; i >= 0; i--) {
			/**
			 * 返信先コメント投稿者名。
			 * @type {HTMLSpanElement}
			 */
			const repliedUserName = repliedUserNames[i];

			/**
			 * 返信先コメント。
			 * @type {?HTMLDivElement}
			 */
			const repliedComment = document.getElementById(repliedUserName.hash.replace('#', ''));

			if (repliedComment) {
				// 返信先コメントが存在すれば
				moveReplyComment(repliedComment, repliedUserName.closest('.post'));
				// 返信先コメント投稿者名を削除
				repliedUserName.parentElement.remove();
			}
		}
	}

	/**
	 * 返信コメントを返信先コメントの下に移動します。
	 * @param {HTMLDivElement} repliedComment - 返信先コメント。
	 * @param {HTMLDivElement} replyComment - 返信コメント。
	 */
	function moveReplyComment(repliedComment, replyComment)
	{
		if (!replyComment.previousElementSibling) {
			const parent = replyComment.parentNode;
			if (parent && parent.classList.contains('tree')) {
				// 返信コメントにラッパー要素が存在すれば
				replyComment = parent;
			}
		}

		/**
		 * 返信先コメントと返信コメントを格納するラッパー要素。
		 * @type {HTMLDivElement}
		 */
		let tree = null;
		if (!repliedComment.previousElementSibling) {
			const parent = repliedComment.parentNode;
			if (parent.classList.contains('tree')) {
				// ラッパー要素がすでに存在すれば
				tree = parent;
			}
		}

		if (!tree) {
			// ラッパー要素が存在しなければ
			// ラッパー要素を作成
			tree = document.createElement('div');
			tree.classList.add('tree');
			// 返信先コメントをラッパー要素に置換
			repliedComment.replaceWith(tree);
			// 返信先コメントをラッパーに追加
			tree.append(repliedComment);
		}

		// 返信コメント移動
		tree.append(replyComment);
	}
}

if (location.pathname.startsWith('/group/')) {
	startScript(
		main,
		parent => parent.id === 'wrapper',
		target => target.id === 'template-drawr-paint-ui',
		() => document.getElementById('template-drawr-paint-ui')
	);
} else {
	document.addEventListener('DOMContentLoaded', function () {
		new StampEraser();
	}, { passive: true, once: true });
}