class TweetViewer { constructor(user, tweetData) { let previousLocation = location.pathname + location.search; this.container = createModal(/*html*/`
`, 'tweet-viewer', () => { this.close(); history.pushState({}, null, previousLocation); }); this.tweetData = tweetData; this.id = tweetData.id_str; history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${this.id}`); this.user = user; this.loadingNewTweets = false; this.lastTweetDate = 0; this.activeTweet; this.pageData = {}; this.tweets = []; this.cursor = undefined; this.mediaToUpload = []; this.excludeUserMentions = []; this.users = {}; this.linkColors = {}; this.likeCursor = undefined; this.retweetCursor = undefined; this.retweetCommentsCursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; this.currentLocation = location.pathname; this.subpage = undefined; this.popstateHelper = undefined; this.scrollHelper = undefined; this.timelineElement = this.container.getElementsByClassName('timeline')[0]; this.moreBtn = this.container.getElementsByClassName('timeline-more')[0]; let event = new CustomEvent('clearActiveTweet'); document.dispatchEvent(event); chrome.storage.sync.get(['viewedtweets'], (result) => { if(!result.viewedtweets) result.viewedtweets = []; result.viewedtweets.unshift(this.id); result.viewedtweets = [...new Set(result.viewedtweets)]; while(result.viewedtweets.length >= 100) { result.viewedtweets.pop(); } chrome.storage.sync.set({ viewedtweets: result.viewedtweets }); }); this.init(); } async savePageData(path) { if(!path) { path = location.pathname.split('?')[0].split('#')[0]; if(path.endsWith('/')) path = path.slice(0, -1); } this.pageData[path] = { linkColors: this.linkColors, cursor: this.cursor, likeCursor: this.likeCursor, retweetCursor: this.retweetCursor, retweetCommentsCursor: this.retweetCommentsCursor, mainTweetLikers: this.mainTweetLikers, seenReplies: this.seenReplies, tweets: this.tweets, scrollY: this.container.scrollTop } console.log(`Saving page: ${path}`, this.pageData[path]); } async restorePageData() { let path = location.pathname.split('?')[0].split('#')[0]; if(path.endsWith('/')) path = path.slice(0, -1); if(this.pageData[path]) { console.log(`Restoring page: ${path}`, this.pageData[path]); this.linkColors = this.pageData[path].linkColors; this.cursor = this.pageData[path].cursor; this.likeCursor = this.pageData[path].likeCursor; this.retweetCursor = this.pageData[path].retweetCursor; this.retweetCommentsCursor = this.pageData[path].retweetCommentsCursor; this.mainTweetLikers = this.pageData[path].mainTweetLikers; this.seenReplies = []; this.tweets = []; this.container.getElementsByClassName('timeline-more')[0].hidden = !this.cursor; let tl = document.getElementsByClassName('timeline')[0]; tl.innerHTML = ''; for(let i in this.pageData[path].tweets) { let t = this.pageData[path].tweets[i]; if(t[0] === 'tweet') { await this.appendTweet(t[1], tl, t[2]); } else if(t[0] === 'compose') { await this.appendComposeComponent(tl, t[1]); } else if(t[0] === 'tombstone') { await this.appendTombstone(tl, t[1]); } } let id = this.currentLocation.match(/status\/(\d{1,32})/)[1]; if(id) { setTimeout(() => { let tweet = this.container.getElementsByClassName(`tweet-id-${id}`)[0]; if(tweet) { tweet.scrollIntoView({ block: 'center' }); } }, 100); } if(this.subpage === 'retweets_with_comments' && this.retweetCommentsCursor) { this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = false; } this.loadingNewTweets = false; return this.pageData[path]; } else { this.tweets = []; this.seenReplies = []; } this.loadingNewTweets = false; return false; } updateSubpage() { let path = location.pathname.slice(1); if(path.endsWith('/')) path = path.slice(0, -1); let tlDiv = document.getElementsByClassName('timeline')[0]; let rtDiv = document.getElementsByClassName('retweets')[0]; let rtwDiv = document.getElementsByClassName('retweets_with_comments')[0]; let likesDiv = document.getElementsByClassName('likes')[0]; let tlMore = document.getElementsByClassName('timeline-more')[0]; let rtMore = document.getElementsByClassName('retweets-more')[0]; let rtwMore = document.getElementsByClassName('retweets_with_comments-more')[0]; let likesMore = document.getElementsByClassName('likes-more')[0]; tlDiv.hidden = true; rtDiv.hidden = true; rtwDiv.hidden = true; likesDiv.hidden = true; tlMore.hidden = true; rtMore.hidden = true; rtwMore.hidden = true; likesMore.hidden = true; if(path.split('/').length === 3) { this.subpage = 'tweet'; tlDiv.hidden = false; } else { if(path.endsWith('/retweets')) { this.subpage = 'retweets'; rtDiv.hidden = false; } else if(path.endsWith('/likes')) { this.subpage = 'likes'; likesDiv.hidden = false; } else if(path.endsWith('/retweets/with_comments')) { this.subpage = 'retweets_with_comments'; rtwDiv.hidden = false; } } } async updateReplies(id, c) { let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; if(!c) { tvl.hidden = false; document.getElementsByClassName('timeline')[0].innerHTML = ''; } let tl, tweetLikers; try { let [tlData, tweetLikersData] = await Promise.allSettled([API.tweet.getRepliesV2(id, c), API.tweet.getLikers(id)]); if(!tlData.value) { this.cursor = undefined; return console.error(tlData.reason); } tl = tlData.value; for(let u in tl.users) { this.users[u] = tl.users[u]; } tweetLikers = tweetLikersData.value; this.loadingNewTweets = false; document.getElementsByClassName('timeline-more')[0].innerText = LOC.load_more.message; } catch(e) { document.getElementsByClassName('timeline-more')[0].innerText = LOC.load_more.message; this.loadingNewTweets = false; tvl.hidden = true; return this.cursor = undefined; } if(vars.linkColorsInTL) { let tlUsers = []; for(let i in tl.list) { let t = tl.list[i]; if(t.type === 'tweet' || t.type === 'mainTweet') { if(!tlUsers.includes(t.data.user.id_str)) tlUsers.push(t.data.user.id_str); } else if(t.type === 'conversation') { for(let j in t.data) { tlUsers.push(t.data[j].user.id_str); } } } tlUsers = tlUsers.filter(i => !this.linkColors[i]); let linkData = await getLinkColors(tlUsers); if(linkData) for(let i in linkData) { this.linkColors[linkData[i].id] = linkData[i].color; } } this.cursor = tl.cursor; if(!this.cursor) { this.container.getElementsByClassName('timeline-more')[0].hidden = true; } else { this.container.getElementsByClassName('timeline-more')[0].hidden = false; } let mainTweet; let mainTweetIndex = tl.list.findIndex(t => t.type === 'mainTweet'); let tlContainer = document.getElementsByClassName('timeline')[0]; for(let i in tl.list) { let t = tl.list[i]; if(t.type === 'mainTweet') { this.mainTweetLikers = tweetLikers.list; this.likeCursor = tweetLikers.cursor; if(i === 0) { mainTweet = await this.appendTweet(t.data, tlContainer, { mainTweet: true }); } else { mainTweet = await this.appendTweet(t.data, tlContainer, { noTop: true, mainTweet: true }); } if(t.data.limited_actions !== "non_compliant") this.appendComposeComponent(tlContainer, t.data); } if(t.type === 'tweet') { await this.appendTweet(t.data, tlContainer, { noTop: i !== 0 && i < mainTweetIndex, threadContinuation: i < mainTweetIndex }); } else if(t.type === 'conversation') { for(let i2 in t.data) { let t2 = t.data[i2]; await this.appendTweet(t2, tlContainer, { noTop: +i2 !== 0, threadContinuation: +i2 !== t.data.length - 1, threadButton: +i2 === t.data.length - 1, threadId: t2.conversation_id_str }); } } else if(t.type === 'tombstone') { this.appendTombstone(tlContainer, t.data); } else if(t.type === 'showMore') { let div = document.createElement('div'); div.className = 'show-more'; div.innerHTML = ` `; let loading = false; div.querySelector('.show-more-button').addEventListener('click', async () => { if(loading) return; loading = true; div.children[0].innerText = LOC.loading_tweets.message; await this.updateReplies(id, t.data.cursor); div.remove(); }); tlContainer.appendChild(div); } } if(mainTweet) mainTweet.scrollIntoView(); if(tvl) tvl.hidden = true; return true; } async updateLikes(id, c) { let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; if(tvl) tvl.hidden = false; let tweetLikers; if(!c && this.mainTweetLikers.length > 0) { tweetLikers = this.mainTweetLikers; } else { try { tweetLikers = await API.tweet.getLikers(id, c); this.likeCursor = tweetLikers.cursor; tweetLikers = tweetLikers.list; if(!c) this.mainTweetLikers = tweetLikers; } catch(e) { console.error(e); if(tvl) tvl.hidden = true; return this.likeCursor = undefined; } } let likeDiv = document.getElementsByClassName('likes')[0]; if(!c) { likeDiv.innerHTML = ''; let tweetData = await API.tweet.getV2(id); let tweet = await this.appendTweet(tweetData, likeDiv, { mainTweet: true }); tweet.style.borderBottom = '1px solid var(--border)'; tweet.style.marginBottom = '10px'; tweet.style.borderRadius = '5px'; let h1 = document.createElement('h1'); h1.innerText = LOC.liked_by.message; h1.className = 'cool-header'; likeDiv.appendChild(h1); } if(!this.likeCursor || tweetLikers.length === 0) { this.container.getElementsByClassName('likes-more')[0].hidden = true; } else { this.container.getElementsByClassName('likes-more')[0].hidden = false; } for(let i in tweetLikers) { appendUser(tweetLikers[i], likeDiv); } if(tvl) tvl.hidden = true; } async updateRetweets(id, c) { let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; tvl.hidden = false; let tweetRetweeters; try { tweetRetweeters = await API.tweet.getRetweeters(id, c); this.retweetCursor = tweetRetweeters.cursor; tweetRetweeters = tweetRetweeters.list; } catch(e) { console.error(e); return this.retweetCursor = undefined; } let retweetDiv = document.getElementsByClassName('retweets')[0]; if(!c) { retweetDiv.innerHTML = ''; let tweetData = await API.tweet.getV2(id); let tweet = await this.appendTweet(tweetData, retweetDiv, { mainTweet: true }); tweet.style.borderBottom = '1px solid var(--border)'; tweet.style.marginBottom = '10px'; tweet.style.borderRadius = '5px'; let h1 = document.createElement('h1'); h1.innerHTML = `${LOC.retweeted_by.message} (${LOC.see_quotes.message})`; h1.className = 'cool-header'; retweetDiv.appendChild(h1); h1.getElementsByTagName('a')[0].addEventListener('click', async e => { e.preventDefault(); history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets/with_comments`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateRetweetsWithComments(tid); this.currentLocation = location.pathname; }); } if(!this.retweetCursor) { this.container.getElementsByClassName('retweets-more')[0].hidden = true; } else { this.container.getElementsByClassName('retweets-more')[0].hidden = false; } for(let i in tweetRetweeters) { let u = tweetRetweeters[i]; let retweetElement = document.createElement('div'); retweetElement.classList.add('following-item'); retweetElement.innerHTML = `
${u.screen_name}
${escapeHTML(u.name)}
@${u.screen_name}
`; let followButton = retweetElement.querySelector('.following-item-btn'); followButton.addEventListener('click', async () => { if (followButton.classList.contains('following')) { await API.user.unfollow(u.screen_name); followButton.classList.remove('following'); followButton.classList.add('follow'); followButton.innerText = LOC.follow.message; } else { await API.user.follow(u.screen_name); followButton.classList.remove('follow'); followButton.classList.add('following'); followButton.innerText = LOC.following_btn.message; } }); retweetDiv.appendChild(retweetElement); } tvl.hidden = true; } async updateRetweetsWithComments(id, c) { let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; tvl.hidden = false; let tweetRetweeters; let tweetData = await API.tweet.getV2(id); try { tweetRetweeters = await API.tweet.getQuotes(id, c); this.retweetCommentsCursor = tweetRetweeters.cursor; tweetRetweeters = tweetRetweeters.list; } catch(e) { console.error(e); tvl.hidden = true; return this.retweetCommentsCursor = undefined; } let retweetDiv = document.getElementsByClassName('retweets_with_comments')[0]; if(!c) { retweetDiv.innerHTML = ''; let h1 = document.createElement('h1'); h1.innerHTML = `${LOC.quote_tweets.message} (${LOC.see_retweets.message})`; h1.className = 'cool-header'; retweetDiv.appendChild(h1); h1.getElementsByTagName('a')[0].addEventListener('click', async e => { e.preventDefault(); let t = await API.tweet.getV2(id); history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateRetweets(tid); this.currentLocation = location.pathname; }); } if(!this.retweetCommentsCursor) { this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = true; } else { this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = false; } for(let i in tweetRetweeters) { await this.appendTweet(tweetRetweeters[i], retweetDiv); } tvl.hidden = true; } async appendComposeComponent(container, replyTweet) { if(!replyTweet) return; this.tweets.push(['compose', replyTweet]); let mentions = replyTweet.full_text.match(/@([\w+]{1,15})/g); if(mentions) { mentions = mentions.map(m => m.slice(1).trim()); } else { mentions = []; } let replyMessage; if(LOC.reply_to.message.includes("$SCREEN_NAME$")) { replyMessage = LOC.reply_to.message.replace("$SCREEN_NAME$", replyTweet.user.screen_name); } else { replyMessage = `${LOC.reply_to.message} @${replyTweet.user.screen_name}`; } let el = document.createElement('div'); el.className = 'new-tweet-container'; el.innerHTML = /*html*/`
`; container.append(el); document.getElementsByClassName('new-tweet-avatar')[0].src = `${(this.user.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(this.user.id_str) % 7}_normal.png`): this.user.profile_image_url_https}`.replace("_normal", "_bigger"); document.getElementsByClassName('new-tweet-view')[0].addEventListener('click', async () => { document.getElementsByClassName('new-tweet-focused')[0].hidden = false; document.getElementsByClassName('new-tweet-char')[0].hidden = false; document.getElementsByClassName('new-tweet-text')[0].classList.add('new-tweet-text-focused'); document.getElementsByClassName('new-tweet-media-div')[0].classList.add('new-tweet-media-div-focused'); }); if(mentions.length > 0) { for(let i = 0; i < mentions.length; i++) { let u = Object.values(this.users).find(u => u.screen_name === mentions[i]); if(!u) { if(mentions[i] === this.user.screen_name) { u = this.user; } else if(typeof pageUser !== 'undefined' && mentions[i] === pageUser.screen_name) { u = pageUser; } else { try { u = await API.user.get(mentions[i], false); } catch(e) { console.error(e); continue; } } } if(!u) continue; this.users[u.id_str] = u; } document.getElementsByClassName('new-tweet-button')[0].style = 'margin-right: -50px;'; document.getElementsByClassName("new-tweet-mentions")[0].addEventListener('click', async () => { let modal = createModal(/*html*/`

${LOC.replying_to.message}

${mentions.map(m => { let u = Object.values(this.users).find(u => u.screen_name === m); if(!u) return ''; return /*html*/`
`}).join('\n')}
`); document.getElementById('new-tweet-mentions-modal-button').addEventListener('click', () => { let excluded = []; document.querySelectorAll('#new-tweet-mentions-modal input[type="checkbox"]').forEach(c => { if(!c.checked) excluded.push(c.dataset.userId); }); this.excludeUserMentions = excluded; console.log(this.excludeUserMentions); modal.removeModal(); }); }); } let mediaList = document.getElementsByClassName('new-tweet-media-c')[0]; let mediaObserver = new MutationObserver(async () => { if(mediaList.children.length > 0) { newTweetButton.style.marginRight = '4px'; } else { newTweetButton.style.marginRight = mentions.length > 0 ? '-50px' : '-32px'; } }); mediaObserver.observe(mediaList, {childList: true}); document.getElementsByClassName('new-tweet-view')[0].addEventListener('drop', e => { handleDrop(e, this.mediaToUpload, mediaList); }); document.getElementsByClassName('new-tweet-media-div')[0].addEventListener('click', async () => { getMedia(this.mediaToUpload, mediaList); }); let newTweetUserSearch = document.getElementsByClassName("new-tweet-user-search")[0]; let newTweetText = document.getElementsByClassName('new-tweet-text')[0]; let newTweetButton = document.getElementsByClassName('new-tweet-button')[0]; let selectedIndex = 0; newTweetText.addEventListener('paste', event => { let items = (event.clipboardData || event.originalEvent.clipboardData).items; for (let index in items) { let item = items[index]; if (item.kind === 'file') { let file = item.getAsFile(); handleFiles([file], this.mediaToUpload, document.getElementsByClassName('new-tweet-media-c')[0]); } } }); newTweetText.addEventListener('focus', async e => { setTimeout(() => { if(/(? { setTimeout(() => { newTweetUserSearch.hidden = true; }, 100); }); newTweetText.addEventListener('keypress', async e => { if ((e.key === 'Enter' || e.key === 'Tab') && !newTweetUserSearch.hidden) { let activeSearch = newTweetUserSearch.querySelector('.search-result-item-active'); if(!e.ctrlKey) { e.preventDefault(); e.stopPropagation(); newTweetText.value = newTweetText.value.split("@").slice(0, -1).join('@').split(" ").slice(0, -1).join(" ") + ` @${activeSearch.querySelector('.search-result-item-screen-name').innerText.slice(1)} `; if(newTweetText.value.startsWith(" ")) newTweetText.value = newTweetText.value.slice(1); if(newTweetText.value.length > 280) newTweetText.value = newTweetText.value.slice(0, 280); newTweetUserSearch.innerHTML = ''; newTweetUserSearch.hidden = true; } } }); newTweetText.addEventListener('keydown', async e => { if(e.key === 'ArrowDown') { if(selectedIndex < newTweetUserSearch.children.length - 1) { selectedIndex++; newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); newTweetUserSearch.children[selectedIndex - 1].classList.remove('search-result-item-active'); } else { selectedIndex = 0; newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); newTweetUserSearch.children[newTweetUserSearch.children.length - 1].classList.remove('search-result-item-active'); } return; } if(e.key === 'ArrowUp') { if(selectedIndex > 0) { selectedIndex--; newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); newTweetUserSearch.children[selectedIndex + 1].classList.remove('search-result-item-active'); } else { selectedIndex = newTweetUserSearch.children.length - 1; newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); newTweetUserSearch.children[0].classList.remove('search-result-item-active'); } return; } if(/(? { let userElement = document.createElement('span'); userElement.className = 'search-result-item'; if(index === 0) userElement.classList.add('search-result-item-active'); userElement.innerHTML = ` ${escapeHTML(user.name)} @${user.screen_name} `; userElement.addEventListener('click', () => { newTweetText.value = newTweetText.value.split("@").slice(0, -1).join('@').split(" ").slice(0, -1).join(" ") + ` @${user.screen_name} `; if(newTweetText.value.startsWith(" ")) newTweetText.value = newTweetText.value.slice(1); if(newTweetText.value.length > 280) newTweetText.value = newTweetText.value.slice(0, 280); newTweetText.focus(); newTweetUserSearch.innerHTML = ''; newTweetUserSearch.hidden = true; }); newTweetUserSearch.appendChild(userElement); }); } else { newTweetUserSearch.innerHTML = ''; newTweetUserSearch.hidden = true; } }); newTweetText.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { document.getElementsByClassName('new-tweet-button')[0].click(); } }); newTweetText.addEventListener('input', e => { let charElement = document.getElementsByClassName('new-tweet-char')[0]; let text = e.target.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); charElement.innerText = `${text.length}/280`; if(text.length > 265) { charElement.style.color = "#c26363"; } else { charElement.style.color = ""; } if (text.length > 280) { charElement.style.color = "red"; newTweetButton.disabled = true; } else { newTweetButton.disabled = false; } }); document.getElementsByClassName('new-tweet-emojis')[0].addEventListener('click', () => { createEmojiPicker(document.getElementsByClassName('new-tweet-emojis')[0], newTweetText, { marginLeft: '-300px' }); }); newTweetButton.addEventListener('click', async () => { let tweet = document.getElementsByClassName('new-tweet-text')[0].value; if (tweet.length === 0 && this.mediaToUpload.length === 0) return; document.getElementsByClassName('new-tweet-button')[0].disabled = true; let uploadedMedia = []; for (let i in this.mediaToUpload) { let media = this.mediaToUpload[i]; try { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; let mediaId = await API.uploadMedia({ media_type: media.type, media_category: media.category, media: media.data, alt: media.alt, cw: media.cw, loadCallback: data => { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; } }); uploadedMedia.push(mediaId); } catch (e) { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; console.error(e); alert(e); } } let tweetObject = { status: tweet, in_reply_to_status_id: replyTweet.id_str, }; if(this.excludeUserMentions.length > 0) tweetObject.exclude_reply_user_ids = this.excludeUserMentions.join(','); if (uploadedMedia.length > 0) { tweetObject.media_ids = uploadedMedia.join(','); } try { let tweet = await API.tweet.postV2(tweetObject); tweet._ARTIFICIAL = true; this.appendTweet(tweet, document.getElementsByClassName('timeline')[0], { after: document.getElementsByClassName('new-tweet-view')[0].parentElement }); } catch (e) { document.getElementsByClassName('new-tweet-button')[0].disabled = false; console.error(e); } document.getElementsByClassName('new-tweet-text')[0].value = ""; document.getElementsByClassName('new-tweet-media-c')[0].innerHTML = ""; this.mediaToUpload = []; this.excludeUserMentions = []; document.getElementsByClassName('new-tweet-focused')[0].hidden = true; document.getElementsByClassName('new-tweet-char')[0].hidden = true; document.getElementsByClassName('new-tweet-text')[0].classList.remove('new-tweet-text-focused'); document.getElementsByClassName('new-tweet-media-div')[0].classList.remove('new-tweet-media-div-focused'); document.getElementsByClassName('new-tweet-button')[0].disabled = false; chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); } async appendTweet(t, timelineContainer, options = {}) { if(this.seenReplies.includes(t.id_str)) return; // if(t.entities && t.entities.urls) { // let webUrl = t.entities.urls.find(u => u.expanded_url.startsWith('https://twitter.com/i/web/status/')); // if(webUrl) { // try { // let source = t.source; // t = await API.tweet.getV2(t.id_str); // t.source = source; // } catch(e) {} // } // } if(vars.twitterBlueCheckmarks && t.user.ext && t.user.ext.isBlueVerified && t.user.ext.isBlueVerified.r && t.user.ext.isBlueVerified.r.ok) { t.user.verified_type = "Blue"; } if(t.user.ext && t.user.ext.verifiedType && t.user.ext.verifiedType.r && t.user.ext.verifiedType.r.ok) { t.user.verified_type = t.user.ext.verifiedType.r.ok; } this.tweets.push(['tweet', t, options]); this.seenReplies.push(t.id_str); const tweet = document.createElement('div'); t.options = options; t.element = tweet; if(!options.mainTweet) { tweet.addEventListener('click', async e => { if(e.target.className.startsWith('tweet tweet-view tweet-id-') || e.target.classList.contains('tweet-body') || e.target.className === 'tweet-interact') { this.savePageData(); history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let restored = await this.restorePageData(); let id = location.pathname.match(/status\/(\d{1,32})/)[1]; if(this.subpage === 'tweet' && !restored) { this.updateReplies(id); } else if(this.subpage === 'likes') { this.updateLikes(id); } else if(this.subpage === 'retweets') { this.updateRetweets(id); } else if(this.subpage === 'retweets_with_comments') { this.updateRetweetsWithComments(id); } this.currentLocation = location.pathname; } }); tweet.addEventListener('mousedown', e => { if(e.button === 1) { e.preventDefault(); if(e.target.className.startsWith('tweet tweet-view tweet-id-') || e.target.classList.contains('tweet-body') || e.target.className === 'tweet-interact') { openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); } } }); } tweet.tabIndex = -1; tweet.className = `tweet tweet-view tweet-id-${t.id_str} ${options.mainTweet ? 'tweet-main' : 'tweet-replying'}`; if(!this.activeTweet) { tweet.classList.add('tweet-active'); this.activeTweet = tweet; } if (options.threadContinuation) tweet.classList.add('tweet-self-thread-continuation'); if (options.noTop) tweet.classList.add('tweet-no-top'); if(vars.linkColorsInTL) { if(this.linkColors[t.user.id_str]) { let sc = makeSeeableColor(this.linkColors[t.user.id_str]); tweet.style.setProperty('--link-color', sc); } else { if(t.user.profile_link_color && t.user.profile_link_color !== '1DA1F2') { let sc = makeSeeableColor(t.user.profile_link_color); tweet.style.setProperty('--link-color', sc); } } } let full_text = t.full_text ? t.full_text : ''; let strippedDownText = full_text .replace(/(?:https?|ftp):\/\/[\n\S]+/g, '') //links .replace(/(? 60 && detectedLanguage.languages[0].language.startsWith(LANGUAGE); let videos = t.extended_entities && t.extended_entities.media && t.extended_entities.media.filter(m => m.type === 'video'); if(!videos || videos.length === 0) { videos = undefined; } if(videos) { for(let v of videos) { if(!v.video_info) continue; v.video_info.variants = v.video_info.variants.sort((a, b) => { if(!b.bitrate) return -1; return b.bitrate-a.bitrate; }); if(typeof(vars.savePreferredQuality) !== 'boolean') { chrome.storage.sync.set({ savePreferredQuality: true }, () => {}); vars.savePreferredQuality = true; } if(localStorage.preferredQuality && vars.savePreferredQuality) { let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => { return (Math.abs(parseInt(curr.url.match(/\/(\d+)x/)[1]) - parseInt(localStorage.preferredQuality)) < Math.abs(parseInt(prev.url.match(/\/(\d+)x/)[1]) - parseInt(localStorage.preferredQuality)) ? curr : prev); }); let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url); if(preferredQualityVariantIndex !== -1) { let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex]; v.video_info.variants.splice(preferredQualityVariantIndex, 1); v.video_info.variants.unshift(preferredQualityVariant); } } } } if(t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) { full_text = ""; } if(t.quoted_status_id_str && !t.quoted_status && options.mainTweet) { //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then) try { if(t.quoted_status_result && t.quoted_status_result.result.tweet) { t.quoted_status = t.quoted_status_result.result.tweet.legacy; t.quoted_status.user = t.quoted_status_result.result.tweet.core.user_results.result.legacy; } else { t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str); } } catch { t.quoted_status = undefined; } } let followUserText, unfollowUserText, blockUserText, unblockUserText; let mentionedUserText = ``; let quoteMentionedUserText = ``; if( LOC.follow_user.message.includes('$SCREEN_NAME$') && LOC.unfollow_user.message.includes('$SCREEN_NAME$') && LOC.block_user.message.includes('$SCREEN_NAME$') && LOC.unblock_user.message.includes('$SCREEN_NAME$') ) { followUserText = `${LOC.follow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; unfollowUserText = `${LOC.unfollow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; blockUserText = `${LOC.block_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; unblockUserText = `${LOC.unblock_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; } else { followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; } if(t.in_reply_to_screen_name && t.display_text_range) { t.entities.user_mentions.forEach(user_mention => { if(user_mention.indices[0] < t.display_text_range[0]){ mentionedUserText += `@${user_mention.screen_name} ` } //else this is not reply but mention }); } if(t.quoted_status && t.quoted_status.in_reply_to_screen_name && t.display_text_range) { t.quoted_status.entities.user_mentions.forEach(user_mention => { if(user_mention.indices[0] < t.display_text_range[0]){ quoteMentionedUserText += `@${user_mention.screen_name} ` } //else this is not reply but mention }); } tweet.innerHTML = /*html*/` ${t.user.name}
${escapeHTML(t.user.name)} @${t.user.screen_name} ${options.mainTweet && t.user.id_str !== user.id_str ? `` : ''}
${timeElapsed(new Date(t.created_at).getTime())}
${vars.useOldStyleReply ? /*html*/mentionedUserText: ''}${full_text ? await renderTweetBodyHTML(full_text, t.entities, t.display_text_range) : ''} ${!isEnglish ? /*html*/`
${LOC.view_translation.message} ` : ``} ${t.extended_entities && t.extended_entities.media ? /*html*/`
${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? /*html*/`
` : ''} ${renderMedia(t)}
${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? `
GIF
` : ''} ${videos ? /*html*/`
${videos[0].ext && videos[0].ext.mediaStats && videos[0].ext.mediaStats.r && videos[0].ext.mediaStats.r.ok ? `${Number(videos[0].ext.mediaStats.r.ok.viewCount).toLocaleString().replace(/\s/g, ',')} ${LOC.views.message} • ` : ''}${LOC.reload.message} • ${videos[0].video_info.variants.filter(v => v.bitrate).map(v => `${v.url.match(/\/(\d+)x/)[1] + 'p'} `).join(" / ")}
` : ``} ` : ``} ${t.card ? `
` : ''} ${t.quoted_status ? /*html*/` ${escapeHTML(t.quoted_status.user.name)}
${escapeHTML(t.quoted_status.user.name)} @${t.quoted_status.user.screen_name}
${timeElapsed(new Date(t.quoted_status.created_at).getTime())} ${quoteMentionedUserText !== `` && !vars.useOldStyleReply ? /*html*/` ${LOC.replying_to_user.message.replace('$SCREEN_NAME$', quoteMentionedUserText.trim().replaceAll(` `, LOC.replying_to_comma.message).replace(LOC.replying_to_comma.message, LOC.replying_to_and.message))} ` : ''} ${vars.useOldStyleReply? quoteMentionedUserText : ''}${t.quoted_status.full_text ? await renderTweetBodyHTML(t.quoted_status.full_text, t.quoted_status.entities, t.quoted_status.display_text_range, true) : ''} ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? /*html*/`
${t.quoted_status.extended_entities.media.map(m => `<${m.type === 'photo' ? 'img' : 'video'} ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''} crossorigin="anonymous" width="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}" height="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}" loading="lazy" ${m.type === 'video' ? 'controls' : ''} ${m.type === 'animated_gif' ? 'loop muted onclick="if(this.paused) this.play(); else this.pause()"' : ''}${m.type === 'animated_gif' && !vars.disableGifAutoplay ? ' autoplay' : ''} src="${m.type === 'photo' ? m.media_url_https : m.video_info.variants.find(v => v.content_type === 'video/mp4').url}" class="tweet-media-element tweet-media-element-quote ${mediaClasses[t.quoted_status.extended_entities.media.length]} ${!vars.displaySensitiveContent && t.quoted_status.possibly_sensitive ? 'tweet-media-element-censor' : ''}">${m.type === 'video' ? '' : ''}`).join('\n')}
` : ''}
` : ``} ${t.limited_actions === 'limit_trusted_friends_tweet' && options.mainTweet ? `
${LOC.circle_limited_tweet.message} ${LOC.learn_more.message}
`.replace('$SCREEN_NAME$', tweetStorage[t.conversation_id_str] ? tweetStorage[t.conversation_id_str].user.screen_name : t.in_reply_to_screen_name ? t.in_reply_to_screen_name : t.user.screen_name) : ''} ${t.tombstone ? `
${t.tombstone}
` : ''} ${((t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) || t.withheld_scope) ? `
This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` : ''} ${t.conversation_control ? `
${t.limited_actions_text ? t.limited_actions_text : LOC.limited_tweet.message}${t.conversation_control.policy && (t.user.id_str === user.id_str || (t.conversation_control.policy.toLowerCase() === 'community' && (t.user.followed_by || (full_text && full_text.includes(`@${user.screen_name}`)))) || (t.conversation_control.policy.toLowerCase() === 'by_invitation' && full_text && full_text.includes(`@${user.screen_name}`))) ? ' ' + LOC.you_can_reply.message : ''}.
` : ''} ${options.mainTweet ? /*html*/` ` : ''}
${new Date(t.created_at).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }).toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}  ・ ${t.source ? t.source.split('>')[1].split('<')[0] : 'Unknown'}
${options.mainTweet ? '' : Number(t.reply_count).toLocaleString().replace(/\s/g, ',')} ${options.mainTweet ? '' : Number(t.retweet_count).toLocaleString().replace(/\s/g, ',')} ${options.mainTweet ? '' : Number(t.favorite_count).toLocaleString().replace(/\s/g, ',')} ${vars.seeTweetViews && t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? /*html*/`${Number(t.ext.views.r.ok.count).toLocaleString().replace(/\s/g, ',')}` : ''} ${t.bookmark_count && vars.showBookmarkCount && options.mainTweet ? /*html*/`${Number(t.bookmark_count).toLocaleString().replace(/\s/g, ',')}` : ''}
`; // video let vidOverlay = tweet.getElementsByClassName('tweet-media-video-overlay')[0]; if(vidOverlay) { vidOverlay.addEventListener('click', () => { let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; vid.play(); vid.controls = true; vid.classList.remove('tweet-media-element-censor'); vidOverlay.style.display = 'none'; }); } if(videos) { let vids = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO'); vids[0].onloadstart = () => { let src = vids[0].currentSrc; Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { if(el.dataset.url === src) el.classList.add('tweet-video-quality-current'); }); tweet.getElementsByClassName('tweet-video-reload')[0].addEventListener('click', () => { let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; let time = vid.currentTime; let paused = vid.paused; vid.load(); vid.onloadstart = () => { let src = vid.currentSrc; vid.currentTime = time; if(!paused) vid.play(); Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current'); else el.classList.remove('tweet-video-quality-current'); }); } }); Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => el.addEventListener('click', () => { if(el.className.includes('tweet-video-quality-current')) return; localStorage.preferredQuality = parseInt(el.innerText); let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; let time = vid.currentTime; let paused = vid.paused; for(let v of videos) { let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => { return (Math.abs(parseInt(curr.url.match(/\/(\d+)x/)[1]) - parseInt(localStorage.preferredQuality)) < Math.abs(parseInt(prev.url.match(/\/(\d+)x/)[1]) - parseInt(localStorage.preferredQuality)) ? curr : prev); }); let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url); if(preferredQualityVariantIndex !== -1) { let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex]; v.video_info.variants.splice(preferredQualityVariantIndex, 1); v.video_info.variants.unshift(preferredQualityVariant); } } tweet.getElementsByClassName('tweet-media')[0].innerHTML = /*html*/` ${t.extended_entities.media.map(m => `<${m.type === 'photo' ? 'img' : 'video'} ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''} crossorigin="anonymous" width="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}" height="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}" loading="lazy" ${m.type === 'video' ? 'controls' : ''} ${m.type === 'animated_gif' ? 'loop muted onclick="if(this.paused) this.play(); else this.pause()"' : ''}${m.type === 'animated_gif' && !vars.disableGifAutoplay ? ' autoplay' : ''} ${m.type === 'photo' ? `src="${m.media_url_https}"` : ''} class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${!vars.displaySensitiveContent && t.possibly_sensitive ? 'tweet-media-element-censor' : ''}">${m.type === 'video' || m.type === 'animated_gif' ? ` ${m.video_info.variants.map(v => ``).join('\n')} ${LOC.unsupported_video.message} ` : ''}`).join('\n')} `; vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; vid.onloadstart = () => { let src = vid.currentSrc; vid.currentTime = time; if(!paused) vid.play(); Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current'); else el.classList.remove('tweet-video-quality-current'); }); } vid.addEventListener('mousedown', e => { if(e.button === 1) { e.preventDefault(); window.open(vid.currentSrc, '_blank'); } }); })); }; for(let vid of vids) { if(typeof vars.volume === 'number') { vid.volume = vars.volume; } vid.onvolumechange = () => { chrome.storage.sync.set({ volume: vid.volume }, () => { }); let allVids = document.getElementsByTagName('video'); for(let i = 0; i < allVids.length; i++) { allVids[i].volume = vid.volume; } }; vid.addEventListener('mousedown', e => { if(e.button === 1) { e.preventDefault(); window.open(vid.currentSrc, '_blank'); } }); } } let footerFavorites = tweet.getElementsByClassName('tweet-footer-favorites')[0]; if(t.card) { generateCard(t, tweet, user); } if (options.top) { tweet.querySelector('.tweet-top').hidden = false; const icon = document.createElement('span'); icon.innerText = options.top.icon; icon.classList.add('tweet-top-icon'); icon.style.color = options.top.color; const span = document.createElement("span"); span.classList.add("tweet-top-text"); span.innerHTML = options.top.text; tweet.querySelector('.tweet-top').append(icon, span); } if(options.mainTweet) { let likers = this.mainTweetLikers.slice(0, 8); for(let i in likers) { let liker = likers[i]; let a = document.createElement('a'); a.href = `https://twitter.com/${liker.screen_name}`; let likerImg = document.createElement('img'); likerImg.src = `${(liker.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(liker.id_str) % 7}_normal.png`): liker.profile_image_url_https}`; likerImg.classList.add('tweet-footer-favorites-img'); likerImg.title = liker.name + ' (@' + liker.screen_name + ')'; likerImg.width = 24; likerImg.height = 24; a.appendChild(likerImg); a.dataset.id = liker.id_str; footerFavorites.appendChild(a); } let likesLink = tweet.getElementsByClassName('tweet-footer-stat-f')[0]; likesLink.addEventListener('click', e => { e.preventDefault(); history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/likes`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateLikes(id); this.currentLocation = location.pathname; }); let retweetsLink = tweet.getElementsByClassName('tweet-footer-stat-r')[0]; retweetsLink.addEventListener('click', e => { e.preventDefault(); history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateRetweets(id); this.currentLocation = location.pathname; }); let repliesLink = tweet.getElementsByClassName('tweet-footer-stat-o')[0]; repliesLink.addEventListener('click', e => { e.preventDefault(); if(location.href === `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`) return; history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateReplies(id); this.currentLocation = location.pathname; }); } if(options.mainTweet && t.user.id_str !== user.id_str) { const tweetFollow = tweet.getElementsByClassName('tweet-header-follow')[0]; tweetFollow.addEventListener('click', async () => { if(t.user.following) { await API.user.unfollow(t.user.screen_name); tweetFollow.innerText = LOC.follow.message; tweetFollow.classList.remove('following'); tweetFollow.classList.add('follow'); t.user.following = false; } else { await API.user.follow(t.user.screen_name); tweetFollow.innerText = LOC.unfollow.message; tweetFollow.classList.remove('follow'); tweetFollow.classList.add('following'); t.user.following = true; } }); } const tweetBody = tweet.getElementsByClassName('tweet-body')[0]; const tweetBodyText = tweet.getElementsByClassName('tweet-body-text')[0]; const tweetTranslate = tweet.getElementsByClassName('tweet-translate')[0]; const tweetBodyQuote = tweet.getElementsByClassName('tweet-body-quote')[0]; const tweetBodyQuoteText = tweet.getElementsByClassName('tweet-body-text-quote')[0]; const tweetReplyCancel = tweet.getElementsByClassName('tweet-reply-cancel')[0]; const tweetReplyUpload = tweet.getElementsByClassName('tweet-reply-upload')[0]; const tweetReplyAddEmoji = tweet.getElementsByClassName('tweet-reply-add-emoji')[0]; const tweetReply = tweet.getElementsByClassName('tweet-reply')[0]; const tweetReplyButton = tweet.getElementsByClassName('tweet-reply-button')[0]; const tweetReplyError = tweet.getElementsByClassName('tweet-reply-error')[0]; const tweetReplyText = tweet.getElementsByClassName('tweet-reply-text')[0]; const tweetReplyChar = tweet.getElementsByClassName('tweet-reply-char')[0]; const tweetReplyMedia = tweet.getElementsByClassName('tweet-reply-media')[0]; const tweetInteract = tweet.getElementsByClassName('tweet-interact')[0]; const tweetInteractReply = tweet.getElementsByClassName('tweet-interact-reply')[0]; const tweetInteractRetweet = tweet.getElementsByClassName('tweet-interact-retweet')[0]; const tweetInteractFavorite = tweet.getElementsByClassName('tweet-interact-favorite')[0]; const tweetInteractBookmark = tweet.getElementsByClassName('tweet-interact-bookmark')[0]; const tweetInteractMore = tweet.getElementsByClassName('tweet-interact-more')[0]; const tweetFooter = tweet.getElementsByClassName('tweet-footer')[0]; const tweetFooterReplies = tweet.getElementsByClassName('tweet-footer-stat-replies')[0]; const tweetFooterRetweets = tweet.getElementsByClassName('tweet-footer-stat-retweets')[0]; const tweetFooterFavorites = tweet.getElementsByClassName('tweet-footer-stat-favorites')[0]; const tweetQuote = tweet.getElementsByClassName('tweet-quote')[0]; const tweetQuoteCancel = tweet.getElementsByClassName('tweet-quote-cancel')[0]; const tweetQuoteUpload = tweet.getElementsByClassName('tweet-quote-upload')[0]; const tweetQuoteAddEmoji = tweet.getElementsByClassName('tweet-quote-add-emoji')[0]; const tweetQuoteButton = tweet.getElementsByClassName('tweet-quote-button')[0]; const tweetQuoteError = tweet.getElementsByClassName('tweet-quote-error')[0]; const tweetQuoteText = tweet.getElementsByClassName('tweet-quote-text')[0]; const tweetQuoteChar = tweet.getElementsByClassName('tweet-quote-char')[0]; const tweetQuoteMedia = tweet.getElementsByClassName('tweet-quote-media')[0]; const tweetInteractRetweetMenu = tweet.getElementsByClassName('tweet-interact-retweet-menu')[0]; const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweet')[0]; const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName('tweet-interact-retweet-menu-quote')[0]; const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName('tweet-interact-retweet-menu-quotes')[0]; const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweeters')[0]; const tweetInteractMoreMenu = tweet.getElementsByClassName('tweet-interact-more-menu')[0]; const tweetInteractMoreMenuCopy = tweet.getElementsByClassName('tweet-interact-more-menu-copy')[0]; const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-tweet-id')[0]; const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-user-id')[0]; const tweetInteractMoreMenuLog = tweet.getElementsByClassName('tweet-interact-more-menu-log')[0]; const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName('tweet-interact-more-menu-embed')[0]; const tweetInteractMoreMenuShare = tweet.getElementsByClassName('tweet-interact-more-menu-share')[0]; const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName('tweet-interact-more-menu-newtwitter')[0]; const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName('tweet-interact-more-menu-analytics')[0]; const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName('tweet-interact-more-menu-refresh')[0]; const tweetInteractMoreMenuMute = tweet.getElementsByClassName('tweet-interact-more-menu-mute')[0]; const tweetInteractMoreMenuDownload = tweet.getElementsByClassName('tweet-interact-more-menu-download')[0]; const tweetInteractMoreMenuDownloadGifs = Array.from(tweet.getElementsByClassName('tweet-interact-more-menu-download-gif')); const tweetInteractMoreMenuDelete = tweet.getElementsByClassName('tweet-interact-more-menu-delete')[0]; const tweetInteractMoreMenuFollow = tweet.getElementsByClassName('tweet-interact-more-menu-follow')[0]; const tweetInteractMoreMenuBlock = tweet.getElementsByClassName('tweet-interact-more-menu-block')[0]; const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName('tweet-interact-more-menu-bookmark')[0]; const tweetInteractMoreMenuHide = tweet.getElementsByClassName('tweet-interact-more-menu-hide')[0]; if(tweetInteractMoreMenuLog) tweetInteractMoreMenuLog.addEventListener('click', () => { console.log(t); }); // moderating tweets if(tweetInteractMoreMenuHide) tweetInteractMoreMenuHide.addEventListener('click', async () => { if(t.moderated) { try { await API.tweet.unmoderate(t.id_str); } catch(e) { console.error(e); alert(e); return; } tweetInteractMoreMenuHide.innerText = LOC.hide_tweet.message; t.moderated = false; } else { let sure = confirm(LOC.hide_tweet_sure.message); if(!sure) return; try { await API.tweet.moderate(t.id_str); } catch(e) { console.error(e); alert(e); return; } tweetInteractMoreMenuHide.innerText = LOC.unhide_tweet.message; t.moderated = true; } }); // community notes if(t.birdwatch && options.mainTweet && !vars.hideCommunityNotes) { let div = document.createElement('div'); div.classList.add('tweet-birdwatch', 'box'); let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); for(let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) { let entity = t.birdwatch.subtitle.entities[e]; if(!entity.ref) continue; text = arrayInsert(text, entity.toIndex, ''); text = arrayInsert(text, entity.fromIndex, ``); } text = text.join(''); div.innerHTML = /*html*/`
${escapeHTML(t.birdwatch.title)}
${text}
`; if(tweetFooter) tweetFooter.before(div); else tweetInteract.before(div); } // rtl languages if(rtlLanguages.includes(t.lang)) { tweetBody.classList.add('rtl'); } // Quote body if(tweetBodyQuote) { tweetBodyQuote.addEventListener('click', e => { e.preventDefault(); history.pushState({}, null, `https://twitter.com/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; if(this.subpage === 'tweet') { this.updateReplies(id); } else if(this.subpage === 'likes') { this.updateLikes(id); } else if(this.subpage === 'retweets') { this.updateRetweets(id); } else if(this.subpage === 'retweets_with_comments') { this.updateRetweetsWithComments(id); } this.currentLocation = location.pathname; }); if(rtlLanguages.includes(t.quoted_status.lang)) { tweetBodyQuoteText.classList.add('rtl'); } else { tweetBodyQuoteText.classList.add('ltr'); } } // Translate if(tweetTranslate) tweetTranslate.addEventListener('click', async () => { let translated = await API.tweet.translate(t.id_str); tweetTranslate.hidden = true; let translatedMessage; if(LOC.translated_from.message.includes("$LANGUAGE$")) { translatedMessage = LOC.translated_from.message.replace("$LANGUAGE$", `[${translated.translated_lang}]`); } else { translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; } tweetBodyText.innerHTML += `
`+ `${translatedMessage}:`+ `
`+ `${await renderTweetBodyHTML(translated.text, translated.entities)}`; if(vars.enableTwemoji) twemoji.parse(tweetBodyText); }); // Bookmarks let switchingBookmark = false; let switchBookmark = () => { if(switchingBookmark) return; switchingBookmark = true; chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); if(t.bookmarked) { API.bookmarks.delete(t.id_str).then(() => { toast.info(LOC.unbookmarked_tweet.message); switchingBookmark = false; t.bookmarked = false; t.bookmark_count--; tweetInteractMoreMenuBookmark.innerText = LOC.bookmark_tweet.message; if(tweetInteractBookmark) { tweetInteractBookmark.classList.remove('tweet-interact-bookmarked'); if(vars.bookmarkButton !== 'show_all_no_count') { tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ','); } } }).catch(e => { switchingBookmark = false; console.error(e); alert(e); }); } else { API.bookmarks.create(t.id_str).then(() => { toast.info(LOC.bookmarked_tweet.message); switchingBookmark = false; t.bookmarked = true; t.bookmark_count++; tweetInteractMoreMenuBookmark.innerText = LOC.remove_bookmark.message; if(tweetInteractBookmark) { tweetInteractBookmark.classList.add('tweet-interact-bookmarked'); if(vars.bookmarkButton !== 'show_all_no_count') { tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ','); } } }).catch(e => { switchingBookmark = false; console.error(e); alert(e); }); } }; if(tweetInteractBookmark) tweetInteractBookmark.addEventListener('click', switchBookmark); if(tweetInteractMoreMenuBookmark) tweetInteractMoreMenuBookmark.addEventListener('click', switchBookmark); // Media if (t.extended_entities && t.extended_entities.media) { const tweetMedia = tweet.getElementsByClassName('tweet-media')[0]; tweetMedia.addEventListener('click', e => { if (e.target.className && e.target.className.includes('tweet-media-element-censor')) { return e.target.classList.remove('tweet-media-element-censor'); } if (e.target.tagName === 'IMG') { if(!e.target.src.endsWith('?name=orig') && !e.target.src.startsWith('data:')) { e.target.src += '?name=orig'; } new Viewer(tweetMedia, { transition: false }); e.target.click(); } }); } // Emojis [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach(e => { e.addEventListener('click', e => { let isReply = e.target.className === 'tweet-reply-add-emoji'; createEmojiPicker(isReply ? tweetReply : tweetQuote, isReply ? tweetReplyText : tweetQuoteText, {}); }); }); // Reply tweetReplyCancel.addEventListener('click', () => { tweetReply.hidden = true; tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); }); let replyMedia = []; tweetReply.addEventListener('drop', e => { handleDrop(e, replyMedia, tweetReplyMedia); }); tweetReply.addEventListener('paste', event => { let items = (event.clipboardData || event.originalEvent.clipboardData).items; for (let index in items) { let item = items[index]; if (item.kind === 'file') { let file = item.getAsFile(); handleFiles([file], replyMedia, tweetReplyMedia); } } }); tweetReplyUpload.addEventListener('click', () => { getMedia(replyMedia, tweetReplyMedia); }); tweetInteractReply.addEventListener('click', () => { if(options.mainTweet) { document.getElementsByClassName('new-tweet-view')[0].click(); document.getElementsByClassName('new-tweet-text')[0].focus(); return; } if (!tweetQuote.hidden) tweetQuote.hidden = true; if (tweetReply.hidden) { tweetInteractReply.classList.add('tweet-interact-reply-clicked'); } else { tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); } tweetReply.hidden = !tweetReply.hidden; setTimeout(() => { tweetReplyText.focus(); }) }); tweetReplyText.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { tweetReplyButton.click(); } }); tweetReplyText.addEventListener('input', e => { let text = tweetReplyText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); tweetReplyChar.innerText = `${text.length}/280`; if(text.length > 265) { tweetReplyChar.style.color = "#c26363"; } else { tweetReplyChar.style.color = ""; } if (text.length > 280) { tweetReplyChar.style.color = "red"; tweetReplyButton.disabled = true; } else { tweetReplyButton.disabled = false; } }); tweetReplyButton.addEventListener('click', async () => { tweetReplyError.innerHTML = ''; let text = tweetReplyText.value; if (text.length === 0 && replyMedia.length === 0) return; tweetReplyButton.disabled = true; let uploadedMedia = []; for (let i in replyMedia) { let media = replyMedia[i]; try { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; let mediaId = await API.uploadMedia({ media_type: media.type, media_category: media.category, media: media.data, alt: media.alt, cw: media.cw, loadCallback: data => { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; } }); uploadedMedia.push(mediaId); } catch (e) { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; console.error(e); alert(e); } } let tweetObject = { status: text, in_reply_to_status_id: t.id_str }; if (uploadedMedia.length > 0) { tweetObject.media_ids = uploadedMedia.join(','); } let tweetData; try { tweetData = await API.tweet.postV2(tweetObject) } catch (e) { tweetReplyError.innerHTML = (e && e.message ? e.message : e) + "
"; tweetReplyButton.disabled = false; return; } if (!tweetData) { tweetReplyButton.disabled = false; tweetReplyError.innerHTML = `${LOC.error_sending_tweet.message}
`; return; } tweetReplyText.value = ''; tweetReplyChar.innerText = '0/280'; tweetReply.hidden = true; tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); if(!options.mainTweet) { tweetInteractReply.dataset.val = parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; tweetInteractReply.innerText = Number(parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } else { tweetFooterReplies.dataset.val = parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; tweetFooterReplies.innerText = Number(parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } tweetData._ARTIFICIAL = true; chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); if(tweet.getElementsByClassName('tweet-self-thread-div')[0]) tweet.getElementsByClassName('tweet-self-thread-div')[0].hidden = false; tweetReplyButton.disabled = false; tweetReplyMedia.innerHTML = []; replyMedia = []; this.appendTweet(tweetData, document.getElementsByClassName('timeline')[0], { noTop: true, after: tweet }); }); // Retweet / Quote Tweet let retweetClicked = false; tweetQuoteCancel.addEventListener('click', () => { tweetQuote.hidden = true; }); tweetInteractRetweet.addEventListener('click', async () => { if(tweetInteractRetweet.classList.contains('tweet-interact-retweet-disabled')) { return; } if (!tweetQuote.hidden) { tweetQuote.hidden = true; return; } if (tweetInteractRetweetMenu.hidden) { tweetInteractRetweetMenu.hidden = false; } if(retweetClicked) return; retweetClicked = true; setTimeout(() => { document.body.addEventListener('click', () => { retweetClicked = false; setTimeout(() => tweetInteractRetweetMenu.hidden = true, 50); }, { once: true }); }, 50); }); tweetInteractRetweetMenuRetweet.addEventListener('click', async () => { if (!t.retweeted) { let tweetData; try { tweetData = await API.tweet.retweet(t.id_str); } catch (e) { console.error(e); return; } if (!tweetData) { return; } tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message; tweetInteractRetweet.classList.add('tweet-interact-retweeted'); t.retweeted = true; t.newTweetId = tweetData.id_str; if(!options.mainTweet) { tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } else { tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } let event = new CustomEvent('tweetAction', { detail: { action: 'retweet', tweet: t, tweetData } }); document.dispatchEvent(event); } else { let tweetData; try { tweetData = await API.tweet.unretweet(t.retweeted_status ? t.retweeted_status.id_str : t.id_str); } catch (e) { console.error(e); return; } if (!tweetData) { return; } tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; tweetInteractRetweet.classList.remove('tweet-interact-retweeted'); t.retweeted = false; if(!options.mainTweet) { tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1; tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); } else { tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); } delete t.newTweetId; let event = new CustomEvent('tweetAction', { detail: { action: 'unretweet', tweet: t, tweetData } }); document.dispatchEvent(event); } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); if(options.mainTweet) { tweetInteractRetweetMenuQuotes.addEventListener('click', async () => { history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; if(this.subpage === 'tweet') { this.updateReplies(id); } else if(this.subpage === 'likes') { this.updateLikes(id); } else if(this.subpage === 'retweets') { this.updateRetweets(id); } else if(this.subpage === 'retweets_with_comments') { this.updateRetweetsWithComments(id); } this.currentLocation = location.pathname; }); tweetInteractRetweetMenuRetweeters.addEventListener('click', async () => { history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`); this.updateSubpage(); this.mediaToUpload = []; this.excludeUserMentions = []; this.linkColors = {}; this.cursor = undefined; this.seenReplies = []; this.mainTweetLikers = []; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; if(this.subpage === 'tweet') { this.updateReplies(id); } else if(this.subpage === 'likes') { this.updateLikes(id); } else if(this.subpage === 'retweets') { this.updateRetweets(id); } else if(this.subpage === 'retweets_with_comments') { this.updateRetweetsWithComments(id); } this.currentLocation = location.pathname; }); } tweetInteractRetweetMenuQuote.addEventListener('click', async () => { if (!tweetReply.hidden) { tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); tweetReply.hidden = true; } tweetQuote.hidden = false; setTimeout(() => { tweetQuoteText.focus(); }) }); let quoteMedia = []; tweetQuote.addEventListener('drop', e => { handleDrop(e, quoteMedia, tweetQuoteMedia); }); tweetQuote.addEventListener('paste', event => { let items = (event.clipboardData || event.originalEvent.clipboardData).items; for (let index in items) { let item = items[index]; if (item.kind === 'file') { let file = item.getAsFile(); handleFiles([file], quoteMedia, tweetQuoteMedia); } } }); tweetQuoteUpload.addEventListener('click', () => { getMedia(quoteMedia, tweetQuoteMedia); }); tweetQuoteText.addEventListener('keydown', e => { if (e.key === 'Enter' && e.ctrlKey) { tweetQuoteButton.click(); } }); tweetQuoteText.addEventListener('input', e => { let text = tweetQuoteText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); tweetQuoteChar.innerText = `${text.length}/280`; if(text.length > 265) { tweetQuoteChar.style.color = "#c26363"; } else { tweetQuoteChar.style.color = ""; } if (text.length > 280) { tweetQuoteChar.style.color = "red"; tweetQuoteButton.disabled = true; } else { tweetQuoteButton.disabled = false; } }); tweetQuoteButton.addEventListener('click', async () => { let text = tweetQuoteText.value; tweetQuoteError.innerHTML = ''; if (text.length === 0 && quoteMedia.length === 0) return; tweetQuoteButton.disabled = true; let uploadedMedia = []; for (let i in quoteMedia) { let media = quoteMedia[i]; try { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; let mediaId = await API.uploadMedia({ media_type: media.type, media_category: media.category, media: media.data, alt: media.alt, cw: media.cw, loadCallback: data => { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; } }); uploadedMedia.push(mediaId); } catch (e) { media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; console.error(e); alert(e); } } let tweetObject = { status: text, attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}` }; if (uploadedMedia.length > 0) { tweetObject.media_ids = uploadedMedia.join(','); } let tweetData; try { tweetData = await API.tweet.postV2(tweetObject) } catch (e) { tweetQuoteError.innerHTML = (e && e.message ? e.message : e) + "
"; tweetQuoteButton.disabled = false; return; } if (!tweetData) { tweetQuoteError.innerHTML = `${LOC.error_sending_tweet.message}
`; tweetQuoteButton.disabled = false; return; } tweetQuoteText.value = ''; tweetQuote.hidden = true; tweetData._ARTIFICIAL = true; quoteMedia = []; tweetQuoteChar.innerText = '0/280'; tweetQuoteButton.disabled = false; tweetQuoteMedia.innerHTML = ''; chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); this.appendTweet(tweetData, timelineContainer, { prepend: true }); }); // Favorite tweetInteractFavorite.addEventListener('click', () => { if (t.favorited) { API.tweet.unfavorite(t.id_str); t.favorited = false; t.favorite_count--; if(!options.mainTweet) { tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1; tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); } else { if(this.mainTweetLikers.find(liker => liker.id_str === user.id_str)) { this.mainTweetLikers.splice(this.mainTweetLikers.findIndex(liker => liker.id_str === user.id_str), 1); let likerImg = footerFavorites.querySelector(`a[data-id="${user.id_str}"]`); if(likerImg) likerImg.remove() } tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); } tweetInteractFavorite.classList.remove('tweet-interact-favorited'); let event = new CustomEvent('tweetAction', { detail: { action: 'unfavorite', tweet: t } }); document.dispatchEvent(event); } else { API.tweet.favorite(t.id_str); t.favorited = true; t.favorite_count++; if(!options.mainTweet) { tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } else { if(footerFavorites.children.length < 8 && !this.mainTweetLikers.find(liker => liker.id_str === user.id_str)) { let a = document.createElement('a'); a.href = `https://twitter.com/${user.screen_name}`; let likerImg = document.createElement('img'); likerImg.src = `${(user.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(user.id_str) % 7}_normal.png`): user.profile_image_url_https}` ; likerImg.classList.add('tweet-footer-favorites-img'); likerImg.title = user.name + ' (@' + user.screen_name + ')'; likerImg.width = 24; likerImg.height = 24; a.dataset.id = user.id_str; a.appendChild(likerImg); footerFavorites.appendChild(a); this.mainTweetLikers.push(user); } tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); } tweetInteractFavorite.classList.add('tweet-interact-favorited'); let event = new CustomEvent('tweetAction', { detail: { action: 'favorite', tweet: t }}); document.dispatchEvent(event); } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); // More let moreClicked = false; tweetInteractMore.addEventListener('click', () => { if (tweetInteractMoreMenu.hidden) { tweetInteractMoreMenu.hidden = false; } if(moreClicked) return; moreClicked = true; setTimeout(() => { document.body.addEventListener('click', () => { moreClicked = false; setTimeout(() => tweetInteractMoreMenu.hidden = true, 50); }, { once: true }); }, 50); }); if(tweetInteractMoreMenuFollow) tweetInteractMoreMenuFollow.addEventListener('click', async () => { if (t.user.following) { await API.user.unfollow(t.user.screen_name); t.user.following = false; if(LOC.follow_user.message.includes("$SCREEN_NAME$")) { tweetInteractMoreMenuFollow.innerText = LOC.follow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; } let event = new CustomEvent('tweetAction', { detail: { action: 'unfollow', tweet: t } }); document.dispatchEvent(event); } else { await API.user.follow(t.user.screen_name); t.user.following = true; if(LOC.unfollow_user.message.includes("$SCREEN_NAME$")) { tweetInteractMoreMenuFollow.innerText = LOC.unfollow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { tweetInteractMoreMenuFollow.innerText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; } let event = new CustomEvent('tweetAction', { detail: { action: 'follow', tweet: t } }); document.dispatchEvent(event); } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); if(tweetInteractMoreMenuBlock) tweetInteractMoreMenuBlock.addEventListener('click', async () => { if (t.user.blocking) { await API.user.unblock(t.user.id_str); t.user.blocking = false; if(LOC.block_user.message.includes("$SCREEN_NAME$")) { tweetInteractMoreMenuBlock.innerText = LOC.block_user.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; } tweetInteractMoreMenuFollow.hidden = false; let event = new CustomEvent('tweetAction', { detail: { action: 'unblock', tweet: t } }); document.dispatchEvent(event); } else { let blockMessage; if(LOC.block_sure.message.includes("$SCREEN_NAME$")) { blockMessage = LOC.block_sure.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; } let c = confirm(blockMessage); if (!c) return; await API.user.block(t.user.id_str); t.user.blocking = true; if(LOC.unblock_user.message.includes("$SCREEN_NAME$")) { tweetInteractMoreMenuBlock.innerText = LOC.unblock_user.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; } tweetInteractMoreMenuFollow.hidden = true; t.user.following = false; if(LOC.follow_user.message.includes("$SCREEN_NAME$")) { tweetInteractMoreMenuFollow.innerText = LOC.follow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); } else { tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; } let event = new CustomEvent('tweetAction', { detail: { action: 'block', tweet: t } }); document.dispatchEvent(event); } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); tweetInteractMoreMenuCopy.addEventListener('click', () => { navigator.clipboard.writeText(`https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}`); }); if(tweetInteractMoreMenuCopyTweetId) tweetInteractMoreMenuCopyTweetId.addEventListener('click', () => { navigator.clipboard.writeText(t.id_str); }); if(tweetInteractMoreMenuCopyUserId) tweetInteractMoreMenuCopyUserId.addEventListener('click', () => { navigator.clipboard.writeText(t.user.id_str); }); if(tweetInteractMoreMenuShare) tweetInteractMoreMenuShare.addEventListener('click', () => { navigator.share({ url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}` }); }); tweetInteractMoreMenuNewtwitter.addEventListener('click', () => { openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}?newtwitter=true`); }); tweetInteractMoreMenuEmbed.addEventListener('click', () => { openInNewTab(`https://publish.twitter.com/?query=https://twitter.com/${t.user.screen_name}/status/${t.id_str}&widget=Tweet`); }); if (t.user.id_str === user.id_str) { tweetInteractMoreMenuAnalytics.addEventListener('click', () => { openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true`); }); tweetInteractMoreMenuDelete.addEventListener('click', async () => { let sure = confirm(LOC.delete_sure.message); if (!sure) return; try { await API.tweet.delete(t.id_str); } catch (e) { alert(e); console.error(e); return; } Array.from(document.getElementsByClassName('timeline')[0].getElementsByClassName(`tweet-id-${t.id_str}`)).forEach(tweet => { tweet.remove(); }); if(document.getElementById('timeline')) Array.from(document.getElementById('timeline').getElementsByClassName(`tweet-id-${t.id_str}`)).forEach(tweet => { tweet.remove(); }); if(options.mainTweet) { let tweets = Array.from(timelineContainer.getElementsByClassName('tweet')); if(tweets.length === 0) { document.getElementsByClassName('modal-close')[0].click(); } else { tweets[0].click(); } } if(typeof timeline !== 'undefined') { timeline.data = timeline.data.filter(tweet => tweet.id_str !== t.id_str); } if(options.after) { if(options.after.getElementsByClassName('tweet-self-thread-div')[0]) options.after.getElementsByClassName('tweet-self-thread-div')[0].hidden = true; if(!options.after.classList.contains('tweet-main')) options.after.getElementsByClassName('tweet-interact-reply')[0].innerText = (+options.after.getElementsByClassName('tweet-interact-reply')[0].innerText - 1).toString(); else options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText = (+options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText - 1).toString(); } let event = new CustomEvent('tweetAction', { detail: { action: 'delete', tweet: t } }); document.dispatchEvent(event); chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); } tweetInteractMoreMenuMute.addEventListener('click', async () => { if(t.conversation_muted) { await API.tweet.unmute(t.id_str); toast.info(LOC.unmuted_convo.message); t.conversation_muted = false; tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; let event = new CustomEvent('tweetAction', { detail: { action: 'unmute', tweet: t } }); document.dispatchEvent(event); } else { await API.tweet.mute(t.id_str); toast.info(LOC.muted_convo.message); t.conversation_muted = true; tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; let event = new CustomEvent('tweetAction', { detail: { action: 'mute', tweet: t } }); document.dispatchEvent(event); } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); tweetInteractMoreMenuRefresh.addEventListener('click', async () => { let tweetData; try { tweetData = await API.tweet.getV2(t.id_str); } catch (e) { console.error(e); return; } if (!tweetData) { return; } if (tweetInteractFavorite.className.includes('tweet-interact-favorited') && !tweetData.favorited) { tweetInteractFavorite.classList.remove('tweet-interact-favorited'); } if (tweetInteractRetweet.className.includes('tweet-interact-retweeted') && !tweetData.retweeted) { tweetInteractRetweet.classList.remove('tweet-interact-retweeted'); } if (!tweetInteractFavorite.className.includes('tweet-interact-favorited') && tweetData.favorited) { tweetInteractFavorite.classList.add('tweet-interact-favorited'); } if (!tweetInteractRetweet.className.includes('tweet-interact-retweeted') && tweetData.retweeted) { tweetInteractRetweet.classList.add('tweet-interact-retweeted'); } if(!options.mainTweet) { tweetInteractFavorite.innerText = tweetData.favorite_count; tweetInteractRetweet.innerText = tweetData.retweet_count; tweetInteractReply.innerText = tweetData.reply_count; } chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); }); let downloading = false; if (t.extended_entities && t.extended_entities.media.length === 1) { tweetInteractMoreMenuDownload.addEventListener('click', () => { if (downloading) return; downloading = true; let media = t.extended_entities.media[0]; let url = media.type === 'photo' ? media.media_url_https : media.video_info.variants[0].url; fetch(url).then(res => res.blob()).then(blob => { downloading = false; let a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = media.type === 'photo' ? media.media_url_https.split('/').pop() : media.video_info.variants[0].url.split('/').pop(); a.download = a.download.split('?')[0]; a.click(); a.remove(); }).catch(e => { downloading = false; console.error(e); }); }); } if (t.extended_entities && t.extended_entities.media.some(m => m.type === 'animated_gif')) { tweetInteractMoreMenuDownloadGifs.forEach(dgb => dgb.addEventListener('click', e => { if (downloading) return; downloading = true; let n = parseInt(e.target.dataset.gifno)-1; let videos = Array.from(tweet.getElementsByClassName('tweet-media-gif')); let video = videos[n]; let canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; let ctx = canvas.getContext('2d'); if (video.duration > 10 && !confirm(LOC.long_vid.message)) { return downloading = false; } let mde = tweet.getElementsByClassName('tweet-media-data')[0]; mde.innerText = LOC.initialization.message + '...'; let gif = new GIF({ workers: 4, quality: 15, debug: true }); video.currentTime = 0; video.loop = false; let isFirst = true; let interval = setInterval(async () => { if(isFirst) { video.currentTime = 0; isFirst = false; await sleep(5); } mde.innerText = `${LOC.initialization.message}... (${Math.round(video.currentTime/video.duration*100|0)}%)`; if (video.currentTime+0.1 >= video.duration) { clearInterval(interval); gif.on('working', (frame, frames) => { mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; }); gif.on('finished', blob => { mde.innerText = ''; let a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `${t.id_str}.gif`; document.body.append(a); a.click(); a.remove(); downloading = false; video.loop = true; video.play(); }); gif.render(); return; } ctx.drawImage(video, 0, 0, canvas.width, canvas.height); let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); gif.addFrame(imgData, { delay: 100 }); }, 100); })); } if(options.after) { options.after.after(tweet); } else if (options.before) { options.before.before(tweet); } else if (options.prepend) { timelineContainer.prepend(tweet); } else { timelineContainer.append(tweet); } if(vars.enableTwemoji) twemoji.parse(tweet); return tweet; } async popstateChange(that) { that.savePageData(that.currentLocation); that.updateSubpage(); if(location.pathname.includes("retweets/with_comments") && that.subpage === 'retweets_with_comments' && document.getElementById("this-is-tweet-page")) { return document.querySelector('.modal-close').click(); } that.mediaToUpload = []; that.excludeUserMentions = []; that.linkColors = {}; that.cursor = undefined; that.seenReplies = []; that.mainTweetLikers = []; let id; try { id = location.pathname.match(/status\/(\d{1,32})/)[1]; } catch(e) { return that.container.getElementsByClassName('modal-close')[0].click(); } let restored = await that.restorePageData(); if(!restored) { if(that.subpage === 'tweet') { that.updateReplies(id); } else if(that.subpage === 'likes') { that.updateLikes(id); } else if(that.subpage === 'retweets') { that.updateRetweets(id); } else if(that.subpage === 'retweets_with_comments') { that.updateRetweetsWithComments(id); } } else { this.container.scrollTop = restored.scrollY; } that.currentLocation = location.pathname; } async onScroll(that) { if(this.container.scrollTop + 300 > this.container.scrollHeight - this.container.clientHeight && !that.loadingNewTweets) { if(this.moreBtn && that.subpage === 'tweet' && !this.moreBtn.hidden) { this.moreBtn.click(); } } } async appendTombstone(timelineContainer, text) { this.tweets.push(['tombstone', text]); let tombstone = document.createElement('div'); tombstone.className = 'tweet-tombstone'; tombstone.innerHTML = text; timelineContainer.append(tombstone); } init() { document.getElementsByClassName('timeline-more')[0].addEventListener('click', async e => { if (!this.cursor || this.loadingNewTweets) return; this.loadingNewTweets = true; e.target.innerText = LOC.loading_tweets.message; let path = location.pathname; if(path.endsWith('/')) path = path.slice(0, -1); this.updateReplies(path.split('/').slice(-1)[0], this.cursor); }); document.getElementsByClassName('likes-more')[0].addEventListener('click', async () => { if(!this.likeCursor) return; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateLikes(id, this.likeCursor); }); document.getElementsByClassName('retweets-more')[0].addEventListener('click', async () => { if(!this.retweetCursor) return; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateRetweets(id, this.retweetCursor); }); document.getElementsByClassName('retweets_with_comments-more')[0].addEventListener('click', async () => { if(!this.retweetCommentsCursor) return; let id = location.pathname.match(/status\/(\d{1,32})/)[1]; this.updateRetweetsWithComments(id, this.retweetCommentsCursor); }); this.updateSubpage(); let id = location.pathname.match(/status\/(\d{1,32})/)[1]; if(this.subpage === 'tweet') { this.updateReplies(id); } else if(this.subpage === 'likes') { this.updateLikes(id); } else if(this.subpage === 'retweets') { this.updateRetweets(id); } else if(this.subpage === 'retweets_with_comments') { this.updateRetweetsWithComments(id); } this.popstateHelper = () => this.popstateChange(this); this.scrollHelper = () => this.onScroll(this); window.addEventListener("popstate", this.popstateHelper); this.container.addEventListener("scroll", this.scrollHelper, { passive: true }); } close() { document.removeEventListener('scroll', this.onscroll); window.removeEventListener("popstate", this.popstateHelper); this.container.removeEventListener("scroll", this.scrollHelper); } }