Greasy Fork is available in English.

Tweetdeck tweaks

Customizes my own Tweetdeck experience. It's unlikely someone else will enjoy this.

// ==UserScript==
// @name         Tweetdeck tweaks
// @namespace    http://tampermonkey.net/
// @description  Customizes my own Tweetdeck experience. It's unlikely someone else will enjoy this.
// @copyright    WTFPL
// @source       https://github.com/B1773rm4n/Tweetdeck_Greasemonkey
// @version      1.10.0
// @author       B1773rm4n
// @match        https://*.twitter.com/*
// @connect      asuka-shikinami.club
// @icon         https://icons.duckduckgo.com/ip2/twitter.com.ico
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @run-at       document-idle
// ==/UserScript==

let arrayListNames;
let leftColumnNode, rightColumnNode
let postAlreadyseen = [];

////// Flow Control //////

(async function start() {

    arrayListNames = await returnNamesFromServer()

    // wait until the page is sufficiently loaded
    let waitThreeSecs = new Promise((resolve) => setTimeout(resolve, 3000))
    await waitThreeSecs

    if (document.URL.indexOf('https://twitter.com/') > -1) {

        await showInListTwitter()

        // watch for changes
        watchDomChangesObserver()

    } else if (document.URL.indexOf('https://tweetdeck.twitter.com/' > -1)) {
        // check if a new element is loaded and do something
        observeTimelineForNewPosts()

        // general css changes
        addStyles()

        // observer for the fullscreen picture improvements
        fullScreenModal()

        // remove unused panels (uBlock origin)
        removePanels()

        // initate localStorage array for seenPosts
        loadLocalStorage()

        doTweetdeckActions()
    } else {
        console.log('cant find domain')
    }

})();

function doTweetdeckActions(newNode) {
    styleNameOfPost(newNode)
    removeShowThisthreadTweetdeck(newNode)
    removeRetweetedTweetdeck(newNode)
}

async function runWhenReady(readySelector) {
    return new Promise((resolve, reject) => {
        var numAttempts = 0;
        var tryNow = function () {
            var elem = document.querySelector(readySelector);
            if (elem) {
                resolve(elem)
            } else {
                numAttempts++;
                if (numAttempts >= 20) {
                    let message = 'Giving up after 20 attempts. Could not find: ' + readySelector
                    console.warn(message);
                    reject(message)
                } else {
                    setTimeout(tryNow, 250 * Math.pow(1.1, numAttempts));
                }
            }
        };
        tryNow();
    })
}


//// Observers /////

function observeTimelineForNewPosts() {

    [leftColumnNode, rightColumnNode] = document.getElementsByClassName("js-column");
    const config = { attributes: false, childList: true, subtree: true };

    const callback = (mutations) => {

        mutations.forEach((element) => {
            element.addedNodes.forEach((newNode) => {

                let isNewTweet = newNode.getAttribute("data-drag-type") == "tweet"
                if (isNewTweet) {
                    console.log(getUserNameFromNode(newNode))
                    doTweetdeckActions(newNode)
                    sendPostToServer(newNode)
                }

            });

        });

    }

    const observer = new MutationObserver(callback);

    observer.observe(leftColumnNode, config);
    observer.observe(rightColumnNode, config);

}

function fullScreenModal() {
    const targetNode = document.getElementById('open-modal');

    const config = { attributes: true, childList: false, subtree: false, attributeFilter: ['style'] };

    const callback = function (mutationsList, observer) {
        for (const mutation of mutationsList) {
            if (mutation.type === 'attributes') {
                // Check if an image is opened
                if (document.getElementsByClassName('med-tray js-mediaembed').length > 0 && document.getElementsByClassName('med-tray js-mediaembed')[0].hasChildNodes()) {
                    // make the whole image area as clickable as you would click on the small x
                    document.getElementsByClassName('js-modal-panel mdl s-full med-fullpanel')[0].onclick = function () { document.getElementsByClassName('mdl-dismiss')[0].click() }

                    // remove unecessary elements
                    // view original under the picture modal
                    document.getElementsByClassName('med-origlink')[0].remove()
                    // view flag media under the picture modal
                    document.getElementsByClassName('med-flaglink')[0].remove()
                }
            }
        }
    };

    const observer = new MutationObserver(callback);

    observer.observe(targetNode, config);

}

function watchDomChangesObserver() {

    let currentLocation = document.location.href

    const domTreeElementToObserve = document.getElementsByTagName('main')[0]
    const config = { attributes: false, childList: true, subtree: true };

    const observer = new MutationObserver((mutationList) => {
        if (currentLocation !== document.location.href) {
            // location changed!
            currentLocation = document.location.href;

            console.log('location changed!');
            showInListTwitter()
        }
    });

    observer.observe(domTreeElementToObserve, config);

}

////// doTweetdeckActions //////

function styleNameOfPost(newNode) {

    if (newNode) {
        // clear only new element

        let element = newNode.querySelectorAll(".username")[0]

        // cut the name field so the name_id can be seen always
        let nameField = element.previousSibling.previousSibling
        nameField.style.display = 'inherit'
        nameField.style.width = '120px'
        nameField.style.overflow = 'clip'

        // color the name_id field if already in list or not
        let currentlyDisplayedElementName = element.innerHTML
        let inNameInList = arrayListNames.includes(currentlyDisplayedElementName)
        if (inNameInList) {
            element.style.color = "green"
        } else {
            element.style.color = "red"
        }
    } else {
        // color whole screen
        let usernameArray = document.getElementsByClassName('username')

        for (let index = 1; index < usernameArray.length; index++) {
            let element = usernameArray[index];

            // cut the name field so the name_id can be seen always
            let nameField = element.previousSibling.previousSibling
            nameField.style.display = 'inherit'
            nameField.style.width = '120px'
            nameField.style.overflow = 'clip'

            // color the name_id field if already in list or not
            let currentlyDisplayedElementName = element.innerHTML
            let inNameInList = arrayListNames.includes(currentlyDisplayedElementName)
            if (inNameInList) {
                element.style.color = "green"
            } else {
                element.style.color = "red"
            }
        }
    }

}


function removeShowThisthreadTweetdeck(newNode) {
    if (newNode) {
        // clear only new element
        newNode.querySelector('.js-show-this-thread').remove()
    } else {
        // clear whole screen
        let list = document.getElementsByClassName('js-show-this-thread')

        for (let index = 1; index < list.length; index++) {
            let element = list[index];
            element.remove()
        }
    }

}

function removeRetweetedTweetdeck(newNode) {
    if (newNode) {
        // todo fix single remove retweeted
        // clear only new element
        let element = newNode.querySelector('.nbfc')
        if (!element.classList.length == 4) {

            // remove retweeted word
            element.childNodes[2].remove()

            // remove self retweet mention
            let accountName = element.parentNode.nextElementSibling.firstElementChild?.children[1].firstElementChild.firstElementChild.innerText

            if (accountName == element.innerText) {
                element.remove()
            }
        }
    } else {
        // clear whole screen
        let retweetList = document.getElementsByClassName('tweet-context')

        for (let index = 1; index < retweetList.length; index++) {
            let element = retweetList[index];
            element.childNodes[3].childNodes[2].remove()
        }

        // TODO reimplement self retweet mention removal
    }
}


////// External API Call Functions //////

function returnNamesFromServer() {

    return new Promise((resolve, reject) => GM_xmlhttpRequest({
        method: "GET",
        url: "https://api.seele-00.asuka-shikinami.club/artists",
        onload: function (response) {
            let artistsArray = response.responseText.split("\n")
            resolve(artistsArray)
        },
        onerror: reject
    }));
}

function sendPostToServer(newNode) {
    // - check if it was scanned already
    // - if already known / scanned -> discard
    // - if new -> send curl with image url

    // select from the column root to the individual post (40 elements as result)
    let rightColumn = document.getElementsByClassName("js-app-columns app-columns horizontal-flow-container without-tweet-drag-handles")[0].children[1]

    if (rightColumn.contains(newNode)) {

        let username = getUserNameFromNode(newNode)

        // check if the artist is in the list
        let isUsernameInList = arrayListNames.includes(username)

        // check if we already processed this post
        let isPostAlreadyseen = isPostAlreadyProcessed(newNode)

        if (isUsernameInList && !isPostAlreadyseen) {
            // check amount of images
            let images = getImageUrlsFromNode(newNode)

            images.forEach(element => {

                // if new -> send curl with image url
                GM_xmlhttpRequest({
                    method: "POST",
                    url: "http://api.seele-00.asuka-shikinami.club/imageurl",
                    data: element,
                    onload: function (response) {
                        console.log(response.responseText);
                        console.log(username + " " + isUsernameInList);

                        addPostToAlreadyProcessedList(newNode)
                    }
                });

            });

        } else {
            // - if already known / scanned -> discard
        }

    }

}


////// Single Action Functions //////

function isPostAlreadyProcessed(newNode) {
    let tweetId = newNode.getAttribute("data-tweet-id")
    return postAlreadyseen.includes(tweetId)
}

function addPostToAlreadyProcessedList(newNode) {
    let tweetId = newNode.getAttribute("data-tweet-id")

    postAlreadyseen.push(tweetId)
    postAlreadyseen = [...new Set(postAlreadyseen)];
    postAlreadyseen = postAlreadyseen.slice(-100)
    let persistPosts = JSON.stringify(postAlreadyseen)
    GM_setValue("postAlreadyseen", persistPosts);
}

async function showInListTwitter() {

    // This colors the text of the artist in the timeline into red when he isn't in the known artist list

    if (document.URL.indexOf('https://twitter.com/') > -1) {
        let nameElement
        if (window.location.href.indexOf('status') > 0) {
            let nameElementTemp = await runWhenReady("div[data-testid='User-Name']")
            nameElement = nameElementTemp.children[1]?.firstChild?.firstChild?.firstChild?.firstChild?.firstChild
        } else {
            let nameElementTemp = await runWhenReady("div[data-testid='UserName']")
            nameElement = nameElementTemp?.firstChild?.firstChild?.children[1]?.firstChild?.firstChild?.firstChild?.firstChild
        }

        let currentlyDisplayedElementName = nameElement.textContent
        let inNameInList = arrayListNames.includes(currentlyDisplayedElementName)

        if (inNameInList) {
            nameElement.style.color = "green"
        } else {
            nameElement.style.color = "red"
        }
    }
}

function getImageUrlsFromNode(node) {

    var images = []
    let imageraws = node.querySelectorAll(".js-media-image-link")

    imageraws.forEach((element) => {
        let image = element.style.getPropertyValue('background-image')
        images.push(image.substr(5, image.length - 7))
    });

    if (images.length > 0) {
        return images
    } else {
        alert("No getImageUrlsFromNode")
    }

}

function removePanels() {
    document.getElementsByClassName("js-column-header js-action-header flex-shrink--0 column-header")[0].remove()
    document.getElementsByClassName("js-column-header js-action-header flex-shrink--0 column-header")[0].remove()

    document.getElementsByClassName("js-column-message scroll-none")[0].parentElement.remove()
    document.getElementsByClassName("js-column-message scroll-none")[0].parentElement.remove()
}

function loadLocalStorage() {
    // initate localStorage array for seenPosts
    let postAlreadyseenString = GM_getValue("postAlreadyseen")
    if (postAlreadyseenString) {
        let postAlreadyseenJson = JSON.parse(postAlreadyseenString)
        postAlreadyseen = Array.from(postAlreadyseenJson)
    }
}

////// Helper Functions //////

function getAllTweetNodes() {
    return document.getElementsByTagName("article")
}

function getLeftColumnTweetNodes() {
    let leftColumnTweetNodes = []
    let allTweetNodes = getAllTweetNodes()
    for (let i = 0; i < allTweetNodes.length; i++) {
        const element = allTweetNodes[i];
        if (leftColumnNode.contains(element)) {
            leftColumnTweetNodes.push(element)
        }
    }
    return leftColumnTweetNodes
}

function getRightColumnTweetNodes() {
    let rightColumnTweetNodes = []
    let allTweetNodes = getAllTweetNodes()
    for (let i = 0; i < allTweetNodes.length; i++) {
        const element = allTweetNodes[i];
        if (rightColumnNode.contains(element)) {
            rightColumnTweetNodes.push(element)
        }
    }
    return rightColumnTweetNodes
}

function isInLeftColumn(node) {
    return leftColumnNode.contains(node)
}

function isInRightColumn(node) {
    return rightColumnNode.contains(node)
}

function getUserNameFromNode(node) {
    return node.querySelector(".username").innerText
}

function getUserIdFromNode(node) {
    return node.querySelector(".username").previousSibling.previousSibling.innerText
}

////// CSS Stylesheets //////

function addStyles() {
    'use strict';

    GM_addStyle(`
    .med-fullpanel {
    background-color: transparent !important;
    box-shadow: 0 !important;
        }
` );

    GM_addStyle(`
    html.dark .mdl {
    background-color: transparent !important;
    box-shadow: none !important;
    border-radius: 0 !important;
        }
` );


    GM_addStyle(`
    html.dark .is-condensed .app-content {
    left: 0px
        }
` );

    GM_addStyle(`
    .overlay, .ovl {
    background: transparent !important;
        }
` );


    GM_addStyle(`
    .mdl-dismiss {
    visibility: hidden !important;
        }
` );

    GM_addStyle(`
    .med-tweet {
    background-color: rgb(21, 32, 43) !important;
        }
` );

    GM_addStyle(`
    .js-app-columns .app-columns .horizontal-flow-container .without-tweet-drag-handles {
        padding-left: 0px !important;
        }
` );

    GM_addStyle(`
    .app-columns {
        padding-left: 0px !important;
        padding: 0px !important;
        }
` );

}