TwiShell

Enhance Twitter Web with lots of features.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

// ==UserScript==
// @name TwiShell
// @namespace TwiShell
// @description Enhance Twitter Web with lots of features.
// @match http://twitter.com/*
// @match https://twitter.com/*
// @version 3.17
// @run-at document-start
// ==/UserScript==

//noinspection ThisExpressionReferencesGlobalObjectJS
(function (window) {
    'use strict';
    var document = window.document;

    var ELEMENT_NODE = 1,
        TEXT_NODE = 3;

    var click = function (node) {
        var event = document.createEvent("Event");
        event.initEvent("click", true, true);
        node.dispatchEvent(event);
    };

    var hasClass = function (node, cls) {
        return node.classList && node.classList.contains(cls);
    };

    var addClass = function (node, cls) {
        node.classList.add(cls);
    };

    var removeClass = function (node, cls) {
        node.classList.remove(cls);
    };

    var toggleClass = function (node, cls) {
        node.classList.toggle(cls);
    };

    var wantsMoreTimelineItems = function () {
        var event = document.createEvent("Event");
        event.initEvent("uiNearTheBottom", true, true);
        document.dispatchEvent(event);
    };

    // throttle function call in specified interval
    var throttle = function (fn, interval) {
        var fnTimer;
        var repeatCalling = false;
        return function wrapper() {
            if (!fnTimer) {
                fn();
                fnTimer = setTimeout(function () {
                    fnTimer = null;
                    if (repeatCalling) {
                        repeatCalling = false;
                        wrapper();
                    }
                }, interval);
            }
            else {
                repeatCalling = true;
            }
        };
    };

    var globalDialog,
        retweetDialog;

    var getOriginalTweetText = function (tweetNode) {
        var originalTweetText = Array.prototype.reduce.call(
            tweetNode.querySelector(".js-tweet-text").childNodes,
            function (acc, childNode) {
                var text;
                if (childNode.nodeType === TEXT_NODE) {
                    text = childNode.textContent;
                }
                else if (childNode.nodeType === ELEMENT_NODE) {
                    text = childNode.getAttribute("data-expanded-url") || childNode.textContent;
                }
                if (text) {
                    acc.push(text);
                }
                return acc;
            },
            []
        ).join("");
        var screenName = tweetNode.getAttribute("data-screen-name");
        return "RT @" + screenName + ": " + originalTweetText.split("\n").map(function (s) {
                return "<div>" + s + "</div>";
            }).join("");
    };

    var hideRetweetDialog = function () {
        click(document.querySelector("#retweet-tweet-dialog .modal-close"));
    };

    var showGlobalTweetDialog = function () {
        globalDialog.querySelector(".draggable").setAttribute(
            "style",
            retweetDialog.querySelector(".draggable").getAttribute("style")
        );
        click(document.getElementById("global-new-tweet-button"));
        globalDialog.querySelector(".modal-title").innerHTML =
            retweetDialog.querySelector(".modal-title").innerHTML;
    };

    var prepareRT = function (tweetNode) {
        hideRetweetDialog();
        showGlobalTweetDialog();
        var text = getOriginalTweetText(tweetNode);
        fillInTweetBox(text);
    };

    var fillInTweetBox = function (text) {
        var tweetBox = globalDialog.querySelector(".tweet-box"),
            range = document.createRange(),
            selection = window.getSelection();

        tweetBox.innerHTML = text;
        tweetBox.focus();
        // Place cursor in the front.
        range.selectNodeContents(tweetBox);
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
    };

    var replaceCancelButton = function () {
        if (!retweetDialog) { // sometimes it will become null
            return;
        }
        var btnCancel = retweetDialog.querySelector(".cancel-action");
        addClass(btnCancel, "rt-action");
        removeClass(btnCancel, "cancel-action");
        btnCancel.innerHTML = "RT";
        btnCancel.addEventListener("click", function () {
            prepareRT(retweetDialog.querySelector(".tweet"));
        }, false);
    };

    var isAttachedMap = Object.create(null);
    var attachProtectedTweet = function () {
        Array.prototype.forEach.call(document.querySelectorAll(".tweet[data-protected=true]"), function (protectedTweet) {
            var itemId = protectedTweet.getAttribute("data-item-id");
            if (!(isAttachedMap[itemId])) {
                isAttachedMap[itemId] = true;
                var rtBtn = protectedTweet.querySelector(".js-toggleRt button");
                rtBtn.removeAttribute("disabled");
                removeClass(rtBtn, "is-disabled");
                removeClass(rtBtn, "js-disableTweetAction");
                rtBtn.addEventListener("click", function (evt) {
                    if (evt.button == 2) { // Ignore right-clicks
                        return;
                    }
                    evt.stopPropagation();
                    evt.preventDefault();
                    for (var tweet = evt.target.parentNode; !(hasClass(tweet, "tweet")); tweet = tweet.parentNode) {}
                    prepareRT(tweet);
                }, false);
            }
        });
    };

    var tcoMatcher = /^http(?:s)?:\/\/t\.co\/[0-9A-Za-z]+$/i;
    var expandAllUrl = function () {
        Array.prototype.forEach.call(
            document.querySelectorAll("a.twitter-timeline-link:not(.url-expanded)"),
            function (tlLink) {
                var expandedUrl = tlLink.getAttribute("data-expanded-url");
                if (expandedUrl && tcoMatcher.test(tlLink.getAttribute("href"))) {
                    tlLink.setAttribute("href", expandedUrl);
                }
                addClass(tlLink, "url-expanded");
            }
        );
    };

    var removeLangAttr = function () {
        Array.prototype.forEach.call(document.querySelectorAll("p.js-tweet-text[lang]"), function (el) {
            el.removeAttribute("lang");
        });
    };

    document.addEventListener("DOMContentLoaded", function () {
        globalDialog = document.getElementById("global-tweet-dialog");
        retweetDialog = document.getElementById("retweet-tweet-dialog");
        replaceCancelButton();
    }, false);

    var styles = [
        "@media screen {",
        ".content-main, .profile-page-header {float: left !important;}",
        "body.three-col .wrapper {width: 900px !important;}",
        "body.three-col .content-main {min-height: 700px;}",
        "body.three-col .permalink-container {width: 900px !important;}",
        ".dashboard {float: right !important;}",
        "#suggested-users {clear: none !important;}",
        "}"
    ].join("");

    var addStyle = function (css) {
        var node = document.createElement("style");
        node.type = "text/css";
        node.appendChild(document.createTextNode(css));
        document.documentElement.appendChild(node);
        node = null;
    };

    addStyle(styles);

    var throttledExpandUrl = throttle(expandAllUrl, 100);
    var throttledAttachProtectedTweet = throttle(attachProtectedTweet, 100);
    var throttledRemoveLangAttr = throttle(removeLangAttr, 100);
    new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            throttledExpandUrl();
            throttledAttachProtectedTweet();
            throttledRemoveLangAttr();
        });
    }).observe(document, {
            childList: true,
            subtree: true
        });
})(this);