您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds button to copy links to threads on Google Chat and adds button to messages to quote reply
当前为
// ==UserScript== // @name Google Chat 引用訊息轉貼到其他視窗 // @version 0.0.7 // @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 addStyle() { var styleElement = document.createElement('style', { type: 'text/css'}); styleElement.appendChild(document.createTextNode(` .gchat-xtra-copy { margin-left: 4px; border: 1px solid #dadce0; background-color: transparent; border-radius: 12px; box-sizing: border-box; font-family: 'Google Sans',Arial,sans-serif; font-size: .875rem; font-weight: 500; line-height: 1.25rem; color: #1967d2; padding: 0 12px; height: 24px; display: inline-flex; align-items: center; cursor: pointer; } .gchat-xtra-copy:hover { border-color: transparent; box-shadow: 0 1px 2px 0 rgba(60,64,67,0.30), 0 1px 3px 1px rgba(60,64,67,0.15); } .gchat-xtra-copy:active { background-color: rgba(26,115,232,0.122) } .gchat-xtra-copy[data-tooltip] { position: relative; } /* Base styles for the entire tooltip */ .gchat-xtra-copy[data-tooltip]:before, .gchat-xtra-copy[data-tooltip]:after { position: absolute; visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out, visibility 0.2s ease-in-out, transform 0.2s cubic-bezier(0.71, 1.7, 0.77, 1.24); transform: translate3d(0, 0, 0); pointer-events: none; } /* Show the entire tooltip on hover and focus */ .gchat-xtra-copy[data-tooltip]:hover:before, .gchat-xtra-copy[data-tooltip]:hover:after, .gchat-xtra-copy[data-tooltip]:focus:before, .gchat-xtra-copy[data-tooltip]:focus:after { visibility: visible; opacity: 1; } /* Base styles for the tooltip's directional arrow */ .gchat-xtra-copy[data-tooltip]:before { z-index: 1001; border: 6px solid transparent; background: transparent; content: ""; } /* Base styles for the tooltip's content area */ .gchat-xtra-copy[data-tooltip]:after { z-index: 1000; padding: 8px; background-color: #000; background-color: hsla(0, 0%, 20%, 0.9); color: #fff; content: attr(data-tooltip); font-size: 14px; line-height: 1.2; } /* Directions */ /* Top (default) */ .gchat-xtra-copy[data-tooltip]:before, .gchat-xtra-copy[data-tooltip]:after { bottom: 100%; left: 50%; } .gchat-xtra-copy[data-tooltip]:before { margin-left: -6px; margin-bottom: -12px; border-top-color: #000; border-top-color: hsla(0, 0%, 20%, 0.9); } /* Horizontally align top/bottom tooltips */ .gchat-xtra-copy[data-tooltip]:after { margin-left: -30px; } .gchat-xtra-copy[data-tooltip]:hover:before, .gchat-xtra-copy[data-tooltip]:hover:after, .gchat-xtra-copy[data-tooltip]:focus:before, .gchat-xtra-copy[data-tooltip]:focus:after { -webkit-transform: translateY(-12px); -moz-transform: translateY(-12px); transform: translateY(-12px); } /* Removes GitHub Enterprise and Google sign-in previews since they always show up empty */ a[aria-label="Build software better, together, Web Page."], a[aria-label$="Google Accounts, Web Page."] { display: none; } `)); document.head.appendChild(styleElement); } function inIframe () { try { return window.self !== window.top; } catch (e) { return true; } } 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){ var copy = e.querySelector('.gchat-xtra-copy'); if(e.getAttribute("data-topic-id") && !copy){ // Adding copy thread link buttons to thread var copyButton = document.createElement("div"); copyButton.className="gchat-xtra-copy"; copyButton.innerHTML = ` Copy thread link `; copyButton.addEventListener('click', function() { const el = document.createElement('textarea'); const threadId = e.getAttribute("data-topic-id"); if (inIframe()) { // The new mail.google.com/chat application uses iframes that point to chat.google.com // Rooms are now renamed to spaces. Getting the space id from an attribute in the element const roomId = e.getAttribute('data-p').match(/space\/([^\\"]*)/)[1]; el.value = `https://mail.google.com/chat/#chat/space/${roomId}/${threadId}`; } else { const roomId = window.location.pathname.match(/\/room\/([^\?\/]*)/)[1]; el.value = `https://chat.google.com/room/${roomId}/${threadId}`; } document.body.appendChild(el); el.select(); document.execCommand('copy'); document.body.removeChild(el); copyButton.setAttribute('data-tooltip', 'Copied'); setTimeout(function() { copyButton.removeAttribute('data-tooltip'); }, 1000); }); var buttonContainer = e.querySelector('div[aria-label="Follow"] > span:first-of-type'); if ( buttonContainer && buttonContainer.children.length === 2 && buttonContainer.children[0].tagName === 'SPAN' && buttonContainer.children[1].tagName === 'SPAN' ) { buttonContainer.style = 'display: inline-block;'; buttonContainer.parentElement.style = 'display: inline-block; width: unset; opacity: 1;'; buttonContainer.parentElement.parentElement.appendChild(copyButton); // Follow button container gets hidden in channels where all notifications are enabled. // Undo that buttonContainer.parentElement.parentElement.parentElement.style += '; display: block;'; copyButtonInsertedCount += 1; scrollContainer.scrollTop += 36; buttonContainer.parentElement.parentElement.parentElement.parentElement.style = 'padding-top: 56px;'; } } // 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; 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]'); 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)\/([^;$]+)/); if (space) { if (space[1] == 'space') { room = 'room'; } if (space[1] == 'dm') { room = 'dm'; } threadLink = `https://chat.google.com/${room}/${space[2]}/${msgId}`; } showPopup(); break; // Can extract time, but adding it into static text surrounded by relative time that's rendered in the chats will only confuse people // time = el.Ref.parentElement.children[messageIndex].querySelector('span[data-absolute-timestamp]').getAttribute('data-absolute-timestamp'); } 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); // inputEl.innerHTML = message; // inputEl.scrollIntoView(); // inputEl.click(); // placeCaretAtEnd(inputEl); }); } } ); } ); 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; } } 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); } addStyle(); main(); var el = document.documentElement; el.addEventListener('DOMSubtreeModified', debounce(main, 2000)); })();