Last active
February 12, 2023 12:53
-
-
Save lbmaian/e2a60a4aa2c534c1575547a60711613a to your computer and use it in GitHub Desktop.
YouTube - Livechat Emoji Fixes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==UserScript== | |
// @name YouTube - Livechat Emoji Fixes | |
// @namespace https://gist.github.com/lbmaian/e2a60a4aa2c534c1575547a60711613a | |
// @downloadURL https://gist.github.com/lbmaian/e2a60a4aa2c534c1575547a60711613a/raw/youtube-livechat-emoji-fixes.user.js | |
// @updateURL https://gist.github.com/lbmaian/e2a60a4aa2c534c1575547a60711613a/raw/youtube-livechat-emoji-fixes.user.js | |
// @version 0.3 | |
// @description Improves YouTube Livechat emoji performance (and other stuff eventually) | |
// @author lbmaian | |
// @match https://www.youtube.com/live_chat* | |
// @icon https://www.youtube.com/favicon.ico | |
// @run-at document-end | |
// @grant none | |
// ==/UserScript== | |
(function() { | |
'use strict'; | |
const logContext = '[YouTube - Livechat Emoji Fixes]'; | |
function debug(...args) { | |
//console.debug(logContext, ...args); // uncomment to disable debugging | |
} | |
function log(...args) { | |
console.log(logContext, ...args); | |
} | |
function warn(...args) { | |
console.warn(logContext, ...args); | |
} | |
function error(...args) { | |
console.error(logContext, ...args); | |
} | |
function waitForLiveChatMessageInput(callback, ...args) { | |
const eltMessageInput = document.getElementById('live-chat-message-input'); | |
if (eltMessageInput) { | |
callback(eltMessageInput, ...args); | |
} else { | |
new MutationObserver((records, observer) => { | |
const eltMessageInput = document.getElementById('live-chat-message-input'); | |
if (eltMessageInput) { | |
observer.disconnect(); | |
callback(eltMessageInput, ...args); | |
} | |
}).observe(document.body, { | |
childList: true, | |
subtree: true, | |
}); | |
} | |
} | |
function watchEmojiPickers(eltMessageInput) { | |
debug('#live-chat-message-input', eltMessageInput); | |
// Hack to remove the 'When you send a message, people will be able to see that you subscribe to this channel.' one-time tooltip whenever it pops up | |
const eltApp = eltMessageInput.closest('yt-live-chat-app'); | |
debug('yt-live-chat-app', eltApp); | |
new MutationObserver((records, observer) => { | |
for (const record of records) { | |
for (const node of record.addedNodes) { | |
if (node.nodeType === 1 && node.tagName.toLowerCase() === 'tp-yt-iron-dropdown') { | |
debug('found tp-yt-iron-dropdown', node); | |
// Note: the 'When you send a message, people will be able to see that you subscribe to this channel.' hasn't been set yet, | |
// so we can't filter for that, so just filter out any tooltip (afaik, this is the only such tooltip anyway). | |
const eltTooltipRenderer = node.firstElementChild?.firstElementChild; | |
if (eltTooltipRenderer && eltTooltipRenderer.tagName.toLowerCase() === 'yt-tooltip-renderer') { | |
//observer.disconnect(); // not disconnecting in case more tooltips pop up | |
log('removing tooltip', eltTooltipRenderer); | |
node.remove(); | |
} | |
} | |
} | |
} | |
}).observe(eltApp, { | |
childList: true, | |
}); | |
// yt-live-chat-app > div#contents > yt-live-chat-renderer > iron-pages#content-pages > div#chat-messages > div#contents (note: non-unique id) | |
// div#ticker | |
// div#chat | |
// iframe#chatframe | |
// ytd-message-renderer.ytd-live-chat-frame | |
// iron-pages#panel-pages | |
// div#input-panel (message input) | |
// yt-live-chat-message-input-renderer#live-chat-message-input>div#container (always exists?) | |
// div#top > div#input-container > yt-live-chat-text-input-field-renderer#input | |
// div#input (text input; note: non-unique id) | |
// tp-yt-iron-dropdown#dropdown (emoji dropdown when manually typing :...) | |
// iron-pages#pickers>yt-emoji-picker-renderer#emoji (emoji picker) | |
// div#search-panel | |
// div#category-buttons (emoji picker category buttons) | |
// div#categories-wrapper>div#categories (emoji picker categories) | |
// yt-emoji-picker-category-renderer (emoji picker category) | |
// div#buttons | |
// div#picker-buttons>yt-live-chat-icon-toggle-button-renderer#emoji (emoji picker toggle) | |
// div#buy-flow (superchat buying) | |
// yt-live-chat-message-buy-flow-renderer (only exists when buying superchats or milestone chats) | |
// iron-pages>div#preview>div#message>div#pickers-container | |
// iron-pages#pickers>yt-emoji-picker-renderer#emoji (emoji picker - same as above) | |
// div#picker-buttons>yt-live-chat-icon-toggle-button-renderer#emoji (emoji picker toggle - same as above) | |
watchEmojiPicker(eltMessageInput, true); | |
// Superchat emoji picker only exists when div#buy-flow is non-empty (its empty whenever not buying superchats or milestone chats), | |
// so need to watch for when it's added. | |
const eltBuyflow = document.getElementById('buy-flow'); | |
debug('#buy-flow', eltBuyflow); | |
new MutationObserver((records, observer) => { | |
for (const record of records) { | |
for (const node of record.addedNodes) { | |
if (node.nodeType === 1 && node.tagName.toLowerCase() === 'yt-live-chat-message-buy-flow-renderer') { | |
const eltBuyflowRenderer = node; | |
debug('yt-live-chat-message-buy-flow-renderer', eltBuyflowRenderer); | |
watchEmojiPicker(eltBuyflowRenderer, false); | |
return; | |
} | |
} | |
} | |
}).observe(eltBuyflow, { | |
childList: true, | |
}); | |
} | |
function watchEmojiPicker(eltContainer, watchForCategoriesRemoval) { | |
// "categories" id isn't necessarily unique, so not using document.getElementById. | |
const eltCategories = eltContainer.querySelector('#categories'); | |
// If chat is hidden, emoji categories won't be found. | |
if (!eltCategories) { | |
log('#categories not found - assuming chat is hidden'); | |
return; | |
} | |
log('watching #categories', eltCategories, 'in container', eltContainer); | |
// Simply remove emoji categories that contain SVGs. Only members-only and YouTube-specific emojis should remain. | |
new MutationObserver((records, observer) => { | |
for (const record of records) { | |
for (const node of record.addedNodes) { | |
if (node.nodeType === 1) { // element | |
for (const child of node.children) { | |
if (child.id === 'emoji') { | |
const eltEmoji = child.firstElementChild; | |
if (eltEmoji && eltEmoji.src && eltEmoji.src.endsWith('svg')) { | |
log('removing category', node); | |
eltCategories.removeChild(node); | |
break; | |
} | |
} | |
} | |
} | |
} | |
} | |
}).observe(eltCategories, { | |
childList: true, | |
}); | |
if (watchForCategoriesRemoval) { | |
// When user joins membership, #categories is removed and refreshed, so need to rewatch emoji pickers. | |
// Specifically, the #live-chat-message-input container gets replaced within its parent #input-panel. | |
debug('watching for #categories removal up to', eltContainer.parentElement); | |
watchForElementRemoval(eltCategories, () => { | |
log('#categories', eltCategories, 'was removed - assuming it was refreshed'); | |
// Should already be replaced, but if it's somehow not, will wait for it. | |
waitForLiveChatMessageInput(watchEmojiPicker, watchForCategoriesRemoval) | |
}, eltContainer.parentElement); | |
} | |
// Hide the category picker since there's only going to be 1 or 2 emoji categories. Also has non-unique id. | |
const eltCategoryButtons = eltContainer.querySelector('#category-buttons'); | |
if (eltCategoryButtons) { | |
log('removed #category-buttons', eltCategoryButtons); | |
eltCategoryButtons.remove(); | |
} else { | |
log('#category-buttons not found - ignoring'); | |
} | |
} | |
// Unfortunately there's no direct way to watch for a target element being removed. | |
// The most performant way I've found so far is to recursively observe child removals for all the ancestors of the target up to root | |
// (as opposed to observing the whole subtree of the root for removals, which is much more expensive). | |
// When the target element is removed, given callback is called with (target, the ancestor that removed the subtree containing target). | |
// If the root already does not contain the target, logs an error and throws. | |
function watchForElementRemoval(target, callback, root) { | |
if (!root) { | |
root = target.ownerDocument; | |
} | |
if (!root.contains(target)) { | |
error('root', root, 'does not contain target', target); | |
throw new Error('root does not contain target'); | |
} | |
if (root.nodeType === 9) { // document | |
root = root.documentElement; | |
} | |
const observer = new MutationObserver((records, observer) => { | |
// If root is document element, probably faster to check for target.isConnected (assuming that element hasn't been re-added) | |
// but following allows determining what exactly removed the element | |
for (const record of records) { | |
let found = false; | |
for (const node of record.removedNodes) { | |
if (node.contains(target)) { | |
debug('element', target, 'was removed via ancestor', record.target); | |
if (!found) { | |
found = true; | |
observer.disconnect(); | |
debug('all mutation records:', records); | |
} | |
callback(target, record.target); | |
} | |
} | |
} | |
}); | |
const options = { | |
childList: true, | |
}; | |
let element = target.parentNode; // don't observe the target (or rather, its children) itself | |
let end = root.parentNode; // ensure root is observed in following loop | |
while (element !== end) { | |
observer.observe(element, options); | |
element = element.parentNode; | |
} | |
} | |
// Workaround for any iframes that were document.write'd having its location inherit from the calling code's frame | |
// (e.g. if document.write called from either a script in parent frame or extension content script matching parent frame, | |
// then the iframe's location would be the same as parent frame's location). | |
const url = frameElement && frameElement.contentDocument?.URL === parent.document.URL ? frameElement.src || 'about:blank' : document.URL; | |
debug('url:', url); | |
if (url.startsWith('https://www.youtube.com/live_chat')) { | |
waitForLiveChatMessageInput(watchEmojiPickers); | |
} | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment