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

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

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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));

})();