Extension to return old Twitter layout from 2015.
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}