enhance livechat on youtube
// ==UserScript== // @name YouTube LiveChat Enhancer // @namespace http://james0x57.com/ // @version 0.2 // @description enhance livechat on youtube // @author James0x57 // @match https://www.youtube.com/* // @grant none // @run-at document-start // ==/UserScript== (function() { 'use strict'; //## Begin Code I didn't write // credit: https://stackoverflow.com/a/6969486/1527109 function escapeRegExp(str) { return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&"); } //## End Code I didn't write var fcs = function(fn) { //functionalCommentString /*Function By James Atherton - http://geckocodes.org/?hacker=James0x57 */ /*You are free to copy and alter however you'd like so long as you leave the credit intact! =)*/ return fn.toString().replace(/^(\r?\n|[^\/])*\/\*!?(\r?\n)*|\s*\*\/(\r|\n|.)*$/gi,""); }; function addCSS(css) { var el = document.createElement('div'); el.innerHTML = '<b>CSS</b><style type="text/css">' + css + '</style>'; el = el.childNodes[1]; if (el) document.getElementsByTagName('head')[0].appendChild(el); return el; } // https://gist.github.com/James0x57/da84cc2bb6087db5f041387b0a586e6c //## Begin Selector Observation Code var selectors = []; (new MutationObserver( function (mutationsList) { var s, selector, nodeMatches var slen = selectors.length for (s = 0; s < slen; s++) { selector = selectors[s] nodeMatches = node => node.nodeType === 1 && node.matches(selector.childSelector) mutationsList.forEach(mu => { if (mu.type === "childList" && mu.target.matches(selector.parentSelector)) { var addedMatches = Array.prototype.filter.call(mu.addedNodes, nodeMatches) var removedMatches = Array.prototype.filter.call(mu.removedNodes, nodeMatches) addedMatches.length && selector.inserted.call(null, addedMatches) removedMatches.length && selector.removed.call(null, removedMatches) } }) } } )).observe(document.documentElement, { childList: true, subtree: true }) // watch the parentSelector for the specific children to be added or removed, // call inserted as that parentSelector > children are added and they match childSelector // call removed as that parentSelector > children are removed and they match childSelector (note they won't be in the dom any more) // inserted and removed callbacks are called with the matching elements passed in as the only parameter (is an array) var onParentChildSelectors = function (opts) { var nullFn = () => {} selectors.push(Object.assign({ parentSelector: "", childSelector: "", inserted: nullFn, removed: nullFn }, opts)) } // remove all watch selectors: // offSelector({ parentSelector }) -> matching that selector // offSelector({ parentSelector, childSelector }) -> matching that selector // offSelector({ parentSelector, inserted }) -> matching that parentSelector && matching that inserted function // offSelector({ parentSelector, childSelector, inserted }) -> matching that selector && matching that inserted function // offSelector({ parentSelector, childSelector, removed }) -> matching that selector && matching that removed function // offSelector({ parentSelector, childSelector, inserted, removed }) -> matching that selector, inserted function, and removed function var offSelector = function (opts) { for (let s = 0; s < selectors.length; s++) { let selectorObj = selectors[s] let comp = Object.assign({}, selectorObj, opts) let matchingProps = ["parentSelector", "childSelector", "inserted", "removed"].filter(prop => selectorObj[prop] === comp[prop]) if (matchingProps.length === 4) { selectors.splice(s, 1) s-- } } } //## End Selector Observation Code addCSS(fcs(function() {/*! @keyframes jca_highlight { 0% { background: #88ccff; } 100% { background: transparent; } } .jca-jump-highlight { animation: jca_highlight 2s; } .jca-user-ref { background-color: rgba(128, 128, 128, 0.15); cursor: pointer; } .jca-user-ref:before { content: "@"; } */})) var userInfo = {} var userRefClickFn = function () { var userRefSpan = this var referencedMessageEl = document.querySelector('yt-live-chat-renderer #chat #items #' + userRefSpan.getAttribute("data-last-id")) if (referencedMessageEl) { referencedMessageEl.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }) referencedMessageEl.classList.add("jca-jump-highlight") setTimeout(() => referencedMessageEl.classList.remove("jca-jump-highlight"), 1000) } } var createSpanAndReferenceUsers = function (text) { var span = document.createElement("span") // sort with longer names first because if somebody's name is a subset of another user's. eg "Amber S" and "Amber" then we'll get the right one var users = Object.keys(userInfo).sort((a, b) => b.length - a.length || a.localeCompare(b)); users.forEach(user => { let info = userInfo[user] text = text.replace(info.userNameRx, `<span class="jca-user-ref" data-last-id="${info.escapedId}">${user}</span>`) }) span.className = "jca-text-node-into-span" span.innerHTML = text span.querySelectorAll(".jca-user-ref").forEach(function (userSpan) { userSpan.addEventListener("click", userRefClickFn.bind(userSpan), false) }) return span } var decorateMessage = function (messageEl) { // console.log(messageEl.textContent) var messageParts = messageEl.childNodes || [] messageParts.forEach(node => { if (node.nodeType === 3) { // textNode let text = node.textContent let hasUserRef = /@/.test(text) if (hasUserRef) { let span = createSpanAndReferenceUsers(text) messageEl.replaceChild(span, node) } } }) } var handleNewChat = function (chatEl) { var escapedId = chatEl.id.replace(/%/g, "\\%") var user = chatEl.querySelector("#author-name").textContent.trim() var messageEl = chatEl.querySelector("#message") var message = messageEl && messageEl.textContent.trim() userInfo[user] = { user, userNameRx: new RegExp("@" + escapeRegExp(user) + "\\b", "gi"), lastId: chatEl.id, escapedId, lastMessage: message } messageEl && decorateMessage(messageEl) } onParentChildSelectors({ parentSelector: "yt-live-chat-renderer #chat #items", childSelector: "yt-live-chat-text-message-renderer, yt-live-chat-paid-message-renderer", inserted: addedChatEls => addedChatEls.forEach(handleNewChat) // removed }) })()