Skip to content

Instantly share code, notes, and snippets.

@cktang88
Last active December 31, 2024 19:38
Show Gist options
  • Save cktang88/84f1f94e8b18ae769cab8648ae306d46 to your computer and use it in GitHub Desktop.
Save cktang88/84f1f94e8b18ae769cab8648ae306d46 to your computer and use it in GitHub Desktop.
no-twitter-bookmarks
/*
Hides Twitter's bookmarking button, prevents "hoarding" bookmarks.
However can still access past bookmarks and unbookmark the ones you have already.
Using data-testid b/c it also hides the bookmark btn on a single tweet
*/
button[data-testid="bookmark"] {
display: none !important;
}
// adds a "copy" button that copies text + image of a tweet into clipboard
document.addEventListener('DOMContentLoaded', () => {
// CSS for our copy button + click animation
const style = document.createElement('style');
style.textContent = `
.my-copy-tweet-btn {
background: none;
border: none;
outline: none;
color: #71767B;
display: inline-flex;
align-items: center;
cursor: pointer;
margin-left: 8px;
}
.my-copy-tweet-btn:hover {
color: #1d9bf0;
}
.my-copy-tweet-btn svg {
fill: currentColor;
width: 18px;
height: 18px;
}
@keyframes copyButtonClick {
0% { transform: scale(1); }
40% { transform: scale(1.3); }
100% { transform: scale(1); }
}
.my-copy-tweet-btn.clicked {
animation: copyButtonClick 0.3s ease-in-out;
}
`;
document.head.appendChild(style);
async function urlToBlob(url) {
const response = await fetch(url);
return response.blob();
}
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async function generateHtmlWithImages(tweetText, imageUrls) {
let html = `<p>${tweetText}</p>`;
for (const url of imageUrls) {
const blob = await urlToBlob(url);
const base64 = await blobToBase64(blob);
html += `<img src="${base64}" /><br/>`;
}
return html;
}
// 4) Main copy logic
async function copyTweetContent(tweetEl, buttonEl) {
// A) --- Gather the main tweet text
const textEl = tweetEl.querySelector('[data-testid="tweetText"]');
const tweetText = textEl?.innerText ?? '';
// B) --- Extract display name, handle, date
let displayName = '';
let handle = '';
let tweetDate = '';
// i) The user name container (display name + handle are often in same block)
// Usually `[data-testid="User-Name"]` has display name in <span> and handle in another <span>, or in adjacent elements.
// We'll do a minimal approach: find the first link in `[data-testid="User-Name"]` and parse text.
const userNameBlock = tweetEl.querySelector('[data-testid="User-Name"]');
if (userNameBlock) {
// The display name often is the first text, and the handle might be an adjacent link or span.
// Different Twitter versions can differ, so let's just parse them carefully:
// (a) Display name is often in a child <span> that doesn't contain '@'.
const nameSpan = [...userNameBlock.querySelectorAll('span')]
.find(sp => sp.innerText && !sp.innerText.startsWith('@'));
displayName = nameSpan?.innerText.trim() ?? '';
// (b) The handle is often in a child <span> that DOES contain '@'
const handleSpan = [...userNameBlock.querySelectorAll('span')]
.find(sp => sp.innerText && sp.innerText.startsWith('@'));
handle = handleSpan?.innerText.trim() ?? '';
}
// ii) The date is typically in a <time> element
const timeEl = tweetEl.querySelector('time');
if (timeEl) {
// Grab the raw datetime attribute (e.g. "2024-12-31T17:35:20.000Z")
const dateStr = timeEl.getAttribute('datetime');
if (dateStr) {
// Convert it into a Date object
const dateObj = new Date(dateStr);
// Format as you like, for example: "Dec 31, 2024, 9:35 AM"
tweetDate = dateObj.toLocaleString([], {
year: 'numeric',
month: 'short',
day: '2-digit',
hour: 'numeric',
minute: '2-digit',
});
}
}
// C) --- Build the appended user info line
// Something like:
// — DisplayName (@handle) on Dec 31, 2024
// If data is missing, it simply won't appear.
let userInfoLine = '—';
if (displayName) userInfoLine += ` ${displayName}`;
if (handle) userInfoLine += ` (${handle})`;
if (tweetDate) userInfoLine += ` on ${tweetDate}`;
// D) --- Gather images
const allImgs = [...tweetEl.querySelectorAll('img')];
// Filter for real tweet images (pbs.twimg.com)
const mediaImgs = allImgs.filter(img => /pbs\.twimg\.com\/media\//.test(img.src));
const imageUrls = mediaImgs.map(img => img.src);
// E) --- Prepare text + HTML
// Append the userInfoLine to the bottom
const plainText = tweetText + '\n\n' + userInfoLine;
// Build HTML snippet
let htmlContent = await generateHtmlWithImages(plainText, imageUrls);
// F) --- Single ClipboardItem
const plainTextBlob = new Blob([plainText], { type: 'text/plain' });
const htmlBlob = new Blob([htmlContent], { type: 'text/html' });
const singleItem = new ClipboardItem({
'text/plain': plainTextBlob,
'text/html': htmlBlob,
});
await navigator.clipboard.write([singleItem]);
// G) --- Small CSS "pop" animation
buttonEl.classList.add('clicked');
setTimeout(() => buttonEl.classList.remove('clicked'), 300);
}
function addCopyButtonsToTweets() {
const tweets = document.querySelectorAll('article[data-testid="tweet"]');
tweets.forEach(tweetEl => {
const likeButton = tweetEl.querySelector('[data-testid="like"]');
if (!likeButton) return;
if (tweetEl.querySelector('.my-copy-tweet-btn')) return;
const copyIconSVG = `
<svg viewBox="0 0 24 24" aria-hidden="true">
<g>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8
c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9
2-2V7c0-1.1-.9-2-2-2z"></path>
</g>
</svg>
`;
const copyBtn = document.createElement('button');
copyBtn.className = 'my-copy-tweet-btn';
copyBtn.innerHTML = copyIconSVG;
copyBtn.onclick = (e) => {
e.stopPropagation();
copyTweetContent(tweetEl, copyBtn).catch(err => console.error(err));
};
likeButton.parentNode.parentNode.insertBefore(copyBtn, likeButton.nextSibling);
});
}
// Try MutationObserver
try {
const observer = new MutationObserver(() => {
addCopyButtonsToTweets();
});
observer.observe(document.body, {
childList: true,
subtree: true
});
} catch (err) {
console.warn('MutationObserver not working—fallback to setInterval:', err);
// Fallback approach: periodically scan for new tweets
setInterval(() => {
addCopyButtonsToTweets();
}, 2000);
}
// Run once initially
addCopyButtonsToTweets();
})
@cktang88
Copy link
Author

great as an Arc boost, or can just edit page CSS in devtools

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment