Twitter media-only filter toggle.

Toggle non-media tweets on and off on the home timeline, for the power-viewer!

As of 2024-09-03. See the latest version.

// ==UserScript==
// @name         Twitter media-only filter toggle.
// @version      0.14
// @description  Toggle non-media tweets on and off on the home timeline, for the power-viewer!
// @author       Cro
// @match        https://*.twitter.com/*
// @match        https://*.x.com/*
// @run-at       document-idle
// @grant        GM_setValue
// @grant        GM_getValue
// @namespace https://greasyfork.org/users/10865
// @icon         https://www.google.com/s2/favicons?domain=twitter.com
// @license      MIT
// ==/UserScript==
/* jshint esversion: 6 */

(function() {
    'use strict';
    let storage_key = "cro-media-toggle";
    let show_all = GM_getValue(storage_key);

    let create_ui = function(target)
    {
        let button = document.createElement("button");
        button.innerText = show_all ? "Showing all home tweets" : "Showing only media home tweets";

        button.onclick = function(event)
        {
            show_all = !show_all;
            GM_setValue(storage_key, show_all);
            location.reload();
        };

        target.prepend(button);
    };

    let find_objects_at_keys = function(obj, keys)
    {
        let found = [];
        let stack = Object.entries(obj);
        while (stack.length > 0)
        {
            let current = stack.pop();
            if (keys.includes(current[0]))
            {
                found.push(current[1]);
            }
            if (current[1] != null && typeof(current[1]) == 'object')
            {
                stack = stack.concat(Object.entries(current[1]));
            }
        }
        return found;
    };

    let has_media = function(obj)
    {
        if (obj.entryId.contains("tweet"))
        {
            return obj.content.itemContent.tweet_results.result.legacy.entities.hasOwnProperty('media');
        }
        return true;
    };

    let update_data = function(data)
    {
        if (show_all || location.pathname != '/home')
        {
            return;
        }
        for (let obj of find_objects_at_keys(data, ['instructions']))
        {
            for (let subobj of obj)
            {
                subobj.entries = subobj.entries.filter(has_media);
            }
        };
    };

    // Intercept JSON parses to alter the sensitive media data.
    let old_parse = unsafeWindow.JSON.parse;
    let new_parse = function(string)
    {
        let data = old_parse(string);
        try
        {
            if (data != null)
            {
                update_data(data);
            }
        }
        catch(error)
        {
            console.log(error);
        }
        return data;
    };
    exportFunction(new_parse, unsafeWindow.JSON, { defineAs: "parse" });

    // Wait for twitter's react crap finish loading things.
    let scan_interval = setInterval(function()
    {
        let target = document.body.querySelector("h1[role='heading']");
        if (target)
        {
            clearInterval(scan_interval);
            create_ui(target);
        }
    }, 10);
})();