Tg Sort by reactions

Allows you to find posts with a lot of reactions (likes) in Telegram Web A.

// ==UserScript==
// @name         Tg Sort by reactions
// @name:ru      Tg Сортировка по реакциям
// @name:zh      Tg 按反应排序
// @version      0.1.5
// @description  Allows you to find posts with a lot of reactions (likes) in Telegram Web A.
// @description:ru Позволяет найти сообщения с наибольшим количеством реакций (лайков) в Телеграм Web A.
// @description:zh 允许您在电报 Web A 中找到有很多反应(喜欢)的消息。
// @author       sokollondon
// @match        https://web.telegram.org/a/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=telegram.org
// @require      http://code.jquery.com/jquery-3.3.1.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery-scrollTo/2.1.3/jquery.scrollTo.min.js
// @grant        none
// @license      MIT // <-- Лицензия добавлена здесь!
// @namespace    https://gist.github.com/sokollondon/4be0d13f33a371895308ed7b1dc15fcf
// ==/UserScript==

(function() {
    'use strict';

    // Ensure jQuery is loaded and available
    if (typeof jQuery === 'undefined') {
        console.error('Tg Sort by Reactions: jQuery is not loaded.');
        return;
    }

    const $ = jQuery.noConflict(true); // Use noConflict to avoid conflicts

    console.log('Tg Sort by Reactions: Script initialized.');

    // --- Styles for the Sort Button ---
    const style = `
        #tg-sort-reactions-btn {
            position: fixed;
            top: 73px;
            right: 16px;
            width: 45px;
            height: 45px;
            padding: 5px;
            border-radius: 10px;
            text-align: center;
            font-size: 20px;
            padding-top: 5px;
            z-index: 2000; /* Increased z-index to ensure visibility */
            opacity: 0.7;
            background: url(https://cdn0.iconfinder.com/data/icons/font-awesome-solid-vol-4/512/sort-amount-down-alt-64.png) no-repeat center center;
            background-size: 32px;
            background-color: var(--tg-theme-background-color, #fff); /* Use Telegram theme variable */
            border: 1px solid var(--tg-theme-border-color, #e0e0e0); /* Add border */
            box-shadow: 0 2px 5px rgba(0,0,0,0.2); /* Add subtle shadow */
            cursor: pointer;
            transition: opacity 0.2s ease-in-out, background-color 0.2s ease-in-out;
        }
        #tg-sort-reactions-btn:hover {
            opacity: 1;
        }
        .theme-dark #tg-sort-reactions-btn {
            background-color: var(--tg-theme-dark-background-color, #a9a9a9); /* Dark theme adjustments */
            border-color: var(--tg-theme-dark-border-color, #606060);
        }
        @media screen and (max-width: 600px) {
            #tg-sort-reactions-btn {
                top: 111px;
                right: 8px;
            }
        }
    `;

    $('head').append('<style id="tg-sort-reactions-style">' + style + '</style>');

    // --- Create and Append the Sort Button ---
    const sortButtonHtml = "<div id='tg-sort-reactions-btn' title='Sort by reactions count'></div>";
    $('body').prepend(sortButtonHtml);
    console.log('Tg Sort by Reactions: Sort button added to DOM.');

    // --- Helper Function to Get Reaction Count ---
    function getReactionCount($messageElement) {
        // In Telegram Web A, reactions are typically in a div with role="button" inside a reaction container.
        // The count is often within a span or element with specific class.
        // Look for elements that represent reactions. Common classes might include:
        // .Reactions is from older webk. Now it's something like .reaction-button or a span.
        // The reaction counts are usually numeric text within a span.

        let totalReactions = 0;
        // Find reaction containers within the message bubble.
        // This selector targets the element that usually wraps all reaction buttons.
        // It's usually a div with a role="button" and aria-label containing the count.
        const $reactionContainer = $messageElement.find('[data-list="reactions-list"]');

        if ($reactionContainer.length) {
            // Find individual reaction buttons/spans within the container
            // The structure is often: <div class="reactions-box"> <button> 👍<span>23</span> </button> </div>
            // Or directly within elements with text content.
            $reactionContainer.find('span[class*="text"], div[aria-label$="reactions"]').each(function() {
                let text = $(this).text().trim();
                let count = parseReactionNumber(text);
                totalReactions += count;
            });

            // Fallback: If no specific spans are found, check the aria-label of the main container itself
            // sometimes the total count is on the parent
            if (totalReactions === 0 && $reactionContainer.attr('aria-label')) {
                 const ariaLabel = $reactionContainer.attr('aria-label');
                 const match = ariaLabel.match(/(\d+\.?\d*)([KM]?)\sreactions?/i);
                 if (match) {
                     totalReactions = parseReactionNumber(match[1] + (match[2] || ''));
                 }
            }
        } else {
            // Fallback for older structures or if the above fails
            // Try to find elements that look like reaction counts directly
            // This might catch cases where reaction counts are simpler spans
            $messageElement.find('span[class*="reaction"], div[class*="reaction"]').each(function() {
                let text = $(this).text().trim();
                let count = parseReactionNumber(text);
                totalReactions += count;
            });
        }
        return totalReactions;
    }

    // --- Function to Parse K/M numbers ---
    function parseReactionNumber(str) {
        let value = parseFloat(str.replace(/[^0-9.,]/g, '').replace(/,/g, '.'));
        if (isNaN(value)) return 0;
        if (str.toUpperCase().includes('K')) {
            value *= 1000;
        } else if (str.toUpperCase().includes('M')) {
            value *= 1000000;
        }
        return value;
    }

    // --- Click Handler for the Sort Button ---
    $('#tg-sort-reactions-btn').on('click', function() {
        console.log('Tg Sort by Reactions: Sort button clicked.');

        const $chatMessagesContainer = $('.Transition.active .MessageList.open'); // Specific to Web A chat view
        if (!$chatMessagesContainer.length) {
            console.warn('Tg Sort by Reactions: Could not find the active chat messages container.');
            alert('Please open a chat with messages to sort.');
            return;
        }

        const $messageList = $chatMessagesContainer.find('.messages-container'); // This is where messages are directly appended
        if (!$messageList.length) {
            console.warn('Tg Sort by Reactions: Could not find the message list container.');
            alert('Could not find messages in the current chat.');
            return;
        }

        // Get all message bubbles
        // Web A message elements usually have a class like 'Message' or similar, and data attributes for ID.
        // Example: <div class="message-wrapper"> or <div class="Message">
        // Look for common parent of messages.
        const $messageBubbles = $chatMessagesContainer.find('[data-message-id]'); // All elements with a message ID

        if ($messageBubbles.length === 0) {
            console.warn('Tg Sort by Reactions: No messages found in the current view.');
            alert('No messages with reactions found to sort.');
            return;
        }

        console.log(`Tg Sort by Reactions: Found ${$messageBubbles.length} messages. Sorting...`);

        // Detach, sort, and re-append
        $messageBubbles.detach().sort(function(a, b) {
            const $a = $(a);
            const $b = $(b);

            let aReactions = getReactionCount($a);
            let bReactions = getReactionCount($b);

            // console.log(`Message A (${$a.attr('data-message-id')}): ${aReactions} reactions`);
            // console.log(`Message B (${$b.attr('data-message-id')}): ${bReactions} reactions`);

            return bReactions - aReactions; // Sort in descending order (highest reactions first)
        }).appendTo($messageList);

        console.log('Tg Sort by Reactions: Messages sorted.');

        // Scroll to the top or to the message with most reactions if it's not visible
        // It's more logical to scroll to the top after sorting by highest reactions.
        $chatMessagesContainer.scrollTop(0); // Scroll to the very top

        alert('Messages sorted by reactions (highest first)!');
    });

    // --- Mutation Observer to Handle Dynamic Content ---
    // Telegram Web A loads content dynamically, so we need to observe for chat changes.
    const observer = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
                // Check if the added nodes contain chat containers, indicating a chat change
                const chatContainerAdded = Array.from(mutation.addedNodes).some(node =>
                    node.nodeType === 1 && $(node).find('.MessageList.open').length
                );
                if (chatContainerAdded) {
                    console.log('Tg Sort by Reactions: Detected new chat container. Ready for sorting.');
                    // No need to re-add button, it's fixed. Just log readiness.
                }
            }
        });
    });

    // Observe the body for changes in chat content
    observer.observe(document.body, { childList: true, subtree: true });

    console.log('Tg Sort by Reactions: MutationObserver started.');

})();