Extension to return old Twitter layout from 2015.
at master 133 kB view raw
1class TweetViewer { 2 constructor(user, tweetData) { 3 let previousLocation = location.pathname + location.search; 4 5 this.container = createModal(/*html*/` 6 <div class="tweet-viewer-loading"> 7 <img src="${chrome.runtime.getURL(`images/loading.svg`)}" width="64" height="64"> 8 </div> 9 <div class="timeline" hidden></div> 10 <div class="retweets" class="box" hidden></div> 11 <div class="retweets_with_comments" hidden></div> 12 <div class="likes" class="box" hidden></div> 13 <div class="timeline-more center-text" hidden>${LOC.load_more.message}</div> 14 <div class="retweets-more center-text" hidden>${LOC.load_more.message}</div> 15 <div class="retweets_with_comments-more center-text" hidden>${LOC.load_more.message}</div> 16 <div class="likes-more center-text" hidden>${LOC.load_more.message}</div> 17 `, 'tweet-viewer', () => { 18 this.close(); 19 history.pushState({}, null, previousLocation); 20 }); 21 this.tweetData = tweetData; 22 this.id = tweetData.id_str; 23 history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${this.id}`); 24 25 this.user = user; 26 this.loadingNewTweets = false; 27 this.lastTweetDate = 0; 28 this.activeTweet; 29 this.pageData = {}; 30 this.tweets = []; 31 this.cursor = undefined; 32 this.mediaToUpload = []; 33 this.excludeUserMentions = []; 34 this.users = {}; 35 this.linkColors = {}; 36 this.likeCursor = undefined; 37 this.retweetCursor = undefined; 38 this.retweetCommentsCursor = undefined; 39 this.seenReplies = []; 40 this.mainTweetLikers = []; 41 this.currentLocation = location.pathname; 42 this.subpage = undefined; 43 this.popstateHelper = undefined; 44 this.scrollHelper = undefined; 45 this.timelineElement = this.container.getElementsByClassName('timeline')[0]; 46 this.moreBtn = this.container.getElementsByClassName('timeline-more')[0]; 47 48 let event = new CustomEvent('clearActiveTweet'); 49 document.dispatchEvent(event); 50 51 chrome.storage.sync.get(['viewedtweets'], (result) => { 52 if(!result.viewedtweets) result.viewedtweets = []; 53 result.viewedtweets.unshift(this.id); 54 result.viewedtweets = [...new Set(result.viewedtweets)]; 55 while(result.viewedtweets.length >= 100) { 56 result.viewedtweets.pop(); 57 } 58 chrome.storage.sync.set({ viewedtweets: result.viewedtweets }); 59 }); 60 61 this.init(); 62 } 63 async savePageData(path) { 64 if(!path) { 65 path = location.pathname.split('?')[0].split('#')[0]; 66 if(path.endsWith('/')) path = path.slice(0, -1); 67 } 68 this.pageData[path] = { 69 linkColors: this.linkColors, 70 cursor: this.cursor, 71 likeCursor: this.likeCursor, 72 retweetCursor: this.retweetCursor, 73 retweetCommentsCursor: this.retweetCommentsCursor, 74 mainTweetLikers: this.mainTweetLikers, 75 seenReplies: this.seenReplies, 76 tweets: this.tweets, 77 scrollY: this.container.scrollTop 78 } 79 console.log(`Saving page: ${path}`, this.pageData[path]); 80 } 81 async restorePageData() { 82 let path = location.pathname.split('?')[0].split('#')[0]; 83 if(path.endsWith('/')) path = path.slice(0, -1); 84 if(this.pageData[path]) { 85 console.log(`Restoring page: ${path}`, this.pageData[path]); 86 this.linkColors = this.pageData[path].linkColors; 87 this.cursor = this.pageData[path].cursor; 88 this.likeCursor = this.pageData[path].likeCursor; 89 this.retweetCursor = this.pageData[path].retweetCursor; 90 this.retweetCommentsCursor = this.pageData[path].retweetCommentsCursor; 91 this.mainTweetLikers = this.pageData[path].mainTweetLikers; 92 this.seenReplies = []; 93 this.tweets = []; 94 this.container.getElementsByClassName('timeline-more')[0].hidden = !this.cursor; 95 let tl = document.getElementsByClassName('timeline')[0]; 96 tl.innerHTML = ''; 97 for(let i in this.pageData[path].tweets) { 98 let t = this.pageData[path].tweets[i]; 99 if(t[0] === 'tweet') { 100 await this.appendTweet(t[1], tl, t[2]); 101 } else if(t[0] === 'compose') { 102 await this.appendComposeComponent(tl, t[1]); 103 } else if(t[0] === 'tombstone') { 104 await this.appendTombstone(tl, t[1]); 105 } 106 } 107 let id = this.currentLocation.match(/status\/(\d{1,32})/)[1]; 108 if(id) { 109 setTimeout(() => { 110 let tweet = this.container.getElementsByClassName(`tweet-id-${id}`)[0]; 111 if(tweet) { 112 tweet.scrollIntoView({ block: 'center' }); 113 } 114 }, 100); 115 } 116 if(this.subpage === 'retweets_with_comments' && this.retweetCommentsCursor) { 117 this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = false; 118 } 119 this.loadingNewTweets = false; 120 return this.pageData[path]; 121 } else { 122 this.tweets = []; 123 this.seenReplies = []; 124 } 125 this.loadingNewTweets = false; 126 return false; 127 } 128 updateSubpage() { 129 let path = location.pathname.slice(1); 130 if(path.endsWith('/')) path = path.slice(0, -1); 131 132 let tlDiv = document.getElementsByClassName('timeline')[0]; 133 let rtDiv = document.getElementsByClassName('retweets')[0]; 134 let rtwDiv = document.getElementsByClassName('retweets_with_comments')[0]; 135 let likesDiv = document.getElementsByClassName('likes')[0]; 136 let tlMore = document.getElementsByClassName('timeline-more')[0]; 137 let rtMore = document.getElementsByClassName('retweets-more')[0]; 138 let rtwMore = document.getElementsByClassName('retweets_with_comments-more')[0]; 139 let likesMore = document.getElementsByClassName('likes-more')[0]; 140 tlDiv.hidden = true; rtDiv.hidden = true; rtwDiv.hidden = true; likesDiv.hidden = true; 141 tlMore.hidden = true; rtMore.hidden = true; rtwMore.hidden = true; likesMore.hidden = true; 142 143 if(path.split('/').length === 3) { 144 this.subpage = 'tweet'; 145 tlDiv.hidden = false; 146 } else { 147 if(path.endsWith('/retweets')) { 148 this.subpage = 'retweets'; 149 rtDiv.hidden = false; 150 } else if(path.endsWith('/likes')) { 151 this.subpage = 'likes'; 152 likesDiv.hidden = false; 153 } else if(path.endsWith('/retweets/with_comments')) { 154 this.subpage = 'retweets_with_comments'; 155 rtwDiv.hidden = false; 156 } 157 } 158 } 159 async updateReplies(id, c) { 160 let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; 161 if(!c) { 162 tvl.hidden = false; 163 document.getElementsByClassName('timeline')[0].innerHTML = ''; 164 } 165 let tl, tweetLikers; 166 try { 167 let [tlData, tweetLikersData] = await Promise.allSettled([API.tweet.getRepliesV2(id, c), API.tweet.getLikers(id)]); 168 if(!tlData.value) { 169 this.cursor = undefined; 170 return console.error(tlData.reason); 171 } 172 tl = tlData.value; 173 for(let u in tl.users) { 174 this.users[u] = tl.users[u]; 175 } 176 tweetLikers = tweetLikersData.value; 177 this.loadingNewTweets = false; 178 document.getElementsByClassName('timeline-more')[0].innerText = LOC.load_more.message; 179 } catch(e) { 180 document.getElementsByClassName('timeline-more')[0].innerText = LOC.load_more.message; 181 this.loadingNewTweets = false; 182 tvl.hidden = true; 183 return this.cursor = undefined; 184 } 185 186 if(vars.linkColorsInTL) { 187 let tlUsers = []; 188 for(let i in tl.list) { 189 let t = tl.list[i]; 190 if(t.type === 'tweet' || t.type === 'mainTweet') { if(!tlUsers.includes(t.data.user.id_str)) tlUsers.push(t.data.user.id_str); } 191 else if(t.type === 'conversation') { 192 for(let j in t.data) { 193 tlUsers.push(t.data[j].user.id_str); 194 } 195 } 196 } 197 tlUsers = tlUsers.filter(i => !this.linkColors[i]); 198 let linkData = await getLinkColors(tlUsers); 199 if(linkData) for(let i in linkData) { 200 this.linkColors[linkData[i].id] = linkData[i].color; 201 } 202 } 203 204 this.cursor = tl.cursor; 205 if(!this.cursor) { 206 this.container.getElementsByClassName('timeline-more')[0].hidden = true; 207 } else { 208 this.container.getElementsByClassName('timeline-more')[0].hidden = false; 209 } 210 let mainTweet; 211 let mainTweetIndex = tl.list.findIndex(t => t.type === 'mainTweet'); 212 let tlContainer = document.getElementsByClassName('timeline')[0]; 213 for(let i in tl.list) { 214 let t = tl.list[i]; 215 if(t.type === 'mainTweet') { 216 this.mainTweetLikers = tweetLikers.list; 217 this.likeCursor = tweetLikers.cursor; 218 if(i === 0) { 219 mainTweet = await this.appendTweet(t.data, tlContainer, { 220 mainTweet: true 221 }); 222 } else { 223 mainTweet = await this.appendTweet(t.data, tlContainer, { 224 noTop: true, 225 mainTweet: true 226 }); 227 } 228 if(t.data.limited_actions !== "non_compliant") this.appendComposeComponent(tlContainer, t.data); 229 } 230 if(t.type === 'tweet') { 231 await this.appendTweet(t.data, tlContainer, { 232 noTop: i !== 0 && i < mainTweetIndex, 233 threadContinuation: i < mainTweetIndex 234 }); 235 } else if(t.type === 'conversation') { 236 for(let i2 in t.data) { 237 let t2 = t.data[i2]; 238 await this.appendTweet(t2, tlContainer, { 239 noTop: +i2 !== 0, 240 threadContinuation: +i2 !== t.data.length - 1, 241 threadButton: +i2 === t.data.length - 1, 242 threadId: t2.conversation_id_str 243 }); 244 } 245 } else if(t.type === 'tombstone') { 246 this.appendTombstone(tlContainer, t.data); 247 } else if(t.type === 'showMore') { 248 let div = document.createElement('div'); 249 div.className = 'show-more'; 250 div.innerHTML = ` 251 <button class="show-more-button center-text">${t.data.labelText ? t.data.labelText : t.data.actionText}</button> 252 `; 253 let loading = false; 254 div.querySelector('.show-more-button').addEventListener('click', async () => { 255 if(loading) return; 256 loading = true; 257 div.children[0].innerText = LOC.loading_tweets.message; 258 await this.updateReplies(id, t.data.cursor); 259 div.remove(); 260 }); 261 tlContainer.appendChild(div); 262 } 263 } 264 if(mainTweet) mainTweet.scrollIntoView(); 265 if(tvl) tvl.hidden = true; 266 return true; 267 } 268 async updateLikes(id, c) { 269 let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; 270 if(tvl) tvl.hidden = false; 271 let tweetLikers; 272 if(!c && this.mainTweetLikers.length > 0) { 273 tweetLikers = this.mainTweetLikers; 274 } else { 275 try { 276 tweetLikers = await API.tweet.getLikers(id, c); 277 this.likeCursor = tweetLikers.cursor; 278 tweetLikers = tweetLikers.list; 279 if(!c) this.mainTweetLikers = tweetLikers; 280 } catch(e) { 281 console.error(e); 282 if(tvl) tvl.hidden = true; 283 return this.likeCursor = undefined; 284 } 285 } 286 let likeDiv = document.getElementsByClassName('likes')[0]; 287 288 if(!c) { 289 likeDiv.innerHTML = ''; 290 let tweetData = await API.tweet.getV2(id); 291 let tweet = await this.appendTweet(tweetData, likeDiv, { 292 mainTweet: true 293 }); 294 tweet.style.borderBottom = '1px solid var(--border)'; 295 tweet.style.marginBottom = '10px'; 296 tweet.style.borderRadius = '5px'; 297 let h1 = document.createElement('h1'); 298 h1.innerText = LOC.liked_by.message; 299 h1.className = 'cool-header'; 300 likeDiv.appendChild(h1); 301 } 302 if(!this.likeCursor || tweetLikers.length === 0) { 303 this.container.getElementsByClassName('likes-more')[0].hidden = true; 304 } else { 305 this.container.getElementsByClassName('likes-more')[0].hidden = false; 306 } 307 308 for(let i in tweetLikers) { 309 appendUser(tweetLikers[i], likeDiv); 310 } 311 312 if(tvl) tvl.hidden = true; 313 } 314 async updateRetweets(id, c) { 315 let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; 316 tvl.hidden = false; 317 let tweetRetweeters; 318 try { 319 tweetRetweeters = await API.tweet.getRetweeters(id, c); 320 this.retweetCursor = tweetRetweeters.cursor; 321 tweetRetweeters = tweetRetweeters.list; 322 } catch(e) { 323 console.error(e); 324 return this.retweetCursor = undefined; 325 } 326 let retweetDiv = document.getElementsByClassName('retweets')[0]; 327 328 if(!c) { 329 retweetDiv.innerHTML = ''; 330 let tweetData = await API.tweet.getV2(id); 331 let tweet = await this.appendTweet(tweetData, retweetDiv, { 332 mainTweet: true 333 }); 334 tweet.style.borderBottom = '1px solid var(--border)'; 335 tweet.style.marginBottom = '10px'; 336 tweet.style.borderRadius = '5px'; 337 let h1 = document.createElement('h1'); 338 h1.innerHTML = `${LOC.retweeted_by.message} (<a href="https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets/with_comments">${LOC.see_quotes.message}</a>)`; 339 h1.className = 'cool-header'; 340 retweetDiv.appendChild(h1); 341 h1.getElementsByTagName('a')[0].addEventListener('click', async e => { 342 e.preventDefault(); 343 history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets/with_comments`); 344 this.updateSubpage(); 345 this.mediaToUpload = []; 346 this.excludeUserMentions = []; 347 this.linkColors = {}; 348 this.cursor = undefined; 349 this.seenReplies = []; 350 this.mainTweetLikers = []; 351 let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; 352 this.updateRetweetsWithComments(tid); 353 this.currentLocation = location.pathname; 354 }); 355 } 356 if(!this.retweetCursor) { 357 this.container.getElementsByClassName('retweets-more')[0].hidden = true; 358 } else { 359 this.container.getElementsByClassName('retweets-more')[0].hidden = false; 360 } 361 362 for(let i in tweetRetweeters) { 363 let u = tweetRetweeters[i]; 364 let retweetElement = document.createElement('div'); 365 retweetElement.classList.add('following-item'); 366 retweetElement.innerHTML = ` 367 <div> 368 <a href="https://twitter.com/${u.screen_name}" class="following-item-link"> 369 <img src="${(u.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(u.id_str) % 7}_normal.png`): u.profile_image_url_https}" alt="${u.screen_name}" class="following-item-avatar tweet-avatar" width="48" height="48"> 370 <div class="following-item-text"> 371 <span class="tweet-header-name following-item-name">${escapeHTML(u.name)}</span><br> 372 <span class="tweet-header-handle">@${u.screen_name}</span> 373 </div> 374 </a> 375 </div> 376 <div> 377 <button class="following-item-btn nice-button ${u.following ? 'following' : 'follow'}">${u.following ? LOC.following_btn.message : LOC.follow.message}</button> 378 </div>`; 379 380 let followButton = retweetElement.querySelector('.following-item-btn'); 381 followButton.addEventListener('click', async () => { 382 if (followButton.classList.contains('following')) { 383 await API.user.unfollow(u.screen_name); 384 followButton.classList.remove('following'); 385 followButton.classList.add('follow'); 386 followButton.innerText = LOC.follow.message; 387 } else { 388 await API.user.follow(u.screen_name); 389 followButton.classList.remove('follow'); 390 followButton.classList.add('following'); 391 followButton.innerText = LOC.following_btn.message; 392 } 393 }); 394 395 retweetDiv.appendChild(retweetElement); 396 } 397 398 tvl.hidden = true; 399 } 400 async updateRetweetsWithComments(id, c) { 401 let tvl = this.container.getElementsByClassName('tweet-viewer-loading')[0]; 402 tvl.hidden = false; 403 let tweetRetweeters; 404 let tweetData = await API.tweet.getV2(id); 405 try { 406 tweetRetweeters = await API.tweet.getQuotes(id, c); 407 this.retweetCommentsCursor = tweetRetweeters.cursor; 408 tweetRetweeters = tweetRetweeters.list; 409 } catch(e) { 410 console.error(e); 411 tvl.hidden = true; 412 return this.retweetCommentsCursor = undefined; 413 } 414 let retweetDiv = document.getElementsByClassName('retweets_with_comments')[0]; 415 416 if(!c) { 417 retweetDiv.innerHTML = ''; 418 let h1 = document.createElement('h1'); 419 h1.innerHTML = `${LOC.quote_tweets.message} (<a href="https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets">${LOC.see_retweets.message}</a>)`; 420 h1.className = 'cool-header'; 421 retweetDiv.appendChild(h1); 422 h1.getElementsByTagName('a')[0].addEventListener('click', async e => { 423 e.preventDefault(); 424 let t = await API.tweet.getV2(id); 425 history.pushState({}, null, `https://twitter.com/${tweetData.user.screen_name}/status/${id}/retweets`); 426 this.updateSubpage(); 427 this.mediaToUpload = []; 428 this.excludeUserMentions = []; 429 this.linkColors = {}; 430 this.cursor = undefined; 431 this.seenReplies = []; 432 this.mainTweetLikers = []; 433 let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; 434 this.updateRetweets(tid); 435 this.currentLocation = location.pathname; 436 }); 437 } 438 if(!this.retweetCommentsCursor) { 439 this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = true; 440 } else { 441 this.container.getElementsByClassName('retweets_with_comments-more')[0].hidden = false; 442 } 443 444 for(let i in tweetRetweeters) { 445 await this.appendTweet(tweetRetweeters[i], retweetDiv); 446 } 447 448 tvl.hidden = true; 449 } 450 async appendComposeComponent(container, replyTweet) { 451 if(!replyTweet) return; 452 this.tweets.push(['compose', replyTweet]); 453 454 let mentions = replyTweet.full_text.match(/@([\w+]{1,15})/g); 455 if(mentions) { 456 mentions = mentions.map(m => m.slice(1).trim()); 457 } else { 458 mentions = []; 459 } 460 461 let replyMessage; 462 if(LOC.reply_to.message.includes("$SCREEN_NAME$")) { 463 replyMessage = LOC.reply_to.message.replace("$SCREEN_NAME$", replyTweet.user.screen_name); 464 } else { 465 replyMessage = `${LOC.reply_to.message} @${replyTweet.user.screen_name}`; 466 } 467 468 let el = document.createElement('div'); 469 el.className = 'new-tweet-container'; 470 el.innerHTML = /*html*/` 471 <div class="new-tweet-view box"> 472 <img width="35" height="35" class="tweet-avatar new-tweet-avatar"> 473 <span class="new-tweet-char" hidden>0/280</span> 474 <textarea class="new-tweet-text" placeholder="${replyMessage}" maxlength="1000"></textarea> 475 <div class="new-tweet-user-search box" hidden></div> 476 <div class="new-tweet-media-div" title="${LOC.add_media.message}"> 477 <span class="new-tweet-media"></span> 478 </div> 479 <div class="new-tweet-focused" hidden> 480 <span class="new-tweet-emojis" title="${LOC.emoji.message}"></span> 481 ${mentions.length > 0 ? /*html*/`<span class="new-tweet-mentions" title="${LOC.mentions.message}"></span>` : ''} 482 <div class="new-tweet-media-cc"><div class="new-tweet-media-c"></div></div> 483 <button class="new-tweet-button nice-button" style="margin-right: -32px;">${LOC.tweet.message}</button> 484 <br><br> 485 </div> 486 </div>`; 487 container.append(el); 488 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"); 489 document.getElementsByClassName('new-tweet-view')[0].addEventListener('click', async () => { 490 document.getElementsByClassName('new-tweet-focused')[0].hidden = false; 491 document.getElementsByClassName('new-tweet-char')[0].hidden = false; 492 document.getElementsByClassName('new-tweet-text')[0].classList.add('new-tweet-text-focused'); 493 document.getElementsByClassName('new-tweet-media-div')[0].classList.add('new-tweet-media-div-focused'); 494 }); 495 if(mentions.length > 0) { 496 for(let i = 0; i < mentions.length; i++) { 497 let u = Object.values(this.users).find(u => u.screen_name === mentions[i]); 498 if(!u) { 499 if(mentions[i] === this.user.screen_name) { 500 u = this.user; 501 } else if(typeof pageUser !== 'undefined' && mentions[i] === pageUser.screen_name) { 502 u = pageUser; 503 } else { 504 try { 505 u = await API.user.get(mentions[i], false); 506 } catch(e) { 507 console.error(e); 508 continue; 509 } 510 } 511 } 512 if(!u) continue; 513 this.users[u.id_str] = u; 514 } 515 document.getElementsByClassName('new-tweet-button')[0].style = 'margin-right: -50px;'; 516 document.getElementsByClassName("new-tweet-mentions")[0].addEventListener('click', async () => { 517 let modal = createModal(/*html*/` 518 <div id="new-tweet-mentions-modal" style="color:var(--almost-black)"> 519 <h3 class="nice-header">${LOC.replying_to.message}</h3> 520 <div class="new-tweet-mentions-modal-item"> 521 <input type="checkbox" id="new-tweet-mentions-modal-item-${replyTweet.user.screen_name}" checked disabled> 522 <label for="new-tweet-mentions-modal-item-${replyTweet.user.screen_name}">${replyTweet.user.name} (@${replyTweet.user.screen_name})</label> 523 </div> 524 ${mentions.map(m => { 525 let u = Object.values(this.users).find(u => u.screen_name === m); 526 if(!u) return ''; 527 return /*html*/` 528 <div class="new-tweet-mentions-modal-item"> 529 <input type="checkbox" data-user-id="${u.id_str}" id="new-tweet-mentions-modal-item-${m}"${this.excludeUserMentions.includes(u.id_str) ? '' : ' checked'}${this.user.screen_name === m ? ' hidden' : ''}> 530 <label for="new-tweet-mentions-modal-item-${m}"${this.user.screen_name === m ? ' hidden' : ''}>${u.name} (@${m})</label> 531 </div> 532 `}).join('\n')} 533 <br> 534 <div style="display:inline-block;float: right;"> 535 <button class="nice-button" id="new-tweet-mentions-modal-button">${LOC.save.message}</button> 536 </div> 537 </div> 538 `); 539 document.getElementById('new-tweet-mentions-modal-button').addEventListener('click', () => { 540 let excluded = []; 541 document.querySelectorAll('#new-tweet-mentions-modal input[type="checkbox"]').forEach(c => { 542 if(!c.checked) excluded.push(c.dataset.userId); 543 }); 544 this.excludeUserMentions = excluded; 545 console.log(this.excludeUserMentions); 546 modal.removeModal(); 547 }); 548 }); 549 } 550 551 let mediaList = document.getElementsByClassName('new-tweet-media-c')[0]; 552 553 let mediaObserver = new MutationObserver(async () => { 554 if(mediaList.children.length > 0) { 555 newTweetButton.style.marginRight = '4px'; 556 } else { 557 newTweetButton.style.marginRight = mentions.length > 0 ? '-50px' : '-32px'; 558 } 559 }); 560 mediaObserver.observe(mediaList, {childList: true}); 561 562 document.getElementsByClassName('new-tweet-view')[0].addEventListener('drop', e => { 563 handleDrop(e, this.mediaToUpload, mediaList); 564 }); 565 document.getElementsByClassName('new-tweet-media-div')[0].addEventListener('click', async () => { 566 getMedia(this.mediaToUpload, mediaList); 567 }); 568 let newTweetUserSearch = document.getElementsByClassName("new-tweet-user-search")[0]; 569 let newTweetText = document.getElementsByClassName('new-tweet-text')[0]; 570 let newTweetButton = document.getElementsByClassName('new-tweet-button')[0]; 571 let selectedIndex = 0; 572 newTweetText.addEventListener('paste', event => { 573 let items = (event.clipboardData || event.originalEvent.clipboardData).items; 574 for (let index in items) { 575 let item = items[index]; 576 if (item.kind === 'file') { 577 let file = item.getAsFile(); 578 handleFiles([file], this.mediaToUpload, document.getElementsByClassName('new-tweet-media-c')[0]); 579 } 580 } 581 }); 582 newTweetText.addEventListener('focus', async e => { 583 setTimeout(() => { 584 if(/(?<!\w)@([\w+]{1,15}\b)$/.test(e.target.value)) { 585 newTweetUserSearch.hidden = false; 586 } else { 587 newTweetUserSearch.hidden = true; 588 newTweetUserSearch.innerHTML = ''; 589 } 590 }, 10); 591 }); 592 newTweetText.addEventListener('blur', async e => { 593 setTimeout(() => { 594 newTweetUserSearch.hidden = true; 595 }, 100); 596 }); 597 newTweetText.addEventListener('keypress', async e => { 598 if ((e.key === 'Enter' || e.key === 'Tab') && !newTweetUserSearch.hidden) { 599 let activeSearch = newTweetUserSearch.querySelector('.search-result-item-active'); 600 if(!e.ctrlKey) { 601 e.preventDefault(); 602 e.stopPropagation(); 603 newTweetText.value = newTweetText.value.split("@").slice(0, -1).join('@').split(" ").slice(0, -1).join(" ") + ` @${activeSearch.querySelector('.search-result-item-screen-name').innerText.slice(1)} `; 604 if(newTweetText.value.startsWith(" ")) newTweetText.value = newTweetText.value.slice(1); 605 if(newTweetText.value.length > 280) newTweetText.value = newTweetText.value.slice(0, 280); 606 newTweetUserSearch.innerHTML = ''; 607 newTweetUserSearch.hidden = true; 608 } 609 } 610 }); 611 newTweetText.addEventListener('keydown', async e => { 612 if(e.key === 'ArrowDown') { 613 if(selectedIndex < newTweetUserSearch.children.length - 1) { 614 selectedIndex++; 615 newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); 616 newTweetUserSearch.children[selectedIndex - 1].classList.remove('search-result-item-active'); 617 } else { 618 selectedIndex = 0; 619 newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); 620 newTweetUserSearch.children[newTweetUserSearch.children.length - 1].classList.remove('search-result-item-active'); 621 } 622 return; 623 } 624 if(e.key === 'ArrowUp') { 625 if(selectedIndex > 0) { 626 selectedIndex--; 627 newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); 628 newTweetUserSearch.children[selectedIndex + 1].classList.remove('search-result-item-active'); 629 } else { 630 selectedIndex = newTweetUserSearch.children.length - 1; 631 newTweetUserSearch.children[selectedIndex].classList.add('search-result-item-active'); 632 newTweetUserSearch.children[0].classList.remove('search-result-item-active'); 633 } 634 return; 635 } 636 if(/(?<!\w)@([\w+]{1,15}\b)$/.test(e.target.value)) { 637 newTweetUserSearch.hidden = false; 638 selectedIndex = 0; 639 let users = (await API.search.typeahead(e.target.value.match(/@([\w+]{1,15}\b)$/)[1])).users; 640 newTweetUserSearch.innerHTML = ''; 641 users.forEach((user, index) => { 642 let userElement = document.createElement('span'); 643 userElement.className = 'search-result-item'; 644 if(index === 0) userElement.classList.add('search-result-item-active'); 645 userElement.innerHTML = ` 646 <img width="16" height="16" class="search-result-item-avatar" 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}"> 647 <span class="search-result-item-name ${user.verified ? 'search-result-item-verified' : ''}">${escapeHTML(user.name)}</span> 648 <span class="search-result-item-screen-name">@${user.screen_name}</span> 649 `; 650 userElement.addEventListener('click', () => { 651 newTweetText.value = newTweetText.value.split("@").slice(0, -1).join('@').split(" ").slice(0, -1).join(" ") + ` @${user.screen_name} `; 652 if(newTweetText.value.startsWith(" ")) newTweetText.value = newTweetText.value.slice(1); 653 if(newTweetText.value.length > 280) newTweetText.value = newTweetText.value.slice(0, 280); 654 newTweetText.focus(); 655 newTweetUserSearch.innerHTML = ''; 656 newTweetUserSearch.hidden = true; 657 }); 658 newTweetUserSearch.appendChild(userElement); 659 }); 660 } else { 661 newTweetUserSearch.innerHTML = ''; 662 newTweetUserSearch.hidden = true; 663 } 664 }); 665 newTweetText.addEventListener('keydown', e => { 666 if (e.key === 'Enter' && e.ctrlKey) { 667 document.getElementsByClassName('new-tweet-button')[0].click(); 668 } 669 }); 670 newTweetText.addEventListener('input', e => { 671 let charElement = document.getElementsByClassName('new-tweet-char')[0]; 672 let text = e.target.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); 673 charElement.innerText = `${text.length}/280`; 674 if(text.length > 265) { 675 charElement.style.color = "#c26363"; 676 } else { 677 charElement.style.color = ""; 678 } 679 if (text.length > 280) { 680 charElement.style.color = "red"; 681 newTweetButton.disabled = true; 682 } else { 683 newTweetButton.disabled = false; 684 } 685 }); 686 document.getElementsByClassName('new-tweet-emojis')[0].addEventListener('click', () => { 687 createEmojiPicker(document.getElementsByClassName('new-tweet-emojis')[0], newTweetText, { 688 marginLeft: '-300px' 689 }); 690 }); 691 newTweetButton.addEventListener('click', async () => { 692 let tweet = document.getElementsByClassName('new-tweet-text')[0].value; 693 if (tweet.length === 0 && this.mediaToUpload.length === 0) return; 694 document.getElementsByClassName('new-tweet-button')[0].disabled = true; 695 let uploadedMedia = []; 696 for (let i in this.mediaToUpload) { 697 let media = this.mediaToUpload[i]; 698 try { 699 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; 700 let mediaId = await API.uploadMedia({ 701 media_type: media.type, 702 media_category: media.category, 703 media: media.data, 704 alt: media.alt, 705 cw: media.cw, 706 loadCallback: data => { 707 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; 708 } 709 }); 710 uploadedMedia.push(mediaId); 711 } catch (e) { 712 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; 713 console.error(e); 714 alert(e); 715 } 716 } 717 let tweetObject = { 718 status: tweet, 719 in_reply_to_status_id: replyTweet.id_str, 720 }; 721 if(this.excludeUserMentions.length > 0) tweetObject.exclude_reply_user_ids = this.excludeUserMentions.join(','); 722 if (uploadedMedia.length > 0) { 723 tweetObject.media_ids = uploadedMedia.join(','); 724 } 725 try { 726 let tweet = await API.tweet.postV2(tweetObject); 727 tweet._ARTIFICIAL = true; 728 this.appendTweet(tweet, document.getElementsByClassName('timeline')[0], { 729 after: document.getElementsByClassName('new-tweet-view')[0].parentElement 730 }); 731 } catch (e) { 732 document.getElementsByClassName('new-tweet-button')[0].disabled = false; 733 console.error(e); 734 } 735 document.getElementsByClassName('new-tweet-text')[0].value = ""; 736 document.getElementsByClassName('new-tweet-media-c')[0].innerHTML = ""; 737 this.mediaToUpload = []; 738 this.excludeUserMentions = []; 739 document.getElementsByClassName('new-tweet-focused')[0].hidden = true; 740 document.getElementsByClassName('new-tweet-char')[0].hidden = true; 741 document.getElementsByClassName('new-tweet-text')[0].classList.remove('new-tweet-text-focused'); 742 document.getElementsByClassName('new-tweet-media-div')[0].classList.remove('new-tweet-media-div-focused'); 743 document.getElementsByClassName('new-tweet-button')[0].disabled = false; 744 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 745 }); 746 } 747 async appendTweet(t, timelineContainer, options = {}) { 748 if(this.seenReplies.includes(t.id_str)) return; 749 // if(t.entities && t.entities.urls) { 750 // let webUrl = t.entities.urls.find(u => u.expanded_url.startsWith('https://twitter.com/i/web/status/')); 751 // if(webUrl) { 752 // try { 753 // let source = t.source; 754 // t = await API.tweet.getV2(t.id_str); 755 // t.source = source; 756 // } catch(e) {} 757 // } 758 // } 759 if(vars.twitterBlueCheckmarks && t.user.ext && t.user.ext.isBlueVerified && t.user.ext.isBlueVerified.r && t.user.ext.isBlueVerified.r.ok) { 760 t.user.verified_type = "Blue"; 761 } 762 if(t.user.ext && t.user.ext.verifiedType && t.user.ext.verifiedType.r && t.user.ext.verifiedType.r.ok) { 763 t.user.verified_type = t.user.ext.verifiedType.r.ok; 764 } 765 this.tweets.push(['tweet', t, options]); 766 this.seenReplies.push(t.id_str); 767 const tweet = document.createElement('div'); 768 t.options = options; 769 t.element = tweet; 770 if(!options.mainTweet) { 771 tweet.addEventListener('click', async e => { 772 if(e.target.className.startsWith('tweet tweet-view tweet-id-') || e.target.classList.contains('tweet-body') || e.target.className === 'tweet-interact') { 773 this.savePageData(); 774 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); 775 this.updateSubpage(); 776 this.mediaToUpload = []; 777 this.excludeUserMentions = []; 778 this.linkColors = {}; 779 this.cursor = undefined; 780 this.seenReplies = []; 781 this.mainTweetLikers = []; 782 let restored = await this.restorePageData(); 783 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 784 if(this.subpage === 'tweet' && !restored) { 785 this.updateReplies(id); 786 } else if(this.subpage === 'likes') { 787 this.updateLikes(id); 788 } else if(this.subpage === 'retweets') { 789 this.updateRetweets(id); 790 } else if(this.subpage === 'retweets_with_comments') { 791 this.updateRetweetsWithComments(id); 792 } 793 this.currentLocation = location.pathname; 794 } 795 }); 796 tweet.addEventListener('mousedown', e => { 797 if(e.button === 1) { 798 e.preventDefault(); 799 if(e.target.className.startsWith('tweet tweet-view tweet-id-') || e.target.classList.contains('tweet-body') || e.target.className === 'tweet-interact') { 800 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); 801 } 802 } 803 }); 804 } 805 tweet.tabIndex = -1; 806 tweet.className = `tweet tweet-view tweet-id-${t.id_str} ${options.mainTweet ? 'tweet-main' : 'tweet-replying'}`; 807 if(!this.activeTweet) { 808 tweet.classList.add('tweet-active'); 809 this.activeTweet = tweet; 810 } 811 if (options.threadContinuation) tweet.classList.add('tweet-self-thread-continuation'); 812 if (options.noTop) tweet.classList.add('tweet-no-top'); 813 if(vars.linkColorsInTL) { 814 if(this.linkColors[t.user.id_str]) { 815 let sc = makeSeeableColor(this.linkColors[t.user.id_str]); 816 tweet.style.setProperty('--link-color', sc); 817 } else { 818 if(t.user.profile_link_color && t.user.profile_link_color !== '1DA1F2') { 819 let sc = makeSeeableColor(t.user.profile_link_color); 820 tweet.style.setProperty('--link-color', sc); 821 } 822 } 823 } 824 let full_text = t.full_text ? t.full_text : ''; 825 let strippedDownText = full_text 826 .replace(/(?:https?|ftp):\/\/[\n\S]+/g, '') //links 827 .replace(/(?<!\w)@([\w+]{1,15}\b)/g, '') //mentions 828 .replace(/[\p{Extended_Pictographic}]/gu, '') //emojis (including ones that arent colored) 829 .replace(/[\u200B-\u200D\uFE0E\uFE0F]/g, '') //sometimes emojis leave these behind 830 .replace(/\d+/g, '') //numbers 831 .trim(); 832 let detectedLanguage = strippedDownText.length < 1 ? {languages:[{language:LANGUAGE, percentage:100}]} : await chrome.i18n.detectLanguage(strippedDownText); 833 if(!detectedLanguage.languages[0]) detectedLanguage = {languages:[{language:t.lang, percentage:100}]}; //fallback to what twitter says 834 let isEnglish = detectedLanguage.languages[0] && detectedLanguage.languages[0].percentage > 60 && detectedLanguage.languages[0].language.startsWith(LANGUAGE); 835 let videos = t.extended_entities && t.extended_entities.media && t.extended_entities.media.filter(m => m.type === 'video'); 836 if(!videos || videos.length === 0) { 837 videos = undefined; 838 } 839 if(videos) { 840 for(let v of videos) { 841 if(!v.video_info) continue; 842 v.video_info.variants = v.video_info.variants.sort((a, b) => { 843 if(!b.bitrate) return -1; 844 return b.bitrate-a.bitrate; 845 }); 846 if(typeof(vars.savePreferredQuality) !== 'boolean') { 847 chrome.storage.sync.set({ 848 savePreferredQuality: true 849 }, () => {}); 850 vars.savePreferredQuality = true; 851 } 852 if(localStorage.preferredQuality && vars.savePreferredQuality) { 853 let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => { 854 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); 855 }); 856 let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url); 857 if(preferredQualityVariantIndex !== -1) { 858 let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex]; 859 v.video_info.variants.splice(preferredQualityVariantIndex, 1); 860 v.video_info.variants.unshift(preferredQualityVariant); 861 } 862 } 863 } 864 } 865 if(t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) { 866 full_text = ""; 867 } 868 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) 869 try { 870 if(t.quoted_status_result && t.quoted_status_result.result.tweet) { 871 t.quoted_status = t.quoted_status_result.result.tweet.legacy; 872 t.quoted_status.user = t.quoted_status_result.result.tweet.core.user_results.result.legacy; 873 } else { 874 t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str); 875 } 876 } catch { 877 t.quoted_status = undefined; 878 } 879 } 880 let followUserText, unfollowUserText, blockUserText, unblockUserText; 881 let mentionedUserText = ``; 882 let quoteMentionedUserText = ``; 883 if( 884 LOC.follow_user.message.includes('$SCREEN_NAME$') && LOC.unfollow_user.message.includes('$SCREEN_NAME$') && 885 LOC.block_user.message.includes('$SCREEN_NAME$') && LOC.unblock_user.message.includes('$SCREEN_NAME$') 886 ) { 887 followUserText = `${LOC.follow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; 888 unfollowUserText = `${LOC.unfollow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; 889 blockUserText = `${LOC.block_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; 890 unblockUserText = `${LOC.unblock_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`; 891 } else { 892 followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; 893 unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; 894 blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; 895 unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; 896 } 897 if(t.in_reply_to_screen_name && t.display_text_range) { 898 t.entities.user_mentions.forEach(user_mention => { 899 if(user_mention.indices[0] < t.display_text_range[0]){ 900 mentionedUserText += `<a href="https://twitter.com/${user_mention.screen_name}">@${user_mention.screen_name}</a> ` 901 } 902 //else this is not reply but mention 903 }); 904 } 905 if(t.quoted_status && t.quoted_status.in_reply_to_screen_name && t.display_text_range) { 906 t.quoted_status.entities.user_mentions.forEach(user_mention => { 907 if(user_mention.indices[0] < t.display_text_range[0]){ 908 quoteMentionedUserText += `@${user_mention.screen_name} ` 909 } 910 //else this is not reply but mention 911 }); 912 } 913 tweet.innerHTML = /*html*/` 914 <div class="tweet-top" hidden></div> 915 <a class="tweet-avatar-link" href="https://twitter.com/${t.user.screen_name}"><img onerror="this.src = '${`${vars.useOldDefaultProfileImage ? chrome.runtime.getURL(`images/default_profile_bigger.png`) : 'https://abs.twimg.com/sticky/default_profile_images/default_profile_bigger.png'}`}'" src="${`${(t.user.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(t.user.id_str) % 7}_normal.png`): t.user.profile_image_url_https}`.replace("_normal.", "_bigger.")}" alt="${t.user.name}" class="tweet-avatar" width="48" height="48"></a> 916 <div class="tweet-header ${options.mainTweet ? 'tweet-header-main' : ''}"> 917 <a class="tweet-header-info ${options.mainTweet ? 'tweet-header-info-main' : ''}" href="https://twitter.com/${t.user.screen_name}"> 918 <b ${t.user.id_str === '1123203847776763904' ? 'title="Old Twitter Layout extension developer" ' : ''}class="tweet-header-name ${options.mainTweet ? 'tweet-header-name-main' : ''} ${t.user.verified || t.user.verified_type ? 'user-verified' : t.user.id_str === '1123203847776763904' ? 'user-verified user-verified-dimden' : ''} ${t.user.protected ? 'user-protected' : ''} ${t.user.verified_type === 'Government' ? 'user-verified-gray' : t.user.verified_type === 'Business' ? 'user-verified-yellow' : t.user.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(t.user.name)}</b> 919 <span class="tweet-header-handle">@${t.user.screen_name}</span> 920 </a> 921 ${options.mainTweet && t.user.id_str !== user.id_str ? `<button class='nice-button tweet-header-follow ${t.user.following ? 'following' : 'follow'}'>${t.user.following ? LOC.following_btn.message : LOC.follow.message}</button>` : ''} 922 </div> 923 <a ${options.mainTweet ? 'hidden' : ''} class="tweet-time" data-timestamp="${new Date(t.created_at).getTime()}" title="${new Date(t.created_at).toLocaleString()}" href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}">${timeElapsed(new Date(t.created_at).getTime())}</a> 924 <div class="tweet-body ${options.mainTweet ? 'tweet-body-main' : ''}"> 925 <span class="tweet-body-text ${vars.noBigFont || (full_text && full_text.length > 100) || !options.mainTweet ? 'tweet-body-text-long' : 'tweet-body-text-short'}">${vars.useOldStyleReply ? /*html*/mentionedUserText: ''}${full_text ? await renderTweetBodyHTML(full_text, t.entities, t.display_text_range) : ''}</span> 926 ${!isEnglish ? /*html*/` 927 <br> 928 <span class="tweet-translate">${LOC.view_translation.message}</span> 929 ` : ``} 930 ${t.extended_entities && t.extended_entities.media ? /*html*/` 931 <div class="tweet-media"> 932 ${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? /*html*/` 933 <div class="tweet-media-video-overlay"> 934 <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play"> 935 <g> 936 <path class="svg-play-path" d="M8 5v14l11-7z"></path> 937 <path d="M0 0h24v24H0z" fill="none"></path> 938 </g> 939 </svg> 940 </div> 941 ` : ''} 942 ${renderMedia(t)} 943 </div> 944 ${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? `<div class="tweet-media-controls">GIF</div>` : ''} 945 ${videos ? /*html*/` 946 <div class="tweet-media-controls"> 947 ${videos[0].ext && videos[0].ext.mediaStats && videos[0].ext.mediaStats.r && videos[0].ext.mediaStats.r.ok ? `<span class="tweet-video-views">${Number(videos[0].ext.mediaStats.r.ok.viewCount).toLocaleString().replace(/\s/g, ',')} ${LOC.views.message}</span> • ` : ''}<span class="tweet-video-reload">${LOC.reload.message}</span> • 948 ${videos[0].video_info.variants.filter(v => v.bitrate).map(v => `<span class="tweet-video-quality" data-url="${v.url}">${v.url.match(/\/(\d+)x/)[1] + 'p'}</span> `).join(" / ")} 949 </div> 950 ` : ``} 951 <span class="tweet-media-data"></span> 952 ` : ``} 953 ${t.card ? `<div class="tweet-card"></div>` : ''} 954 ${t.quoted_status ? /*html*/` 955 <a class="tweet-body-quote" href="https://twitter.com/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}"> 956 <img src="${(t.quoted_status.user.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(t.quoted_status.user.id_str) % 7}_normal.png`): t.quoted_status.user.profile_image_url_https}" alt="${escapeHTML(t.quoted_status.user.name)}" class="tweet-avatar-quote" width="24" height="24"> 957 <div class="tweet-header-quote"> 958 <span class="tweet-header-info-quote"> 959 <b class="tweet-header-name-quote ${t.quoted_status.user.verified || t.quoted_status.user.id_str === '1123203847776763904' ? 'user-verified' : ''} ${t.quoted_status.user.protected ? 'user-protected' : ''}">${escapeHTML(t.quoted_status.user.name)}</b> 960 <span class="tweet-header-handle-quote">@${t.quoted_status.user.screen_name}</span> 961 </span> 962 </div> 963 <span class="tweet-time-quote" data-timestamp="${new Date(t.quoted_status.created_at).getTime()}" title="${new Date(t.quoted_status.created_at).toLocaleString()}">${timeElapsed(new Date(t.quoted_status.created_at).getTime())}</span> 964 ${quoteMentionedUserText !== `` && !vars.useOldStyleReply ? /*html*/` 965 <span class="tweet-reply-to tweet-quote-reply-to">${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))}</span> 966 ` : ''} 967 <span class="tweet-body-text tweet-body-text-quote tweet-body-text-long" style="color:var(--default-text-color)!important">${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) : ''}</span> 968 ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? /*html*/` 969 <div class="tweet-media-quote"> 970 ${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' ? '</video>' : ''}`).join('\n')} 971 </div> 972 ` : ''} 973 </a> 974 ` : ``} 975 ${t.limited_actions === 'limit_trusted_friends_tweet' && options.mainTweet ? ` 976 <div class="tweet-limited"> 977 ${LOC.circle_limited_tweet.message} 978 <a href="https://help.twitter.com/en/using-twitter/twitter-circle" target="_blank">${LOC.learn_more.message}</a> 979 </div> 980 `.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) : ''} 981 ${t.tombstone ? `<div class="tweet-warning">${t.tombstone}</div>` : ''} 982 ${((t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) || t.withheld_scope) ? `<div class="tweet-warning">This Tweet has been withheld in response to a report from the copyright holder. <a href="https://help.twitter.com/en/rules-and-policies/copyright-policy" target="_blank">Learn more.</a></div>` : ''} 983 ${t.conversation_control ? `<div class="tweet-warning">${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 : ''}.</div>` : ''} 984 ${options.mainTweet ? /*html*/` 985 <div class="tweet-footer"> 986 <div class="tweet-footer-stats"> 987 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}" class="tweet-footer-stat tweet-footer-stat-o"> 988 <span class="tweet-footer-stat-text">${LOC.replies.message}</span> 989 <b class="tweet-footer-stat-count tweet-footer-stat-replies">${Number(t.reply_count).toLocaleString().replace(/\s/g, ',')}</b> 990 </a> 991 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets" class="tweet-footer-stat tweet-footer-stat-r"> 992 <span class="tweet-footer-stat-text">${LOC.retweets.message}</span> 993 <b class="tweet-footer-stat-count tweet-footer-stat-retweets">${Number(t.retweet_count).toLocaleString().replace(/\s/g, ',')}</b> 994 </a> 995 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}/likes" class="tweet-footer-stat tweet-footer-stat-f"> 996 <span class="tweet-footer-stat-text">${vars.heartsNotStars ? LOC.likes.message : LOC.favorites.message}</span> 997 <b class="tweet-footer-stat-count tweet-footer-stat-favorites">${Number(t.favorite_count).toLocaleString().replace(/\s/g, ',')}</b> 998 </a> 999 </div> 1000 <div class="tweet-footer-favorites"></div> 1001 </div> 1002 ` : ''} 1003 <a ${!options.mainTweet ? 'hidden' : ''} class="tweet-date" title="${new Date(t.created_at).toLocaleString()}" href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}"><br>${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'}</a> 1004 <div class="tweet-interact"> 1005 <span class="tweet-interact-reply" title="${LOC.reply_btn.message}${!vars.disableHotkeys ? ' (R)' : ''}" data-val="${t.reply_count}">${options.mainTweet ? '' : Number(t.reply_count).toLocaleString().replace(/\s/g, ',')}</span> 1006 <span title="${LOC.retweet_btn.message}" class="tweet-interact-retweet${t.retweeted ? ' tweet-interact-retweeted' : ''}${(t.user.protected || t.limited_actions === 'limit_trusted_friends_tweet') && t.user.id_str !== user.id_str ? ' tweet-interact-retweet-disabled' : ''}" data-val="${t.retweet_count}">${options.mainTweet ? '' : Number(t.retweet_count).toLocaleString().replace(/\s/g, ',')}</span> 1007 <div class="tweet-interact-retweet-menu dropdown-menu" hidden> 1008 <span class="tweet-interact-retweet-menu-retweet">${t.retweeted ? LOC.unretweet.message : LOC.retweet.message}</span> 1009 <span class="tweet-interact-retweet-menu-quote">${LOC.quote_tweet.message}</span> 1010 ${options.mainTweet ? /*html*/` 1011 <span class="tweet-interact-retweet-menu-quotes">${LOC.see_quotes_big.message}</span> 1012 <span class="tweet-interact-retweet-menu-retweeters">${LOC.see_retweeters.message}</span> 1013 ` : ''} 1014 </div> 1015 <span title="${vars.heartsNotStars ? LOC.like_btn.message : LOC.favorite_btn.message}${!vars.disableHotkeys ? ' (L)' : ''}" class="tweet-interact-favorite ${t.favorited ? 'tweet-interact-favorited' : ''}" data-val="${t.favorite_count}">${options.mainTweet ? '' : Number(t.favorite_count).toLocaleString().replace(/\s/g, ',')}</span> 1016 ${vars.seeTweetViews && t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? /*html*/`<span title="${LOC.views_count.message}" class="tweet-interact-views" data-val="${t.ext.views.r.ok.count}">${Number(t.ext.views.r.ok.count).toLocaleString().replace(/\s/g, ',')}</span>` : ''} 1017 ${t.bookmark_count && vars.showBookmarkCount && options.mainTweet ? 1018 /*html*/`<span title="${LOC.bookmarks_count.message}" class="tweet-interact-bookmark${t.bookmarked ? ' tweet-interact-bookmarked' : ''}" data-val="${t.bookmark_count}">${Number(t.bookmark_count).toLocaleString().replace(/\s/g, ',')}</span>` : 1019 ''} 1020 <span class="tweet-interact-more"></span> 1021 <div class="tweet-interact-more-menu dropdown-menu" hidden> 1022 <span class="tweet-interact-more-menu-copy">${LOC.copy_link.message}</span> 1023 <span class="tweet-interact-more-menu-embed">${LOC.embed_tweet.message}</span> 1024 ${navigator.canShare ? `<span class="tweet-interact-more-menu-share">${LOC.share_tweet.message}</span>` : ''} 1025 <span class="tweet-interact-more-menu-newtwitter">${LOC.open_tweet_newtwitter.message}</span> 1026 ${t.user.id_str === user.id_str ? /*html*/` 1027 <hr> 1028 <span class="tweet-interact-more-menu-analytics">${LOC.tweet_analytics.message}</span> 1029 <span class="tweet-interact-more-menu-delete">${LOC.delete_tweet.message}</span> 1030 ` : ``} 1031 ${t.conversation_id_str && tweetStorage[t.conversation_id_str] && tweetStorage[t.conversation_id_str].user.id_str === user.id_str && t.user.id_str !== user.id_str ? /*html*/` 1032 <span class="tweet-interact-more-menu-hide">${t.moderated ? LOC.unhide_tweet.message : LOC.hide_tweet.message}</span> 1033 `: ''} 1034 ${t.hasModeratedReplies ? /*html*/` 1035 <span class="tweet-interact-more-menu-hidden"><a target="_blank" href="/${t.user.screen_name}/status/${t.id_str}/hidden?newtwitter=true">${LOC.see_hidden_replies.message}</a></span> 1036 ` : ''} 1037 <hr> 1038 ${t.user.id_str !== user.id_str && !options.mainTweet ? /*html*/` 1039 <span class="tweet-interact-more-menu-follow"${t.user.blocking ? ' hidden' : ''}>${t.user.following ? unfollowUserText : followUserText}</span> 1040 ` : ''} 1041 ${t.user.id_str !== user.id_str ? /*html*/` 1042 <span class="tweet-interact-more-menu-block">${t.user.blocking ? unblockUserText : blockUserText}</span> 1043 ` : ''} 1044 <span class="tweet-interact-more-menu-bookmark">${LOC.bookmark_tweet.message}</span> 1045 <span class="tweet-interact-more-menu-mute">${t.conversation_muted ? LOC.unmute_convo.message : LOC.mute_convo.message}</span> 1046 <hr> 1047 <span class="tweet-interact-more-menu-refresh">${LOC.refresh_tweet.message}</span> 1048 ${t.extended_entities && t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'animated_gif' ? /*html*/`<span class="tweet-interact-more-menu-download-gif" data-gifno="1">${LOC.download_gif.message}</span>` : ``} 1049 ${t.extended_entities && t.extended_entities.media.length > 1 ? t.extended_entities.media.filter(m => m.type === 'animated_gif').map((m, i) => /*html*/`<span class="tweet-interact-more-menu-download-gif" data-gifno="${i+1}">${LOC.download_gif.message} (#${i+1})</span>`).join('\n') : ''} 1050 ${t.extended_entities && t.extended_entities.media.length === 1 ? `<span class="tweet-interact-more-menu-download">${LOC.download_media.message}</span>` : ``} 1051 ${vars.developerMode ? /*html*/` 1052 <hr> 1053 <span class="tweet-interact-more-menu-copy-user-id">${LOC.copy_user_id.message}</span> 1054 <span class="tweet-interact-more-menu-copy-tweet-id">${LOC.copy_tweet_id.message}</span> 1055 <span class="tweet-interact-more-menu-log">Log tweet object</span> 1056 ` : ''} 1057 </div> 1058 </div> 1059 <div class="tweet-reply" hidden> 1060 <br> 1061 <b style="font-size: 12px;display: block;margin-bottom: 5px;">${LOC.replying_to_tweet.message} <span ${!vars.disableHotkeys ? 'title="ALT+M"' : ''} class="tweet-reply-upload">${LOC.upload_media_btn.message}</span> <span class="tweet-reply-add-emoji">${LOC.emoji_btn.message}</span> <span ${!vars.disableHotkeys ? 'title="ALT+R"' : ''} class="tweet-reply-cancel">${LOC.cancel_btn.message}</span></b> 1062 <span class="tweet-reply-error" style="color:red"></span> 1063 <textarea maxlength="1000" class="tweet-reply-text" placeholder="${LOC.reply_example.message}"></textarea> 1064 <button title="CTRL+ENTER" class="tweet-reply-button nice-button">${LOC.reply.message}</button><br> 1065 <span class="tweet-reply-char">0/280</span><br> 1066 <div class="tweet-reply-media" style="padding-bottom: 10px;"></div> 1067 </div> 1068 <div class="tweet-quote" hidden> 1069 <br> 1070 <b style="font-size: 12px;display: block;margin-bottom: 5px;">${LOC.quote_tweet.message} <span ${!vars.disableHotkeys ? 'title="ALT+M"' : ''} class="tweet-quote-upload">${LOC.upload_media_btn.message}</span> <span class="tweet-quote-add-emoji">${LOC.emoji_btn.message}</span> <span ${!vars.disableHotkeys ? 'title="ALT+Q"' : ''} class="tweet-quote-cancel">${LOC.cancel_btn.message}</span></b> 1071 <span class="tweet-quote-error" style="color:red"></span> 1072 <textarea maxlength="1000" class="tweet-quote-text" placeholder="${LOC.quote_example.message}"></textarea> 1073 <button title="CTRL+ENTER" class="tweet-quote-button nice-button">${LOC.quote.message}</button><br> 1074 <span class="tweet-quote-char">0/280</span><br> 1075 <div class="tweet-quote-media" style="padding-bottom: 10px;"></div> 1076 </div> 1077 <div class="tweet-view-self-thread-div" ${options.threadContinuation ? '' : 'hidden'}> 1078 <span class="tweet-view-self-thread-line"></span> 1079 <div class="tweet-view-self-thread-line-dots"></div> 1080 </div> 1081 </div> 1082 `; 1083 // video 1084 let vidOverlay = tweet.getElementsByClassName('tweet-media-video-overlay')[0]; 1085 if(vidOverlay) { 1086 vidOverlay.addEventListener('click', () => { 1087 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; 1088 vid.play(); 1089 vid.controls = true; 1090 vid.classList.remove('tweet-media-element-censor'); 1091 vidOverlay.style.display = 'none'; 1092 }); 1093 } 1094 if(videos) { 1095 let vids = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO'); 1096 vids[0].onloadstart = () => { 1097 let src = vids[0].currentSrc; 1098 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { 1099 if(el.dataset.url === src) el.classList.add('tweet-video-quality-current'); 1100 }); 1101 tweet.getElementsByClassName('tweet-video-reload')[0].addEventListener('click', () => { 1102 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; 1103 let time = vid.currentTime; 1104 let paused = vid.paused; 1105 vid.load(); 1106 vid.onloadstart = () => { 1107 let src = vid.currentSrc; 1108 vid.currentTime = time; 1109 if(!paused) vid.play(); 1110 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { 1111 if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current'); 1112 else el.classList.remove('tweet-video-quality-current'); 1113 }); 1114 } 1115 }); 1116 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => el.addEventListener('click', () => { 1117 if(el.className.includes('tweet-video-quality-current')) return; 1118 localStorage.preferredQuality = parseInt(el.innerText); 1119 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; 1120 let time = vid.currentTime; 1121 let paused = vid.paused; 1122 for(let v of videos) { 1123 let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => { 1124 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); 1125 }); 1126 let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url); 1127 if(preferredQualityVariantIndex !== -1) { 1128 let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex]; 1129 v.video_info.variants.splice(preferredQualityVariantIndex, 1); 1130 v.video_info.variants.unshift(preferredQualityVariant); 1131 } 1132 } 1133 tweet.getElementsByClassName('tweet-media')[0].innerHTML = /*html*/` 1134 ${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' ? ` 1135 ${m.video_info.variants.map(v => `<source src="${v.url}&ttd=${Date.now()}" type="${v.content_type}">`).join('\n')} 1136 ${LOC.unsupported_video.message} 1137 </video>` : ''}`).join('\n')} 1138 `; 1139 vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0]; 1140 vid.onloadstart = () => { 1141 let src = vid.currentSrc; 1142 vid.currentTime = time; 1143 if(!paused) vid.play(); 1144 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => { 1145 if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current'); 1146 else el.classList.remove('tweet-video-quality-current'); 1147 }); 1148 } 1149 vid.addEventListener('mousedown', e => { 1150 if(e.button === 1) { 1151 e.preventDefault(); 1152 window.open(vid.currentSrc, '_blank'); 1153 } 1154 }); 1155 })); 1156 }; 1157 for(let vid of vids) { 1158 if(typeof vars.volume === 'number') { 1159 vid.volume = vars.volume; 1160 } 1161 vid.onvolumechange = () => { 1162 chrome.storage.sync.set({ 1163 volume: vid.volume 1164 }, () => { }); 1165 let allVids = document.getElementsByTagName('video'); 1166 for(let i = 0; i < allVids.length; i++) { 1167 allVids[i].volume = vid.volume; 1168 } 1169 }; 1170 vid.addEventListener('mousedown', e => { 1171 if(e.button === 1) { 1172 e.preventDefault(); 1173 window.open(vid.currentSrc, '_blank'); 1174 } 1175 }); 1176 } 1177 } 1178 1179 let footerFavorites = tweet.getElementsByClassName('tweet-footer-favorites')[0]; 1180 if(t.card) { 1181 generateCard(t, tweet, user); 1182 } 1183 if (options.top) { 1184 tweet.querySelector('.tweet-top').hidden = false; 1185 const icon = document.createElement('span'); 1186 icon.innerText = options.top.icon; 1187 icon.classList.add('tweet-top-icon'); 1188 icon.style.color = options.top.color; 1189 1190 const span = document.createElement("span"); 1191 span.classList.add("tweet-top-text"); 1192 span.innerHTML = options.top.text; 1193 tweet.querySelector('.tweet-top').append(icon, span); 1194 } 1195 if(options.mainTweet) { 1196 let likers = this.mainTweetLikers.slice(0, 8); 1197 for(let i in likers) { 1198 let liker = likers[i]; 1199 let a = document.createElement('a'); 1200 a.href = `https://twitter.com/${liker.screen_name}`; 1201 let likerImg = document.createElement('img'); 1202 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}`; 1203 likerImg.classList.add('tweet-footer-favorites-img'); 1204 likerImg.title = liker.name + ' (@' + liker.screen_name + ')'; 1205 likerImg.width = 24; 1206 likerImg.height = 24; 1207 a.appendChild(likerImg); 1208 a.dataset.id = liker.id_str; 1209 footerFavorites.appendChild(a); 1210 } 1211 let likesLink = tweet.getElementsByClassName('tweet-footer-stat-f')[0]; 1212 likesLink.addEventListener('click', e => { 1213 e.preventDefault(); 1214 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/likes`); 1215 this.updateSubpage(); 1216 this.mediaToUpload = []; 1217 this.excludeUserMentions = []; 1218 this.linkColors = {}; 1219 this.cursor = undefined; 1220 this.seenReplies = []; 1221 this.mainTweetLikers = []; 1222 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1223 this.updateLikes(id); 1224 this.currentLocation = location.pathname; 1225 }); 1226 let retweetsLink = tweet.getElementsByClassName('tweet-footer-stat-r')[0]; 1227 retweetsLink.addEventListener('click', e => { 1228 e.preventDefault(); 1229 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`); 1230 this.updateSubpage(); 1231 this.mediaToUpload = []; 1232 this.excludeUserMentions = []; 1233 this.linkColors = {}; 1234 this.cursor = undefined; 1235 this.seenReplies = []; 1236 this.mainTweetLikers = []; 1237 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1238 this.updateRetweets(id); 1239 this.currentLocation = location.pathname; 1240 }); 1241 let repliesLink = tweet.getElementsByClassName('tweet-footer-stat-o')[0]; 1242 repliesLink.addEventListener('click', e => { 1243 e.preventDefault(); 1244 if(location.href === `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`) return; 1245 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`); 1246 this.updateSubpage(); 1247 this.mediaToUpload = []; 1248 this.excludeUserMentions = []; 1249 this.linkColors = {}; 1250 this.cursor = undefined; 1251 this.seenReplies = []; 1252 this.mainTweetLikers = []; 1253 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1254 this.updateReplies(id); 1255 this.currentLocation = location.pathname; 1256 }); 1257 } 1258 if(options.mainTweet && t.user.id_str !== user.id_str) { 1259 const tweetFollow = tweet.getElementsByClassName('tweet-header-follow')[0]; 1260 tweetFollow.addEventListener('click', async () => { 1261 if(t.user.following) { 1262 await API.user.unfollow(t.user.screen_name); 1263 tweetFollow.innerText = LOC.follow.message; 1264 tweetFollow.classList.remove('following'); 1265 tweetFollow.classList.add('follow'); 1266 t.user.following = false; 1267 } else { 1268 await API.user.follow(t.user.screen_name); 1269 tweetFollow.innerText = LOC.unfollow.message; 1270 tweetFollow.classList.remove('follow'); 1271 tweetFollow.classList.add('following'); 1272 t.user.following = true; 1273 } 1274 }); 1275 } 1276 const tweetBody = tweet.getElementsByClassName('tweet-body')[0]; 1277 const tweetBodyText = tweet.getElementsByClassName('tweet-body-text')[0]; 1278 const tweetTranslate = tweet.getElementsByClassName('tweet-translate')[0]; 1279 const tweetBodyQuote = tweet.getElementsByClassName('tweet-body-quote')[0]; 1280 const tweetBodyQuoteText = tweet.getElementsByClassName('tweet-body-text-quote')[0]; 1281 1282 const tweetReplyCancel = tweet.getElementsByClassName('tweet-reply-cancel')[0]; 1283 const tweetReplyUpload = tweet.getElementsByClassName('tweet-reply-upload')[0]; 1284 const tweetReplyAddEmoji = tweet.getElementsByClassName('tweet-reply-add-emoji')[0]; 1285 const tweetReply = tweet.getElementsByClassName('tweet-reply')[0]; 1286 const tweetReplyButton = tweet.getElementsByClassName('tweet-reply-button')[0]; 1287 const tweetReplyError = tweet.getElementsByClassName('tweet-reply-error')[0]; 1288 const tweetReplyText = tweet.getElementsByClassName('tweet-reply-text')[0]; 1289 const tweetReplyChar = tweet.getElementsByClassName('tweet-reply-char')[0]; 1290 const tweetReplyMedia = tweet.getElementsByClassName('tweet-reply-media')[0]; 1291 1292 const tweetInteract = tweet.getElementsByClassName('tweet-interact')[0]; 1293 const tweetInteractReply = tweet.getElementsByClassName('tweet-interact-reply')[0]; 1294 const tweetInteractRetweet = tweet.getElementsByClassName('tweet-interact-retweet')[0]; 1295 const tweetInteractFavorite = tweet.getElementsByClassName('tweet-interact-favorite')[0]; 1296 const tweetInteractBookmark = tweet.getElementsByClassName('tweet-interact-bookmark')[0]; 1297 const tweetInteractMore = tweet.getElementsByClassName('tweet-interact-more')[0]; 1298 1299 const tweetFooter = tweet.getElementsByClassName('tweet-footer')[0]; 1300 const tweetFooterReplies = tweet.getElementsByClassName('tweet-footer-stat-replies')[0]; 1301 const tweetFooterRetweets = tweet.getElementsByClassName('tweet-footer-stat-retweets')[0]; 1302 const tweetFooterFavorites = tweet.getElementsByClassName('tweet-footer-stat-favorites')[0]; 1303 1304 const tweetQuote = tweet.getElementsByClassName('tweet-quote')[0]; 1305 const tweetQuoteCancel = tweet.getElementsByClassName('tweet-quote-cancel')[0]; 1306 const tweetQuoteUpload = tweet.getElementsByClassName('tweet-quote-upload')[0]; 1307 const tweetQuoteAddEmoji = tweet.getElementsByClassName('tweet-quote-add-emoji')[0]; 1308 const tweetQuoteButton = tweet.getElementsByClassName('tweet-quote-button')[0]; 1309 const tweetQuoteError = tweet.getElementsByClassName('tweet-quote-error')[0]; 1310 const tweetQuoteText = tweet.getElementsByClassName('tweet-quote-text')[0]; 1311 const tweetQuoteChar = tweet.getElementsByClassName('tweet-quote-char')[0]; 1312 const tweetQuoteMedia = tweet.getElementsByClassName('tweet-quote-media')[0]; 1313 1314 const tweetInteractRetweetMenu = tweet.getElementsByClassName('tweet-interact-retweet-menu')[0]; 1315 const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweet')[0]; 1316 const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName('tweet-interact-retweet-menu-quote')[0]; 1317 const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName('tweet-interact-retweet-menu-quotes')[0]; 1318 const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweeters')[0]; 1319 1320 const tweetInteractMoreMenu = tweet.getElementsByClassName('tweet-interact-more-menu')[0]; 1321 const tweetInteractMoreMenuCopy = tweet.getElementsByClassName('tweet-interact-more-menu-copy')[0]; 1322 const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-tweet-id')[0]; 1323 const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-user-id')[0]; 1324 const tweetInteractMoreMenuLog = tweet.getElementsByClassName('tweet-interact-more-menu-log')[0]; 1325 const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName('tweet-interact-more-menu-embed')[0]; 1326 const tweetInteractMoreMenuShare = tweet.getElementsByClassName('tweet-interact-more-menu-share')[0]; 1327 const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName('tweet-interact-more-menu-newtwitter')[0]; 1328 const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName('tweet-interact-more-menu-analytics')[0]; 1329 const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName('tweet-interact-more-menu-refresh')[0]; 1330 const tweetInteractMoreMenuMute = tweet.getElementsByClassName('tweet-interact-more-menu-mute')[0]; 1331 const tweetInteractMoreMenuDownload = tweet.getElementsByClassName('tweet-interact-more-menu-download')[0]; 1332 const tweetInteractMoreMenuDownloadGifs = Array.from(tweet.getElementsByClassName('tweet-interact-more-menu-download-gif')); 1333 const tweetInteractMoreMenuDelete = tweet.getElementsByClassName('tweet-interact-more-menu-delete')[0]; 1334 const tweetInteractMoreMenuFollow = tweet.getElementsByClassName('tweet-interact-more-menu-follow')[0]; 1335 const tweetInteractMoreMenuBlock = tweet.getElementsByClassName('tweet-interact-more-menu-block')[0]; 1336 const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName('tweet-interact-more-menu-bookmark')[0]; 1337 const tweetInteractMoreMenuHide = tweet.getElementsByClassName('tweet-interact-more-menu-hide')[0]; 1338 1339 if(tweetInteractMoreMenuLog) tweetInteractMoreMenuLog.addEventListener('click', () => { 1340 console.log(t); 1341 }); 1342 1343 // moderating tweets 1344 if(tweetInteractMoreMenuHide) tweetInteractMoreMenuHide.addEventListener('click', async () => { 1345 if(t.moderated) { 1346 try { 1347 await API.tweet.unmoderate(t.id_str); 1348 } catch(e) { 1349 console.error(e); 1350 alert(e); 1351 return; 1352 } 1353 tweetInteractMoreMenuHide.innerText = LOC.hide_tweet.message; 1354 t.moderated = false; 1355 } else { 1356 let sure = confirm(LOC.hide_tweet_sure.message); 1357 if(!sure) return; 1358 try { 1359 await API.tweet.moderate(t.id_str); 1360 } catch(e) { 1361 console.error(e); 1362 alert(e); 1363 return; 1364 } 1365 tweetInteractMoreMenuHide.innerText = LOC.unhide_tweet.message; 1366 t.moderated = true; 1367 } 1368 }); 1369 1370 // community notes 1371 if(t.birdwatch && options.mainTweet && !vars.hideCommunityNotes) { 1372 let div = document.createElement('div'); 1373 div.classList.add('tweet-birdwatch', 'box'); 1374 let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); 1375 for(let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) { 1376 let entity = t.birdwatch.subtitle.entities[e]; 1377 if(!entity.ref) continue; 1378 text = arrayInsert(text, entity.toIndex, '</a>'); 1379 text = arrayInsert(text, entity.fromIndex, `<a href="${entity.ref.url}" target="_blank">`); 1380 } 1381 text = text.join(''); 1382 1383 div.innerHTML = /*html*/` 1384 <div class="tweet-birdwatch-header"> 1385 <span class="tweet-birdwatch-title">${escapeHTML(t.birdwatch.title)}</span> 1386 </div> 1387 <div class="tweet-birdwatch-body"> 1388 <span class="tweet-birdwatch-subtitle">${text}</span> 1389 </div> 1390 `; 1391 1392 if(tweetFooter) tweetFooter.before(div); 1393 else tweetInteract.before(div); 1394 } 1395 1396 // rtl languages 1397 if(rtlLanguages.includes(t.lang)) { 1398 tweetBody.classList.add('rtl'); 1399 } 1400 1401 // Quote body 1402 if(tweetBodyQuote) { 1403 tweetBodyQuote.addEventListener('click', e => { 1404 e.preventDefault(); 1405 history.pushState({}, null, `https://twitter.com/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`); 1406 this.updateSubpage(); 1407 this.mediaToUpload = []; 1408 this.excludeUserMentions = []; 1409 this.linkColors = {}; 1410 this.cursor = undefined; 1411 this.seenReplies = []; 1412 this.mainTweetLikers = []; 1413 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1414 if(this.subpage === 'tweet') { 1415 this.updateReplies(id); 1416 } else if(this.subpage === 'likes') { 1417 this.updateLikes(id); 1418 } else if(this.subpage === 'retweets') { 1419 this.updateRetweets(id); 1420 } else if(this.subpage === 'retweets_with_comments') { 1421 this.updateRetweetsWithComments(id); 1422 } 1423 this.currentLocation = location.pathname; 1424 }); 1425 if(rtlLanguages.includes(t.quoted_status.lang)) { 1426 tweetBodyQuoteText.classList.add('rtl'); 1427 } else { 1428 tweetBodyQuoteText.classList.add('ltr'); 1429 } 1430 } 1431 1432 // Translate 1433 if(tweetTranslate) tweetTranslate.addEventListener('click', async () => { 1434 let translated = await API.tweet.translate(t.id_str); 1435 tweetTranslate.hidden = true; 1436 let translatedMessage; 1437 if(LOC.translated_from.message.includes("$LANGUAGE$")) { 1438 translatedMessage = LOC.translated_from.message.replace("$LANGUAGE$", `[${translated.translated_lang}]`); 1439 } else { 1440 translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; 1441 } 1442 tweetBodyText.innerHTML += `<br>`+ 1443 `<span style="font-size: 12px;color: var(--light-gray);">${translatedMessage}:</span>`+ 1444 `<br>`+ 1445 `<span class="tweet-translated-text">${await renderTweetBodyHTML(translated.text, translated.entities)}</span>`; 1446 if(vars.enableTwemoji) twemoji.parse(tweetBodyText); 1447 }); 1448 1449 // Bookmarks 1450 let switchingBookmark = false; 1451 let switchBookmark = () => { 1452 if(switchingBookmark) return; 1453 switchingBookmark = true; 1454 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1455 if(t.bookmarked) { 1456 API.bookmarks.delete(t.id_str).then(() => { 1457 toast.info(LOC.unbookmarked_tweet.message); 1458 switchingBookmark = false; 1459 t.bookmarked = false; 1460 t.bookmark_count--; 1461 tweetInteractMoreMenuBookmark.innerText = LOC.bookmark_tweet.message; 1462 if(tweetInteractBookmark) { 1463 tweetInteractBookmark.classList.remove('tweet-interact-bookmarked'); 1464 if(vars.bookmarkButton !== 'show_all_no_count') { 1465 tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ','); 1466 } 1467 } 1468 }).catch(e => { 1469 switchingBookmark = false; 1470 console.error(e); 1471 alert(e); 1472 }); 1473 } else { 1474 API.bookmarks.create(t.id_str).then(() => { 1475 toast.info(LOC.bookmarked_tweet.message); 1476 switchingBookmark = false; 1477 t.bookmarked = true; 1478 t.bookmark_count++; 1479 tweetInteractMoreMenuBookmark.innerText = LOC.remove_bookmark.message; 1480 if(tweetInteractBookmark) { 1481 tweetInteractBookmark.classList.add('tweet-interact-bookmarked'); 1482 if(vars.bookmarkButton !== 'show_all_no_count') { 1483 tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ','); 1484 } 1485 } 1486 }).catch(e => { 1487 switchingBookmark = false; 1488 console.error(e); 1489 alert(e); 1490 }); 1491 } 1492 }; 1493 if(tweetInteractBookmark) tweetInteractBookmark.addEventListener('click', switchBookmark); 1494 if(tweetInteractMoreMenuBookmark) tweetInteractMoreMenuBookmark.addEventListener('click', switchBookmark); 1495 1496 // Media 1497 if (t.extended_entities && t.extended_entities.media) { 1498 const tweetMedia = tweet.getElementsByClassName('tweet-media')[0]; 1499 tweetMedia.addEventListener('click', e => { 1500 if (e.target.className && e.target.className.includes('tweet-media-element-censor')) { 1501 return e.target.classList.remove('tweet-media-element-censor'); 1502 } 1503 if (e.target.tagName === 'IMG') { 1504 if(!e.target.src.endsWith('?name=orig') && !e.target.src.startsWith('data:')) { 1505 e.target.src += '?name=orig'; 1506 } 1507 new Viewer(tweetMedia, { 1508 transition: false 1509 }); 1510 e.target.click(); 1511 } 1512 }); 1513 } 1514 1515 // Emojis 1516 [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach(e => { 1517 e.addEventListener('click', e => { 1518 let isReply = e.target.className === 'tweet-reply-add-emoji'; 1519 createEmojiPicker(isReply ? tweetReply : tweetQuote, isReply ? tweetReplyText : tweetQuoteText, {}); 1520 }); 1521 }); 1522 1523 // Reply 1524 tweetReplyCancel.addEventListener('click', () => { 1525 tweetReply.hidden = true; 1526 tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); 1527 }); 1528 let replyMedia = []; 1529 tweetReply.addEventListener('drop', e => { 1530 handleDrop(e, replyMedia, tweetReplyMedia); 1531 }); 1532 tweetReply.addEventListener('paste', event => { 1533 let items = (event.clipboardData || event.originalEvent.clipboardData).items; 1534 for (let index in items) { 1535 let item = items[index]; 1536 if (item.kind === 'file') { 1537 let file = item.getAsFile(); 1538 handleFiles([file], replyMedia, tweetReplyMedia); 1539 } 1540 } 1541 }); 1542 tweetReplyUpload.addEventListener('click', () => { 1543 getMedia(replyMedia, tweetReplyMedia); 1544 }); 1545 tweetInteractReply.addEventListener('click', () => { 1546 if(options.mainTweet) { 1547 document.getElementsByClassName('new-tweet-view')[0].click(); 1548 document.getElementsByClassName('new-tweet-text')[0].focus(); 1549 return; 1550 } 1551 if (!tweetQuote.hidden) tweetQuote.hidden = true; 1552 if (tweetReply.hidden) { 1553 tweetInteractReply.classList.add('tweet-interact-reply-clicked'); 1554 } else { 1555 tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); 1556 } 1557 tweetReply.hidden = !tweetReply.hidden; 1558 setTimeout(() => { 1559 tweetReplyText.focus(); 1560 }) 1561 }); 1562 tweetReplyText.addEventListener('keydown', e => { 1563 if (e.key === 'Enter' && e.ctrlKey) { 1564 tweetReplyButton.click(); 1565 } 1566 }); 1567 tweetReplyText.addEventListener('input', e => { 1568 let text = tweetReplyText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); 1569 tweetReplyChar.innerText = `${text.length}/280`; 1570 if(text.length > 265) { 1571 tweetReplyChar.style.color = "#c26363"; 1572 } else { 1573 tweetReplyChar.style.color = ""; 1574 } 1575 if (text.length > 280) { 1576 tweetReplyChar.style.color = "red"; 1577 tweetReplyButton.disabled = true; 1578 } else { 1579 tweetReplyButton.disabled = false; 1580 } 1581 }); 1582 tweetReplyButton.addEventListener('click', async () => { 1583 tweetReplyError.innerHTML = ''; 1584 let text = tweetReplyText.value; 1585 if (text.length === 0 && replyMedia.length === 0) return; 1586 tweetReplyButton.disabled = true; 1587 let uploadedMedia = []; 1588 for (let i in replyMedia) { 1589 let media = replyMedia[i]; 1590 try { 1591 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; 1592 let mediaId = await API.uploadMedia({ 1593 media_type: media.type, 1594 media_category: media.category, 1595 media: media.data, 1596 alt: media.alt, 1597 cw: media.cw, 1598 loadCallback: data => { 1599 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; 1600 } 1601 }); 1602 uploadedMedia.push(mediaId); 1603 } catch (e) { 1604 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; 1605 console.error(e); 1606 alert(e); 1607 } 1608 } 1609 let tweetObject = { 1610 status: text, 1611 in_reply_to_status_id: t.id_str 1612 }; 1613 if (uploadedMedia.length > 0) { 1614 tweetObject.media_ids = uploadedMedia.join(','); 1615 } 1616 let tweetData; 1617 try { 1618 tweetData = await API.tweet.postV2(tweetObject) 1619 } catch (e) { 1620 tweetReplyError.innerHTML = (e && e.message ? e.message : e) + "<br>"; 1621 tweetReplyButton.disabled = false; 1622 return; 1623 } 1624 if (!tweetData) { 1625 tweetReplyButton.disabled = false; 1626 tweetReplyError.innerHTML = `${LOC.error_sending_tweet.message}<br>`; 1627 return; 1628 } 1629 tweetReplyText.value = ''; 1630 tweetReplyChar.innerText = '0/280'; 1631 tweetReply.hidden = true; 1632 tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); 1633 if(!options.mainTweet) { 1634 tweetInteractReply.dataset.val = parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; 1635 tweetInteractReply.innerText = Number(parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1636 } else { 1637 tweetFooterReplies.dataset.val = parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; 1638 tweetFooterReplies.innerText = Number(parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1639 } 1640 tweetData._ARTIFICIAL = true; 1641 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1642 if(tweet.getElementsByClassName('tweet-self-thread-div')[0]) tweet.getElementsByClassName('tweet-self-thread-div')[0].hidden = false; 1643 tweetReplyButton.disabled = false; 1644 tweetReplyMedia.innerHTML = []; 1645 replyMedia = []; 1646 this.appendTweet(tweetData, document.getElementsByClassName('timeline')[0], { 1647 noTop: true, 1648 after: tweet 1649 }); 1650 }); 1651 1652 // Retweet / Quote Tweet 1653 let retweetClicked = false; 1654 tweetQuoteCancel.addEventListener('click', () => { 1655 tweetQuote.hidden = true; 1656 }); 1657 tweetInteractRetweet.addEventListener('click', async () => { 1658 if(tweetInteractRetweet.classList.contains('tweet-interact-retweet-disabled')) { 1659 return; 1660 } 1661 if (!tweetQuote.hidden) { 1662 tweetQuote.hidden = true; 1663 return; 1664 } 1665 if (tweetInteractRetweetMenu.hidden) { 1666 tweetInteractRetweetMenu.hidden = false; 1667 } 1668 if(retweetClicked) return; 1669 retweetClicked = true; 1670 setTimeout(() => { 1671 document.body.addEventListener('click', () => { 1672 retweetClicked = false; 1673 setTimeout(() => tweetInteractRetweetMenu.hidden = true, 50); 1674 }, { once: true }); 1675 }, 50); 1676 }); 1677 1678 tweetInteractRetweetMenuRetweet.addEventListener('click', async () => { 1679 if (!t.retweeted) { 1680 let tweetData; 1681 try { 1682 tweetData = await API.tweet.retweet(t.id_str); 1683 } catch (e) { 1684 console.error(e); 1685 return; 1686 } 1687 if (!tweetData) { 1688 return; 1689 } 1690 tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message; 1691 tweetInteractRetweet.classList.add('tweet-interact-retweeted'); 1692 t.retweeted = true; 1693 t.newTweetId = tweetData.id_str; 1694 if(!options.mainTweet) { 1695 tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; 1696 tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1697 } else { 1698 tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1699 } 1700 let event = new CustomEvent('tweetAction', { detail: { 1701 action: 'retweet', 1702 tweet: t, 1703 tweetData 1704 } }); 1705 document.dispatchEvent(event); 1706 } else { 1707 let tweetData; 1708 try { 1709 tweetData = await API.tweet.unretweet(t.retweeted_status ? t.retweeted_status.id_str : t.id_str); 1710 } catch (e) { 1711 console.error(e); 1712 return; 1713 } 1714 if (!tweetData) { 1715 return; 1716 } 1717 tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; 1718 tweetInteractRetweet.classList.remove('tweet-interact-retweeted'); 1719 t.retweeted = false; 1720 if(!options.mainTweet) { 1721 tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1; 1722 tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); 1723 } else { 1724 tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); 1725 } 1726 delete t.newTweetId; 1727 let event = new CustomEvent('tweetAction', { detail: { 1728 action: 'unretweet', 1729 tweet: t, 1730 tweetData 1731 } }); 1732 document.dispatchEvent(event); 1733 } 1734 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1735 }); 1736 if(options.mainTweet) { 1737 tweetInteractRetweetMenuQuotes.addEventListener('click', async () => { 1738 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`); 1739 this.updateSubpage(); 1740 this.mediaToUpload = []; 1741 this.excludeUserMentions = []; 1742 this.linkColors = {}; 1743 this.cursor = undefined; 1744 this.seenReplies = []; 1745 this.mainTweetLikers = []; 1746 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1747 if(this.subpage === 'tweet') { 1748 this.updateReplies(id); 1749 } else if(this.subpage === 'likes') { 1750 this.updateLikes(id); 1751 } else if(this.subpage === 'retweets') { 1752 this.updateRetweets(id); 1753 } else if(this.subpage === 'retweets_with_comments') { 1754 this.updateRetweetsWithComments(id); 1755 } 1756 this.currentLocation = location.pathname; 1757 }); 1758 tweetInteractRetweetMenuRetweeters.addEventListener('click', async () => { 1759 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`); 1760 this.updateSubpage(); 1761 this.mediaToUpload = []; 1762 this.excludeUserMentions = []; 1763 this.linkColors = {}; 1764 this.cursor = undefined; 1765 this.seenReplies = []; 1766 this.mainTweetLikers = []; 1767 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 1768 if(this.subpage === 'tweet') { 1769 this.updateReplies(id); 1770 } else if(this.subpage === 'likes') { 1771 this.updateLikes(id); 1772 } else if(this.subpage === 'retweets') { 1773 this.updateRetweets(id); 1774 } else if(this.subpage === 'retweets_with_comments') { 1775 this.updateRetweetsWithComments(id); 1776 } 1777 this.currentLocation = location.pathname; 1778 }); 1779 } 1780 tweetInteractRetweetMenuQuote.addEventListener('click', async () => { 1781 if (!tweetReply.hidden) { 1782 tweetInteractReply.classList.remove('tweet-interact-reply-clicked'); 1783 tweetReply.hidden = true; 1784 } 1785 tweetQuote.hidden = false; 1786 setTimeout(() => { 1787 tweetQuoteText.focus(); 1788 }) 1789 }); 1790 let quoteMedia = []; 1791 tweetQuote.addEventListener('drop', e => { 1792 handleDrop(e, quoteMedia, tweetQuoteMedia); 1793 }); 1794 tweetQuote.addEventListener('paste', event => { 1795 let items = (event.clipboardData || event.originalEvent.clipboardData).items; 1796 for (let index in items) { 1797 let item = items[index]; 1798 if (item.kind === 'file') { 1799 let file = item.getAsFile(); 1800 handleFiles([file], quoteMedia, tweetQuoteMedia); 1801 } 1802 } 1803 }); 1804 tweetQuoteUpload.addEventListener('click', () => { 1805 getMedia(quoteMedia, tweetQuoteMedia); 1806 }); 1807 tweetQuoteText.addEventListener('keydown', e => { 1808 if (e.key === 'Enter' && e.ctrlKey) { 1809 tweetQuoteButton.click(); 1810 } 1811 }); 1812 tweetQuoteText.addEventListener('input', e => { 1813 let text = tweetQuoteText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim(); 1814 tweetQuoteChar.innerText = `${text.length}/280`; 1815 if(text.length > 265) { 1816 tweetQuoteChar.style.color = "#c26363"; 1817 } else { 1818 tweetQuoteChar.style.color = ""; 1819 } 1820 if (text.length > 280) { 1821 tweetQuoteChar.style.color = "red"; 1822 tweetQuoteButton.disabled = true; 1823 } else { 1824 tweetQuoteButton.disabled = false; 1825 } 1826 }); 1827 tweetQuoteButton.addEventListener('click', async () => { 1828 let text = tweetQuoteText.value; 1829 tweetQuoteError.innerHTML = ''; 1830 if (text.length === 0 && quoteMedia.length === 0) return; 1831 tweetQuoteButton.disabled = true; 1832 let uploadedMedia = []; 1833 for (let i in quoteMedia) { 1834 let media = quoteMedia[i]; 1835 try { 1836 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false; 1837 let mediaId = await API.uploadMedia({ 1838 media_type: media.type, 1839 media_category: media.category, 1840 media: media.data, 1841 alt: media.alt, 1842 cw: media.cw, 1843 loadCallback: data => { 1844 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`; 1845 } 1846 }); 1847 uploadedMedia.push(mediaId); 1848 } catch (e) { 1849 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true; 1850 console.error(e); 1851 alert(e); 1852 } 1853 } 1854 let tweetObject = { 1855 status: text, 1856 attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}` 1857 }; 1858 if (uploadedMedia.length > 0) { 1859 tweetObject.media_ids = uploadedMedia.join(','); 1860 } 1861 let tweetData; 1862 try { 1863 tweetData = await API.tweet.postV2(tweetObject) 1864 } catch (e) { 1865 tweetQuoteError.innerHTML = (e && e.message ? e.message : e) + "<br>"; 1866 tweetQuoteButton.disabled = false; 1867 return; 1868 } 1869 if (!tweetData) { 1870 tweetQuoteError.innerHTML = `${LOC.error_sending_tweet.message}<br>`; 1871 tweetQuoteButton.disabled = false; 1872 return; 1873 } 1874 tweetQuoteText.value = ''; 1875 tweetQuote.hidden = true; 1876 tweetData._ARTIFICIAL = true; 1877 quoteMedia = []; 1878 tweetQuoteChar.innerText = '0/280'; 1879 tweetQuoteButton.disabled = false; 1880 tweetQuoteMedia.innerHTML = ''; 1881 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1882 this.appendTweet(tweetData, timelineContainer, { prepend: true }); 1883 }); 1884 1885 // Favorite 1886 tweetInteractFavorite.addEventListener('click', () => { 1887 if (t.favorited) { 1888 API.tweet.unfavorite(t.id_str); 1889 t.favorited = false; 1890 t.favorite_count--; 1891 if(!options.mainTweet) { 1892 tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1; 1893 tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); 1894 } else { 1895 if(this.mainTweetLikers.find(liker => liker.id_str === user.id_str)) { 1896 this.mainTweetLikers.splice(this.mainTweetLikers.findIndex(liker => liker.id_str === user.id_str), 1); 1897 let likerImg = footerFavorites.querySelector(`a[data-id="${user.id_str}"]`); 1898 if(likerImg) likerImg.remove() 1899 } 1900 tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ','); 1901 } 1902 tweetInteractFavorite.classList.remove('tweet-interact-favorited'); 1903 let event = new CustomEvent('tweetAction', { detail: { 1904 action: 'unfavorite', 1905 tweet: t 1906 } }); 1907 document.dispatchEvent(event); 1908 } else { 1909 API.tweet.favorite(t.id_str); 1910 t.favorited = true; 1911 t.favorite_count++; 1912 if(!options.mainTweet) { 1913 tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1; 1914 tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1915 } else { 1916 if(footerFavorites.children.length < 8 && !this.mainTweetLikers.find(liker => liker.id_str === user.id_str)) { 1917 let a = document.createElement('a'); 1918 a.href = `https://twitter.com/${user.screen_name}`; 1919 let likerImg = document.createElement('img'); 1920 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}` ; 1921 likerImg.classList.add('tweet-footer-favorites-img'); 1922 likerImg.title = user.name + ' (@' + user.screen_name + ')'; 1923 likerImg.width = 24; 1924 likerImg.height = 24; 1925 a.dataset.id = user.id_str; 1926 a.appendChild(likerImg); 1927 footerFavorites.appendChild(a); 1928 this.mainTweetLikers.push(user); 1929 } 1930 tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ','); 1931 } 1932 tweetInteractFavorite.classList.add('tweet-interact-favorited'); 1933 let event = new CustomEvent('tweetAction', { detail: { 1934 action: 'favorite', 1935 tweet: t 1936 }}); 1937 document.dispatchEvent(event); 1938 } 1939 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1940 }); 1941 1942 // More 1943 let moreClicked = false; 1944 tweetInteractMore.addEventListener('click', () => { 1945 if (tweetInteractMoreMenu.hidden) { 1946 tweetInteractMoreMenu.hidden = false; 1947 } 1948 if(moreClicked) return; 1949 moreClicked = true; 1950 setTimeout(() => { 1951 document.body.addEventListener('click', () => { 1952 moreClicked = false; 1953 setTimeout(() => tweetInteractMoreMenu.hidden = true, 50); 1954 }, { once: true }); 1955 }, 50); 1956 }); 1957 if(tweetInteractMoreMenuFollow) tweetInteractMoreMenuFollow.addEventListener('click', async () => { 1958 if (t.user.following) { 1959 await API.user.unfollow(t.user.screen_name); 1960 t.user.following = false; 1961 if(LOC.follow_user.message.includes("$SCREEN_NAME$")) { 1962 tweetInteractMoreMenuFollow.innerText = LOC.follow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); 1963 } else { 1964 tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; 1965 } 1966 let event = new CustomEvent('tweetAction', { detail: { 1967 action: 'unfollow', 1968 tweet: t 1969 } }); 1970 document.dispatchEvent(event); 1971 } else { 1972 await API.user.follow(t.user.screen_name); 1973 t.user.following = true; 1974 if(LOC.unfollow_user.message.includes("$SCREEN_NAME$")) { 1975 tweetInteractMoreMenuFollow.innerText = LOC.unfollow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); 1976 } else { 1977 tweetInteractMoreMenuFollow.innerText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; 1978 } 1979 let event = new CustomEvent('tweetAction', { detail: { 1980 action: 'follow', 1981 tweet: t 1982 } }); 1983 document.dispatchEvent(event); 1984 } 1985 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 1986 }); 1987 if(tweetInteractMoreMenuBlock) tweetInteractMoreMenuBlock.addEventListener('click', async () => { 1988 if (t.user.blocking) { 1989 await API.user.unblock(t.user.id_str); 1990 t.user.blocking = false; 1991 if(LOC.block_user.message.includes("$SCREEN_NAME$")) { 1992 tweetInteractMoreMenuBlock.innerText = LOC.block_user.message.replace("$SCREEN_NAME$", t.user.screen_name); 1993 } else { 1994 tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; 1995 } 1996 tweetInteractMoreMenuFollow.hidden = false; 1997 let event = new CustomEvent('tweetAction', { detail: { 1998 action: 'unblock', 1999 tweet: t 2000 } }); 2001 document.dispatchEvent(event); 2002 } else { 2003 let blockMessage; 2004 if(LOC.block_sure.message.includes("$SCREEN_NAME$")) { 2005 blockMessage = LOC.block_sure.message.replace("$SCREEN_NAME$", t.user.screen_name); 2006 } else { 2007 blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; 2008 } 2009 let c = confirm(blockMessage); 2010 if (!c) return; 2011 await API.user.block(t.user.id_str); 2012 t.user.blocking = true; 2013 if(LOC.unblock_user.message.includes("$SCREEN_NAME$")) { 2014 tweetInteractMoreMenuBlock.innerText = LOC.unblock_user.message.replace("$SCREEN_NAME$", t.user.screen_name); 2015 } else { 2016 tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; 2017 } 2018 tweetInteractMoreMenuFollow.hidden = true; 2019 t.user.following = false; 2020 if(LOC.follow_user.message.includes("$SCREEN_NAME$")) { 2021 tweetInteractMoreMenuFollow.innerText = LOC.follow_user.message.replace("$SCREEN_NAME$", t.user.screen_name); 2022 } else { 2023 tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; 2024 } 2025 let event = new CustomEvent('tweetAction', { detail: { 2026 action: 'block', 2027 tweet: t 2028 } }); 2029 document.dispatchEvent(event); 2030 } 2031 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 2032 }); 2033 tweetInteractMoreMenuCopy.addEventListener('click', () => { 2034 navigator.clipboard.writeText(`https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}`); 2035 }); 2036 if(tweetInteractMoreMenuCopyTweetId) tweetInteractMoreMenuCopyTweetId.addEventListener('click', () => { 2037 navigator.clipboard.writeText(t.id_str); 2038 }); 2039 if(tweetInteractMoreMenuCopyUserId) tweetInteractMoreMenuCopyUserId.addEventListener('click', () => { 2040 navigator.clipboard.writeText(t.user.id_str); 2041 }); 2042 if(tweetInteractMoreMenuShare) tweetInteractMoreMenuShare.addEventListener('click', () => { 2043 navigator.share({ url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}` }); 2044 }); 2045 tweetInteractMoreMenuNewtwitter.addEventListener('click', () => { 2046 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}?newtwitter=true`); 2047 }); 2048 tweetInteractMoreMenuEmbed.addEventListener('click', () => { 2049 openInNewTab(`https://publish.twitter.com/?query=https://twitter.com/${t.user.screen_name}/status/${t.id_str}&widget=Tweet`); 2050 }); 2051 if (t.user.id_str === user.id_str) { 2052 tweetInteractMoreMenuAnalytics.addEventListener('click', () => { 2053 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true`); 2054 }); 2055 tweetInteractMoreMenuDelete.addEventListener('click', async () => { 2056 let sure = confirm(LOC.delete_sure.message); 2057 if (!sure) return; 2058 try { 2059 await API.tweet.delete(t.id_str); 2060 } catch (e) { 2061 alert(e); 2062 console.error(e); 2063 return; 2064 } 2065 Array.from(document.getElementsByClassName('timeline')[0].getElementsByClassName(`tweet-id-${t.id_str}`)).forEach(tweet => { 2066 tweet.remove(); 2067 }); 2068 if(document.getElementById('timeline')) Array.from(document.getElementById('timeline').getElementsByClassName(`tweet-id-${t.id_str}`)).forEach(tweet => { 2069 tweet.remove(); 2070 }); 2071 if(options.mainTweet) { 2072 let tweets = Array.from(timelineContainer.getElementsByClassName('tweet')); 2073 if(tweets.length === 0) { 2074 document.getElementsByClassName('modal-close')[0].click(); 2075 } else { 2076 tweets[0].click(); 2077 } 2078 } 2079 if(typeof timeline !== 'undefined') { 2080 timeline.data = timeline.data.filter(tweet => tweet.id_str !== t.id_str); 2081 } 2082 if(options.after) { 2083 if(options.after.getElementsByClassName('tweet-self-thread-div')[0]) options.after.getElementsByClassName('tweet-self-thread-div')[0].hidden = true; 2084 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(); 2085 else options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText = (+options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText - 1).toString(); 2086 } 2087 let event = new CustomEvent('tweetAction', { detail: { 2088 action: 'delete', 2089 tweet: t 2090 } }); 2091 document.dispatchEvent(event); 2092 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 2093 }); 2094 } 2095 tweetInteractMoreMenuMute.addEventListener('click', async () => { 2096 if(t.conversation_muted) { 2097 await API.tweet.unmute(t.id_str); 2098 toast.info(LOC.unmuted_convo.message); 2099 t.conversation_muted = false; 2100 tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; 2101 let event = new CustomEvent('tweetAction', { detail: { 2102 action: 'unmute', 2103 tweet: t 2104 } }); 2105 document.dispatchEvent(event); 2106 } else { 2107 await API.tweet.mute(t.id_str); 2108 toast.info(LOC.muted_convo.message); 2109 t.conversation_muted = true; 2110 tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; 2111 let event = new CustomEvent('tweetAction', { detail: { 2112 action: 'mute', 2113 tweet: t 2114 } }); 2115 document.dispatchEvent(event); 2116 } 2117 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 2118 }); 2119 tweetInteractMoreMenuRefresh.addEventListener('click', async () => { 2120 let tweetData; 2121 try { 2122 tweetData = await API.tweet.getV2(t.id_str); 2123 } catch (e) { 2124 console.error(e); 2125 return; 2126 } 2127 if (!tweetData) { 2128 return; 2129 } 2130 if (tweetInteractFavorite.className.includes('tweet-interact-favorited') && !tweetData.favorited) { 2131 tweetInteractFavorite.classList.remove('tweet-interact-favorited'); 2132 } 2133 if (tweetInteractRetweet.className.includes('tweet-interact-retweeted') && !tweetData.retweeted) { 2134 tweetInteractRetweet.classList.remove('tweet-interact-retweeted'); 2135 } 2136 if (!tweetInteractFavorite.className.includes('tweet-interact-favorited') && tweetData.favorited) { 2137 tweetInteractFavorite.classList.add('tweet-interact-favorited'); 2138 } 2139 if (!tweetInteractRetweet.className.includes('tweet-interact-retweeted') && tweetData.retweeted) { 2140 tweetInteractRetweet.classList.add('tweet-interact-retweeted'); 2141 } 2142 if(!options.mainTweet) { 2143 tweetInteractFavorite.innerText = tweetData.favorite_count; 2144 tweetInteractRetweet.innerText = tweetData.retweet_count; 2145 tweetInteractReply.innerText = tweetData.reply_count; 2146 } 2147 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {}); 2148 }); 2149 let downloading = false; 2150 if (t.extended_entities && t.extended_entities.media.length === 1) { 2151 tweetInteractMoreMenuDownload.addEventListener('click', () => { 2152 if (downloading) return; 2153 downloading = true; 2154 let media = t.extended_entities.media[0]; 2155 let url = media.type === 'photo' ? media.media_url_https : media.video_info.variants[0].url; 2156 fetch(url).then(res => res.blob()).then(blob => { 2157 downloading = false; 2158 let a = document.createElement('a'); 2159 a.href = URL.createObjectURL(blob); 2160 a.download = media.type === 'photo' ? media.media_url_https.split('/').pop() : media.video_info.variants[0].url.split('/').pop(); 2161 a.download = a.download.split('?')[0]; 2162 a.click(); 2163 a.remove(); 2164 }).catch(e => { 2165 downloading = false; 2166 console.error(e); 2167 }); 2168 }); 2169 } 2170 if (t.extended_entities && t.extended_entities.media.some(m => m.type === 'animated_gif')) { 2171 tweetInteractMoreMenuDownloadGifs.forEach(dgb => dgb.addEventListener('click', e => { 2172 if (downloading) return; 2173 downloading = true; 2174 let n = parseInt(e.target.dataset.gifno)-1; 2175 let videos = Array.from(tweet.getElementsByClassName('tweet-media-gif')); 2176 let video = videos[n]; 2177 let canvas = document.createElement('canvas'); 2178 canvas.width = video.videoWidth; 2179 canvas.height = video.videoHeight; 2180 let ctx = canvas.getContext('2d'); 2181 if (video.duration > 10 && !confirm(LOC.long_vid.message)) { 2182 return downloading = false; 2183 } 2184 let mde = tweet.getElementsByClassName('tweet-media-data')[0]; 2185 mde.innerText = LOC.initialization.message + '...'; 2186 let gif = new GIF({ 2187 workers: 4, 2188 quality: 15, 2189 debug: true 2190 }); 2191 video.currentTime = 0; 2192 video.loop = false; 2193 let isFirst = true; 2194 let interval = setInterval(async () => { 2195 if(isFirst) { 2196 video.currentTime = 0; 2197 isFirst = false; 2198 await sleep(5); 2199 } 2200 mde.innerText = `${LOC.initialization.message}... (${Math.round(video.currentTime/video.duration*100|0)}%)`; 2201 if (video.currentTime+0.1 >= video.duration) { 2202 clearInterval(interval); 2203 gif.on('working', (frame, frames) => { 2204 mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; 2205 }); 2206 gif.on('finished', blob => { 2207 mde.innerText = ''; 2208 let a = document.createElement('a'); 2209 a.href = URL.createObjectURL(blob); 2210 a.download = `${t.id_str}.gif`; 2211 document.body.append(a); 2212 a.click(); 2213 a.remove(); 2214 downloading = false; 2215 video.loop = true; 2216 video.play(); 2217 }); 2218 gif.render(); 2219 return; 2220 } 2221 ctx.drawImage(video, 0, 0, canvas.width, canvas.height); 2222 let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); 2223 gif.addFrame(imgData, { delay: 100 }); 2224 }, 100); 2225 })); 2226 } 2227 2228 if(options.after) { 2229 options.after.after(tweet); 2230 } else if (options.before) { 2231 options.before.before(tweet); 2232 } else if (options.prepend) { 2233 timelineContainer.prepend(tweet); 2234 } else { 2235 timelineContainer.append(tweet); 2236 } 2237 if(vars.enableTwemoji) twemoji.parse(tweet); 2238 return tweet; 2239 } 2240 async popstateChange(that) { 2241 that.savePageData(that.currentLocation); 2242 that.updateSubpage(); 2243 if(location.pathname.includes("retweets/with_comments") && that.subpage === 'retweets_with_comments' && document.getElementById("this-is-tweet-page")) { 2244 return document.querySelector('.modal-close').click(); 2245 } 2246 that.mediaToUpload = []; 2247 that.excludeUserMentions = []; 2248 that.linkColors = {}; 2249 that.cursor = undefined; 2250 that.seenReplies = []; 2251 that.mainTweetLikers = []; 2252 let id; 2253 try { 2254 id = location.pathname.match(/status\/(\d{1,32})/)[1]; 2255 } catch(e) { 2256 return that.container.getElementsByClassName('modal-close')[0].click(); 2257 } 2258 let restored = await that.restorePageData(); 2259 if(!restored) { 2260 if(that.subpage === 'tweet') { 2261 that.updateReplies(id); 2262 } else if(that.subpage === 'likes') { 2263 that.updateLikes(id); 2264 } else if(that.subpage === 'retweets') { 2265 that.updateRetweets(id); 2266 } else if(that.subpage === 'retweets_with_comments') { 2267 that.updateRetweetsWithComments(id); 2268 } 2269 } else { 2270 this.container.scrollTop = restored.scrollY; 2271 } 2272 that.currentLocation = location.pathname; 2273 } 2274 async onScroll(that) { 2275 if(this.container.scrollTop + 300 > this.container.scrollHeight - this.container.clientHeight && !that.loadingNewTweets) { 2276 if(this.moreBtn && that.subpage === 'tweet' && !this.moreBtn.hidden) { 2277 this.moreBtn.click(); 2278 } 2279 } 2280 } 2281 async appendTombstone(timelineContainer, text) { 2282 this.tweets.push(['tombstone', text]); 2283 let tombstone = document.createElement('div'); 2284 tombstone.className = 'tweet-tombstone'; 2285 tombstone.innerHTML = text; 2286 timelineContainer.append(tombstone); 2287 } 2288 init() { 2289 document.getElementsByClassName('timeline-more')[0].addEventListener('click', async e => { 2290 if (!this.cursor || this.loadingNewTweets) return; 2291 this.loadingNewTweets = true; 2292 e.target.innerText = LOC.loading_tweets.message; 2293 let path = location.pathname; 2294 if(path.endsWith('/')) path = path.slice(0, -1); 2295 this.updateReplies(path.split('/').slice(-1)[0], this.cursor); 2296 }); 2297 document.getElementsByClassName('likes-more')[0].addEventListener('click', async () => { 2298 if(!this.likeCursor) return; 2299 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 2300 this.updateLikes(id, this.likeCursor); 2301 }); 2302 document.getElementsByClassName('retweets-more')[0].addEventListener('click', async () => { 2303 if(!this.retweetCursor) return; 2304 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 2305 this.updateRetweets(id, this.retweetCursor); 2306 }); 2307 document.getElementsByClassName('retweets_with_comments-more')[0].addEventListener('click', async () => { 2308 if(!this.retweetCommentsCursor) return; 2309 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 2310 this.updateRetweetsWithComments(id, this.retweetCommentsCursor); 2311 }); 2312 2313 this.updateSubpage(); 2314 let id = location.pathname.match(/status\/(\d{1,32})/)[1]; 2315 if(this.subpage === 'tweet') { 2316 this.updateReplies(id); 2317 } else if(this.subpage === 'likes') { 2318 this.updateLikes(id); 2319 } else if(this.subpage === 'retweets') { 2320 this.updateRetweets(id); 2321 } else if(this.subpage === 'retweets_with_comments') { 2322 this.updateRetweetsWithComments(id); 2323 } 2324 this.popstateHelper = () => this.popstateChange(this); 2325 this.scrollHelper = () => this.onScroll(this); 2326 window.addEventListener("popstate", this.popstateHelper); 2327 this.container.addEventListener("scroll", this.scrollHelper, { passive: true }); 2328 } 2329 close() { 2330 document.removeEventListener('scroll', this.onscroll); 2331 window.removeEventListener("popstate", this.popstateHelper); 2332 this.container.removeEventListener("scroll", this.scrollHelper); 2333 } 2334}