Filters messages in YouTube stream chat.
当前为
// ==UserScript==
// @name YouTube Chat Filter
// @namespace https://greasyfork.org/users/696211-ctl2
// @version 0.1
// @description Filters messages in YouTube stream chat.
// @author Callum Latham
// @match *://www.youtube.com/*
// @match *://youtube.com/*
// @grant none
// ==/UserScript==
const CONFIG = {
/**
* A higher number means more message space.
* A value of 1 will make the chat area as tall as the chat iframe's body.
*/
'CHAT_HEIGHT': 2
};
const FILTER = [
{
'streamer': /^/,
'author': /$./,
// Filters out non-Japanese messages (allows 'w' (笑))
'message': /[abcdefghijklmnopqrstuvxyz]/i,
'requireBadge': true,
'queueTime': 1000,
'stopOnHover': true
}
];
(() => {
if (window.frameElement.id !== 'chatframe') {
return;
}
(function style() {
const addStyle = (sheet, selector, rules) => {
const ruleString = rules.map(
([selector, rule]) => `${selector}:${typeof rule === 'function' ? rule() : rule} !important;`
);
sheet.insertRule(`${selector}{${ruleString.join('')}}`);
};
const styleElement = document.createElement('style');
const {sheet} = document.head.appendChild(styleElement);
const styles = [
['#item-offset', [
['height', `${document.body.clientHeight * CONFIG.CHAT_HEIGHT}px`]
]],
['#items:not(.cf)', [
['display', 'none']
]],
['#items.cf > :nth-child(even)', [
['background-color', '#1f1f1f']
]]
];
for (const style of styles) {
addStyle(sheet, style[0], style[1]);
}
})();
window.onload = async () => {
const filter = (() => {
const streamer = parent.document.querySelector('#meta').querySelector('#channel-name').innerText;
for (const {'streamer': regex, ...filter} of FILTER) {
if (regex.test(streamer)) {
return filter;
}
}
})();
// Terminate if there's no valid filter
if (!filter) {
return;
}
const chatElements = {'held': document.body.querySelector('#chat').querySelector('#items')};
chatElements.accepted = chatElements.held.cloneNode(false);
chatElements.accepted.classList.add('cf');
chatElements.held.parentElement.appendChild(chatElements.accepted);
let queuedPost;
let doQueue = false;
let hovered = false;
function trimPosts() {
const chat = chatElements.accepted;
const container = chat.parentElement;
const {top} = container.getBoundingClientRect();
for (let topPost = chat.firstChild; topPost.getBoundingClientRect().top - top < 0; topPost = chat.firstChild) {
topPost.remove();
}
}
function showPost(post) {
const container = chatElements.accepted;
container.appendChild(post);
// Save memory by deleting passed posts
trimPosts();
queuedPost = undefined;
if (filter.queueTime > 0) {
// Start queueing
doQueue = true;
window.setTimeout(() => {
doQueue = false;
// Unqueue
acceptPost();
}, filter.queueTime);
}
}
function acceptPost(post = queuedPost) {
if (!post) {
return;
}
if (doQueue || (filter.stopOnHover && hovered)) {
queuedPost = post;
} else {
showPost(post);
}
}
window.document.body.addEventListener('mouseenter', () => {
hovered = true;
});
window.document.body.addEventListener('mouseleave', () => {
hovered = false;
/** Unqueue iff:
* - Nothing was queued at the most recent unqueue
* - No posts have been shown since the last unqueue
* - A post is queued
*/
acceptPost();
});
function processPost(post) {
chatElements.held.parentElement.style.removeProperty('height');
try {
if (
filter.author.test(post.querySelector('#author-name').textContent) ||
filter.message.test(post.querySelector('#message').textContent) ||
(filter.requireBadge && !post.querySelector('#chat-badges').hasChildNodes())
) {
// Save memory by deleting rejected posts
post.remove();
} else {
acceptPost(post);
}
} catch (e) {
// console.group('STRANGE POST');
// console.warn(post);
// console.warn(e);
// console.groupEnd();
}
}
new MutationObserver((mutations) => {
for (const {addedNodes} of mutations) {
addedNodes.forEach(processPost);
}
}).observe(
chatElements.held,
{childList: true}
);
};
})();