Google Chat 引用訊息轉貼到其他視窗

Adds button to copy links to threads on Google Chat and adds button to messages to quote reply

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Google Chat 引用訊息轉貼到其他視窗
// @version      0.0.13
// @namespace    Ymx1ZTMyNmNj
// @description  Adds button to copy links to threads on Google Chat and adds button to messages to quote reply
// @author       upman
// @match      https://chat.google.com/*
// @grant        none
// @run-at       document-idle
// @license      https://github.com/upman/gchat-copy
// ==/UserScript==

; (function () {
    function main() {
        var scrollContainer = document.querySelector('c-wiz[data-group-id][data-is-client-side] > div:nth-child(1)');
        var copyButtonInsertedCount = 0;

        const itemContainer = document.querySelector('div[class="CfUpN"]');

        if (itemContainer) {
            if (!itemContainer.querySelector('[data-tooltip*="ForwardMsg"')) {

                const forwardButton = document.createElement('div');
                // Quote svg icon
                forwardButton.innerHTML = `
                    <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top: 4px">
                    <path
                        d="M9.25045 7.78369L7.83624 6.36948L3.59363 10.6121L7.83627 14.8547L9.25048 13.4405L6.42205 10.6121L9.25045 7.78369Z"
                        fill="currentColor"
                    />
                    <path
                        d="M13.4932 13.4405L12.0789 14.8547L7.83627 10.6121L12.0789 6.36948L13.4931 7.78369L11.6463 9.63049L16.4063 9.63049C18.6155 9.63049 20.4063 11.4214 20.4063 13.6305L20.4063 17.6305L18.4063 17.6305L18.4063 13.6305C18.4063 12.5259 17.5109 11.6305 16.4063 11.6305L11.6831 11.6305L13.4932 13.4405Z"
                        fill="currentColor"
                    />
                    </svg>`;

                forwardButton.className = itemContainer.children[1].className;
                forwardButton.setAttribute('data-tooltip', 'ForwardMsg');

                const forwardMessage = window.localStorage.getItem('forwardMessage');

                if (forwardMessage) {
                    itemContainer.prepend(forwardButton);
                }

                forwardButton.addEventListener('click', () => {
                    let input = document.querySelector('div[contenteditable="true"]');

                    input.innerHTML = window.localStorage.getItem('forwardMessage');
                    input.scrollIntoView();
                    input.click();
                    placeCaretAtEnd(input);
                    window.localStorage.removeItem('forwardMessage');
                    forwardButton.remove();
                });
            }
        }

        // Iterating on threads and in the case of DMs, the whole message history is one thread
        document.querySelectorAll("c-wiz[data-topic-id][data-local-topic-id]")
            .forEach(
                function(e,t,i){

                    // Iterating on each message in the thread
                    e.querySelectorAll('div[jscontroller="VXdfxd"]').forEach(
                        // Adding quote message buttons
                        function(addreactionButton) {
                            if (
                                addreactionButton.parentElement.parentElement.querySelector('[data-tooltip*="Quote Message"') || // Quote reply button already exists
                                addreactionButton.parentElement.parentElement.children.length === 1 // Add reaction button next to existing emoji reactions to a message
                            ) {
                                return;
                            }
                            const container = document.createElement('div');
                            // Quote svg icon
                            container.innerHTML = `
                                <svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top: 4px;color:#9aa0a6;">
                                <path
                                    d="M14.7495 7.78369L16.1638 6.36948L20.4064 10.6121L16.1637 14.8547L14.7495 13.4405L17.5779 10.6121L14.7495 7.78369Z"
                                    fill="currentColor"
                                />
                                <path
                                    d="M10.5068 13.4405L11.9211 14.8547L16.1637 10.6121L11.9211 6.36948L10.5069 7.78369L12.3537 9.63049L7.59366 9.63049C5.38452 9.63049 3.59366 11.4214 3.59366 13.6305L3.59366 17.6305L5.59366 17.6305L5.59366 13.6305C5.59366 12.5259 6.48909 11.6305 7.59366 11.6305L12.3169 11.6305L10.5068 13.4405Z"
                                    fill="currentColor"
                                />
                                </svg>
                                `;
                            container.className=addreactionButton.className;
                            container.setAttribute('data-tooltip', 'Quote Message');
                            const quoteSVG = container.children[0]
                            const svg = addreactionButton.querySelector('svg');
                            if (svg) {
                                svg.classList.forEach(c => quoteSVG.classList.add(c));
                            } else {
                                return;
                            }

                            var elRef = addreactionButton;
                            // Find parent container of the message
                            // These messages are then grouped together when they are from the recipient
                            // and the upper most one has the name and time of the message
                            while(!(elRef.className && elRef.className.includes('nF6pT')) && elRef.parentElement) {
                                elRef = elRef.parentElement;
                            }
                            if (elRef.className.includes('nF6pT')) {

                                var messageIndex, name, msgId, space, time, room, absTime, chat;
                                let threadLink = '';
                                [...elRef.parentElement.children].forEach((messageEl, index) => {
                                    if (messageEl === elRef) {
                                        messageIndex = index;
                                    }
                                });

                                addreactionButton.parentElement.parentElement.appendChild(container);
                                container.addEventListener('click', () => {
                                    while (messageIndex >= 0) {
                                        if (elRef.parentElement.children[messageIndex].className.includes('AnmYv')) {
                                            const nameContainer = elRef.parentElement.children[messageIndex].querySelector('[data-hovercard-id], [data-member-id]');
                                            const idContainer = elRef.parentElement.children[messageIndex].querySelector('[data-message-id]');
                                            const timeContainer = elRef.parentElement.children[messageIndex].querySelector('[data-absolute-timestamp]');
                                            const chatContainer = elRef.parentElement.children[messageIndex];

                                            time = timeContainer.getAttribute('data-absolute-timestamp');
                                            absTime = getAbsoluteTime(parseInt(time));
                                            name = `${nameContainer.getAttribute('data-name')} [${absTime}]`;
                                            msgId = idContainer.getAttribute('data-message-id');
                                            space = nameContainer.getAttribute('jsdata').match(/(space|dm)\/([^;$]+)/);
                                            chat = chatContainer.getAttribute('jsdata').match(/(?<=,)[^,]+(?=,(space|dm)\/)/)[0];

                                            if (space) {
                                                if (space[1] == 'space') {
                                                    room = 'room';
                                                }

                                                if (space[1] == 'dm') {
                                                    room = 'dm';
                                                }

                                                threadLink = `https://chat.google.com/${room}/${space[2]}/${chat}/${msgId}`;
                                            }

                                            showPopup();

                                            break;
                                        }
                                        messageIndex -= 1;
                                    }

                                    var messageContainer = addreactionButton.parentElement.parentElement.parentElement.parentElement.parentElement.children[0];

                                    var quoteText = getQuoteText(messageContainer);
                                    let inputEl = e.querySelector('div[contenteditable="true"]'); // This fetches the input element in channels
                                    let dmInput = document.body.querySelectorAll('div[contenteditable="true"]'); // This fetches the input in DMs
                                    inputEl = inputEl ? inputEl : dmInput[dmInput.length - 1];
                                    if (!inputEl) {
                                        return;
                                    }

                                    let message = `${makeInputText(name, quoteText, inputEl, messageContainer)}${threadLink}`;
                                    window.localStorage.setItem('forwardMessage', message);
                                });
                            }
                        }
                    );
                }
            );

        if (copyButtonInsertedCount > 1) {
            scrollContainer.scrollTop += 36;
        }
    }

    function makeInputText(name, quoteText, inputEl, messageContainer) {
        var isDM = window.location.href.includes('/dm/');
        var selection = window.getSelection().toString();
        var text = getText(messageContainer);
        var oneLineQuote = '';

        if (selection && text.includes(selection) && selection.trim()) {
            var regexp = new RegExp('(.*)' + selection + '(.*)');
            var matches = regexp.exec(text);
            if (matches[1]) {
                // Has text before the match
                oneLineQuote += '... ';
            }
            oneLineQuote += selection.trim();

            if (matches[2]) {
                // Has text after the match
                oneLineQuote += ' ...';
            }
        }

        if(isDM) {
            return oneLineQuote ? '`' + oneLineQuote + '`\n' :
                ("```\n" + quoteText + "\n```\n" + inputEl.innerHTML)
        } else {

            return oneLineQuote ? '`' + name + ': ' + oneLineQuote + '`\n' :
                ("```\n" + name + ":\n" + quoteText + "\n```\n" + inputEl.innerHTML);
        }
    }

    function showPopup() {
        const popup = document.createElement('div');
        popup.innerHTML = '在任意視窗按<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style="margin-top: 4px"><path d="M9.25045 7.78369L7.83624 6.36948L3.59363 10.6121L7.83627 14.8547L9.25048 13.4405L6.42205 10.6121L9.25045 7.78369Z" fill="currentColor"/><path d="M13.4932 13.4405L12.0789 14.8547L7.83627 10.6121L12.0789 6.36948L13.4931 7.78369L11.6463 9.63049L16.4063 9.63049C18.6155 9.63049 20.4063 11.4214 20.4063 13.6305L20.4063 17.6305L18.4063 17.6305L18.4063 13.6305C18.4063 12.5259 17.5109 11.6305 16.4063 11.6305L11.6831 11.6305L13.4932 13.4405Z" fill="currentColor"/></svg>轉貼訊息';
        popup.style.position = 'fixed';
        popup.style.top = '50%';
        popup.style.left = '50%';
        popup.style.transform = 'translate(-50%, -50%)';
        popup.style.backgroundColor = '#333'; // 深色背景
        popup.style.color = '#fff'; // 文字颜色
        popup.style.padding = '10px 20px';
        popup.style.border = '1px solid #555'; // 边框颜色
        popup.style.boxShadow = '0px 0px 10px rgba(0, 0, 0, 0.5)';
        popup.style.zIndex = '9999';

        document.body.appendChild(popup);

        setTimeout(function () {
            document.body.removeChild(popup);
        }, 2000);
    }

    function getQuoteText(messageContainer) {
        var regularText = getText(messageContainer);
        var videoCall = messageContainer.querySelector('a[href*="https://meet.google.com/"]');
        var image = messageContainer.querySelector('a img[alt]');
        var text = regularText ||
            (videoCall ? "🎥: " + videoCall.href: null) ||
            (image ? "📷: " + image.alt: null) ||
            '...';

        return truncateQuoteText(text);
    }

    function truncateQuoteText(text) {
        let splitText = text.split('\n');
        let quoteText = splitText.slice(0,500).join('\n') + (splitText.length > 500 ? '\n...' : '');
        if (quoteText.length > 5000) {
            quoteText = quoteText.slice(0, 5000) + ' ...';
        }
        return quoteText;
    }

    function getText(messageContainer) {
        const multilineMarkdownClass = 'FMTudf';
        let textContent = '';
        const childNodes = messageContainer.children[0].childNodes;

        for (var i = 0; i < childNodes.length; i += 1) {
            if (childNodes[i].nodeType === Node.TEXT_NODE) {
                textContent += childNodes[i].textContent;
            } else if (childNodes[i].className === 'jn351e') {
                continue;
            } else if (childNodes[i].className === multilineMarkdownClass) {
                textContent += '...\n';
            } else if (childNodes[i].tagName === 'IMG') {
                // emojis
                textContent += childNodes[i].alt;
            } else {
                textContent += childNodes[i].innerHTML + '</br>';
            }
        }

        if (messageContainer.children[1] && messageContainer.children[1].getAttribute('jsname') == 'bgckF') {
            textContent = `\n${messageContainer.children[1].textContent}`;
        }

        return textContent;
    }

    function placeCaretAtEnd(el) {
        el.focus();
        if (typeof window.getSelection != "undefined"
                && typeof document.createRange != "undefined") {
            var range = document.createRange();
            range.selectNodeContents(el);
            range.collapse(false);
            var sel = window.getSelection();
            sel.removeAllRanges();
            sel.addRange(range);
            range.insertNode(document.createElement('br'));
            range.collapse();
        } else if (typeof document.body.createTextRange != "undefined") {
            var textRange = document.body.createTextRange();
            textRange.moveToElementText(el);
            textRange.collapse(false);
            textRange.select();
        }
    }

    function debounce(fn, delay) {
        var timeout = null;
        return function() {
            if(timeout) {
                return;
            } else {
                timeout = setTimeout(function() {
                    fn();
                    timeout = null;
                }, delay);
            }
        }
    }

    function getAbsoluteTime(d) {
        d = new Date(new Date(d).getTime() - new Date(d).getTimezoneOffset() * 60 * 1000);

        return d.toISOString().replace('T', ' ').substr(0, 19);
    }

    main();
    var el = document.documentElement;
    el.addEventListener('DOMSubtreeModified', debounce(main, 2000));

})();