Twitter Image Expander

Prevents images in the twitter feed from having the top and bottom cropped off

// ==UserScript==
// @name         Twitter Image Expander
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Prevents images in the twitter feed from having the top and bottom cropped off
// @author       Ambit
// @match        https://twitter.com/*
// @icon         https://www.google.com/s2/favicons?domain=twitter.com
// @grant        none
// @licnse       ISC; https://opensource.org/licenses/ISC
// ==/UserScript==

// Copyright 2021 Ambit
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
// FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

(function() {
    'use strict';
    const enableDebugLogging = false;
    // Width to use for resizing when the container has no computed size yet
    const defaultWidth = 505;

    console.info("Twitter Image Expander loaded");

    function logat(level, ...args) {
        if (level == "debug" && !enableDebugLogging) return;
        console[level]("twimg-expand:", ...args);
    }

    function debug(...args) { logat("debug", ...args); }
    function warn(...args) { logat("warn", ...args); }

    var globalStyle = document.createElement("style");
    globalStyle.type = "text/css";
    document.head.appendChild(globalStyle);
    globalStyle.sheet.insertRule(`
        [data-testid="tweetPhoto"] {
            margin: 0px !important;
        }`);
    globalStyle.sheet.insertRule(`
        [data-testid="tweetPhoto"] >div {
            background-size: contain !important;
        }`);

    function fixImageContainer(container, image) {
        debug(`h: ${image.naturalHeight}, w: ${image.naturalWidth}, container:`, container);
        if (image.naturalHeight == 0 || image.naturalWidth == 0) return;
        var child = container.querySelector(".r-1adg3ll.r-13qz1uu");
        if (!child) {
            warn("image viewport controlling container not found, skipping");
            return;
        }
        var ratio = (container.clientWidth || defaultWidth) / image.naturalWidth
        var height = image.naturalHeight * ratio;
        debug(`setting height to ${height}px based on ratio ${ratio} for element`, child);
        child.style.paddingBottom = `${height}px`;
    }

    function isPhotoDiv(node) {
        return node && node.localName == "div" && node.getAttribute("data-testid") == "tweetPhoto";
    }

    function fixNode(node) {
        if (node.localName == "img"
            && isPhotoDiv(node.parentElement)
            && node.parentElement.parentElement
            && node.parentElement.parentElement.parentElement) {
            var container = node.parentElement.parentElement.parentElement;

            debug("attaching image fixer");
            node.addEventListener("load", function(event) {
                fixImageContainer(container, node);
            });

            if (node.complete) {
                fixImageContainer(container, node);
            }
        }
    }

    debug("fixing initial nodes");
    var nodes = document.body.getElementsByTagName("img");
    for (var i = 0; i < nodes.length; i++) {
        fixNode(nodes[i]);
    }

    var observer = new MutationObserver(mutationList => {
        mutationList.forEach(mutation => {
            mutation.addedNodes.forEach(fixNode);
        });
    });

    debug("watching for new image posts");
    observer.observe(document.body, {childList: true, subtree: true});
})();