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.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));
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址