Hamster Kombat Desktop

Allow Hamster Kombat to run on Telegram Web and optionally open it in a new tab.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name        Hamster Kombat Desktop
// @namespace   https://violentmonkey.github.io
// @version     1.1.0
// @description Allow Hamster Kombat to run on Telegram Web and optionally open it in a new tab.
// @author      Teraskull
// @match       https://web.telegram.org/*
// @license     GPLv3
// @grant       none
// @run-at      document-end
// ==/UserScript==

/**
 * @typedef {Object} FragmentObject
 * @property {string} tgWebAppData - The authentication data for the Telegram web app.
 * @property {("android"|"android_x"|"ios")} tgWebAppPlatform - The current platform of the Telegram web app.
 * @property {string} tgWebAppThemeParams - The theme parameters for the Telegram web app.
 * @property {string} tgWebAppVersion - The version of the Telegram web app.
 */

(() => {
    "use strict";

    /**
     * Parse the URL fragment into an object.
     * @param {string} fragment - The URL fragment.
     * @returns {FragmentObject} The parsed fragment object.
     */
    const parseFragment = (fragment) => Object.fromEntries(new URLSearchParams(fragment).entries());

    /**
     * Serialize the object into a URL fragment string.
     * @param {FragmentObject} obj - The object to serialize.
     * @returns {string} The serialized fragment string.
     */
    const serializeFragment = (obj) => new URLSearchParams(obj).toString();

    /**
     * Get the icon content from the icon code.
     * @param {string} icon - The icon code.
     * @returns {string} The icon content.
     */
    const getIconContent = (icon) => String.fromCharCode(Number.parseInt(icon, 16));

    /**
     * Handle adding the menu item for version A.
     * @param {URL} url - The URL to be opened in a new tab.
     */
    const addMenuItemVersionA = (url) => {
        const menu = document.querySelector(".modal-header .menu-container");
        if (!menu || menu.querySelector(".new-menu-item")) return;

        const menuItem = `
            <div class="MenuItem compact new-menu-item" role="menuitem" tabindex="0">
                <i class="icon icon-open-in-new-tab"></i>
                Open in New Tab
            </div>
        `;

        menu.insertAdjacentHTML("beforeend", menuItem);

        menu.lastElementChild.addEventListener("click", () => window.open(url.href, "_blank"));
    };

    /**
     * Handle adding the menu item for version K.
     * @param {URL} url - The URL to be opened in a new tab.
     */
    const addMenuItemVersionK = (url) => {
        const menuToggleButton = document.querySelector(".popup-header .btn-icon.rp.btn-menu-toggle");
        if (!menuToggleButton) return;

        // Add menu item on menu icon click because the dropdown is dynamically created.
        menuToggleButton.addEventListener("click", () => {
            const menu = document.querySelector(".popup-header .btn-menu");
            if (!menu || menu.querySelector(".new-menu-item")) return;

            const menuItem = `
                <div class="btn-menu-item rp-overflow new-menu-item">
                    <span class="tgico btn-menu-item-icon">${getIconContent("e9a3")}</span>
                    <span class="i18n btn-menu-item-text">Open in New Tab</span>
                </div>
            `;

            menu.insertAdjacentHTML("beforeend", menuItem);

            menu.lastElementChild.addEventListener("click", () => window.open(url.href, "_blank"));
        });
    };

    /**
     * Add a new menu item to open the iframe URL in a new tab.
     * @param {URL} url - The URL to open in a new tab.
     */
    const addOpenInNewTabMenuItem = (url) => {
        if (location.pathname.startsWith("/a/")) {
            addMenuItemVersionA(url);
        } else if (location.pathname.startsWith("/k/")) {
            addMenuItemVersionK(url);
        }
    };

    /**
     * Manipulate the iframe src to allow running the web app on Telegram Web.
     * @param {HTMLIFrameElement} iframe - The iframe element.
     */
    const handleIframe = (iframe) => {
        if (!iframe) return;

        const url = new URL(iframe.src);
        const fragmentObject = parseFragment(url.hash.substring(1));
        fragmentObject.tgWebAppPlatform = "android";
        delete fragmentObject.tgWebAppThemeParams; // Remove unnecessary parameter.

        url.hash = serializeFragment(fragmentObject);
        iframe.src = url.href;

        addOpenInNewTabMenuItem(url);
    };

    /**
     * Handle mutations and process the iframe for the web app popup.
     * @param {MutationRecord[]} mutations - List of mutations observed.
     */
    const mutationCallback = (mutations) => {
        for (const { addedNodes, target } of mutations) {
            if (addedNodes.length !== 1) continue;

            if (target.id === "portals" || addedNodes[0].classList.contains("popup-web-app")) {
                const iframe = target.querySelector("iframe");
                handleIframe(iframe);
            }
        }
    };

    // Set up MutationObserver to monitor for relevant changes.
    const observer = new MutationObserver(mutationCallback);
    const parentElement = document.getElementById("portals") || document.body; // Get the parent element for A or K.

    if (parentElement) {
        observer.observe(parentElement, { childList: true });
    }
})();