Extension to return old Twitter layout from 2015.
1const linkRegex = /(\s|^)(http(s)?:\/\/.)?(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,60}\.(삼성|닷컴|닷넷|香格里拉|餐厅|食品|飞利浦|電訊盈科|集团|通販|购物|谷歌|诺基亚|联通|网络|网站|网店|网址|组织机构|移动|珠宝|点看|游戏|淡马锡|机构|書籍|时尚|新闻|政府|政务|招聘|手表|手机|我爱你|慈善|微博|广东|工行|家電|娱乐|天主教|大拿|大众汽车|在线|嘉里大酒店|嘉里|商标|商店|商城|公益|公司|八卦|健康|信息|佛山|企业|亚马逊|中文网|中信|世界|ポイント|ファッション|セール|ストア|コム|グーグル|クラウド|アマゾン|みんな|คอม|संगठन|नेट|कॉम|همراه|موقع|موبايلي|كوم|كاثوليك|عرب|شبكة|بيتك|بازار|العليان|ارامكو|اتصالات|ابوظبي|קום|сайт|рус|орг|онлайн|москва|ком|католик|дети|zuerich|zone|zippo|zip|zero|zara|zappos|yun|youtube|you|yokohama|yoga|yodobashi|yandex|yamaxun|yahoo|yachts|xyz|xxx|xperia|xin|xihuan|xfinity|xerox|xbox|wtf|wtc|wow|world|works|work|woodside|wolterskluwer|wme|winners|wine|windows|win|williamhill|wiki|wien|whoswho|weir|weibo|wedding|wed|website|weber|webcam|weatherchannel|weather|watches|watch|warman|wanggou|wang|walter|walmart|wales|vuelos|voyage|voto|voting|vote|volvo|volkswagen|vodka|vlaanderen|vivo|viva|vistaprint|vista|vision|visa|virgin|vip|vin|villas|viking|vig|video|viajes|vet|versicherung|vermögensberatung|vermögensberater|verisign|ventures|vegas|vanguard|vana|vacations|ups|uol|uno|university|unicom|uconnect|ubs|ubank|tvs|tushu|tunes|tui|tube|trv|trust|travelersinsurance|travelers|travelchannel|travel|training|trading|trade|toys|toyota|town|tours|total|toshiba|toray|top|tools|tokyo|today|tmall|tkmaxx|tjx|tjmaxx|tirol|tires|tips|tiffany|tienda|tickets|tiaa|theatre|theater|thd|teva|tennis|temasek|telefonica|telecity|tel|technology|tech|team|tdk|tci|taxi|tax|tattoo|tatar|tatamotors|target|taobao|talk|taipei|tab|systems|symantec|sydney|swiss|swiftcover|swatch|suzuki|surgery|surf|support|supply|supplies|sucks|style|study|studio|stream|store|storage|stockholm|stcgroup|stc|statoil|statefarm|statebank|starhub|star|staples|stada|srt|srl|spreadbetting|spot|sport|spiegel|space|soy|sony|song|solutions|solar|sohu|software|softbank|social|soccer|sncf|smile|smart|sling|skype|sky|skin|ski|site|singles|sina|silk|shriram|showtime|show|shouji|shopping|shop|shoes|shiksha|shia|shell|shaw|sharp|shangrila|sfr|sexy|sex|sew|seven|ses|services|sener|select|seek|security|secure|seat|search|scot|scor|scjohnson|science|schwarz|schule|school|scholarships|schmidt|schaeffler|scb|sca|sbs|sbi|saxo|save|sas|sarl|sapo|sap|sanofi|sandvikcoromant|sandvik|samsung|samsclub|salon|sale|sakura|safety|safe|saarland|ryukyu|rwe|run|ruhr|rugby|rsvp|room|rogers|rodeo|rocks|rocher|rmit|rip|rio|ril|rightathome|ricoh|richardli|rich|rexroth|reviews|review|restaurant|rest|republican|report|repair|rentals|rent|ren|reliance|reit|reisen|reise|rehab|redumbrella|redstone|red|recipes|realty|realtor|realestate|read|raid|radio|racing|qvc|quest|quebec|qpon|pwc|pub|prudential|pru|protection|property|properties|promo|progressive|prof|productions|prod|pro|prime|press|praxi|pramerica|post|porn|politie|poker|pohl|pnc|plus|plumbing|playstation|play|place|pizza|pioneer|pink|ping|pin|pid|pictures|pictet|pics|piaget|physio|photos|photography|photo|phone|philips|phd|pharmacy|pfizer|pet|pccw|pay|passagens|party|parts|partners|pars|paris|panerai|panasonic|pamperedchef|page|ovh|ott|otsuka|osaka|origins|orientexpress|organic|org|orange|oracle|open|ooo|onyourside|online|onl|ong|one|omega|ollo|oldnavy|olayangroup|olayan|okinawa|office|off|observer|obi|nyc|ntt|nrw|nra|nowtv|nowruz|now|norton|northwesternmutual|nokia|nissay|nissan|ninja|nikon|nike|nico|nhk|ngo|nfl|nexus|nextdirect|next|news|newholland|new|neustar|network|netflix|netbank|net|nec|nba|navy|natura|nationwide|name|nagoya|nadex|nab|mutuelle|mutual|museum|mtr|mtpc|mtn|msd|movistar|movie|mov|motorcycles|moto|moscow|mortgage|mormon|mopar|montblanc|monster|money|monash|mom|moi|moe|moda|mobily|mobile|mobi|mma|mls|mlb|mitsubishi|mit|mint|mini|mil|microsoft|miami|metlife|merckmsd|meo|menu|men|memorial|meme|melbourne|meet|media|med|mckinsey|mcdonalds|mcd|mba|mattel|maserati|marshalls|marriott|markets|marketing|market|map|mango|management|man|makeup|maison|maif|madrid|macys|luxury|luxe|lupin|lundbeck|ltda|ltd|lplfinancial|lpl|love|lotto|lotte|london|lol|loft|locus|locker|loans|loan|llp|llc|lixil|living|live|lipsy|link|linde|lincoln|limo|limited|lilly|like|lighting|lifestyle|lifeinsurance|life|lidl|liaison|lgbt|lexus|lego|legal|lefrak|leclerc|lease|lds|lawyer|law|latrobe|latino|lat|lasalle|lanxess|landrover|land|lancome|lancia|lancaster|lamer|lamborghini|ladbrokes|lacaixa|kyoto|kuokgroup|kred|krd|kpn|kpmg|kosher|komatsu|koeln|kiwi|kitchen|kindle|kinder|kim|kia|kfh|kerryproperties|kerrylogistics|kerryhotels|kddi|kaufen|juniper|juegos|jprs|jpmorgan|joy|jot|joburg|jobs|jnj|jmp|jll|jlc|jio|jewelry|jetzt|jeep|jcp|jcb|java|jaguar|iwc|iveco|itv|itau|istanbul|ist|ismaili|iselect|irish|ipiranga|investments|intuit|international|intel|int|insure|insurance|institute|ink|ing|info|infiniti|industries|inc|immobilien|immo|imdb|imamat|ikano|iinet|ifm|ieee|icu|ice|icbc|ibm|hyundai|hyatt|hughes|htc|hsbc|how|house|hotmail|hotels|hoteles|hot|hosting|host|hospital|horse|honeywell|honda|homesense|homes|homegoods|homedepot|holiday|holdings|hockey|hkt|hiv|hitachi|hisamitsu|hiphop|hgtv|hermes|here|helsinki|help|healthcare|health|hdfcbank|hdfc|hbo|haus|hangout|hamburg|hair|guru|guitars|guide|guge|gucci|guardian|group|grocery|gripe|green|gratis|graphics|grainger|gov|got|gop|google|goog|goodyear|goodhands|goo|golf|goldpoint|gold|godaddy|gmx|gmo|gmbh|gmail|globo|global|gle|glass|glade|giving|gives|gifts|gift|ggee|george|genting|gent|gea|gdn|gbiz|gay|garden|gap|games|game|gallup|gallo|gallery|gal|fyi|futbol|furniture|fund|fun|fujixerox|fujitsu|ftr|frontier|frontdoor|frogans|frl|fresenius|free|fox|foundation|forum|forsale|forex|ford|football|foodnetwork|food|foo|fly|flsmidth|flowers|florist|flir|flights|flickr|fitness|fit|fishing|fish|firmdale|firestone|fire|financial|finance|final|film|fido|fidelity|fiat|ferrero|ferrari|feedback|fedex|fast|fashion|farmers|farm|fans|fan|family|faith|fairwinds|fail|fage|extraspace|express|exposed|expert|exchange|everbank|events|eus|eurovision|etisalat|esurance|estate|esq|erni|ericsson|equipment|epson|epost|enterprises|engineering|engineer|energy|emerck|email|education|edu|edeka|eco|eat|earth|dvr|dvag|durban|dupont|duns|dunlop|duck|dubai|dtv|drive|download|dot|doosan|domains|doha|dog|dodge|doctor|docs|dnp|diy|dish|discover|discount|directory|direct|digital|diet|diamonds|dhl|dev|design|desi|dentist|dental|democrat|delta|deloitte|dell|delivery|degree|deals|dealer|deal|dds|dclk|day|datsun|dating|date|data|dance|dad|dabur|cyou|cymru|cuisinella|csc|cruises|cruise|crs|crown|cricket|creditunion|creditcard|credit|cpa|courses|coupons|coupon|country|corsica|coop|cool|cookingchannel|cooking|contractors|contact|consulting|construction|condos|comsec|computer|compare|company|community|commbank|comcast|com|cologne|college|coffee|codes|coach|clubmed|club|cloud|clothing|clinique|clinic|click|cleaning|claims|cityeats|city|citic|citi|citadel|cisco|circle|cipriani|church|chrysler|chrome|christmas|chloe|chintai|cheap|chat|chase|charity|channel|chanel|cfd|cfa|cern|ceo|center|ceb|cbs|cbre|cbn|cba|catholic|catering|cat|casino|cash|caseih|case|casa|cartier|cars|careers|career|care|cards|caravan|car|capitalone|capital|capetown|canon|cancerresearch|camp|camera|cam|calvinklein|call|cal|cafe|cab|bzh|buzz|buy|business|builders|build|bugatti|budapest|brussels|brother|broker|broadway|bridgestone|bradesco|box|boutique|bot|boston|bostik|bosch|boots|booking|book|boo|bond|bom|bofa|boehringer|boats|bnpparibas|bnl|bmw|bms|blue|bloomberg|blog|blockbuster|blanco|blackfriday|black|biz|bio|bingo|bing|bike|bid|bible|bharti|bet|bestbuy|best|berlin|bentley|beer|beauty|beats|bcn|bcg|bbva|bbt|bbc|bayern|bauhaus|basketball|baseball|bargains|barefoot|barclays|barclaycard|barcelona|bar|bank|band|bananarepublic|banamex|baidu|baby|azure|axa|aws|avianca|autos|auto|author|auspost|audio|audible|audi|auction|attorney|athleta|associates|asia|asda|arte|art|arpa|army|archi|aramco|arab|aquarelle|apple|app|apartments|aol|anz|anquan|android|analytics|amsterdam|amica|amfam|amex|americanfamily|americanexpress|amazon|alstom|alsace|ally|allstate|allfinanz|alipay|alibaba|alfaromeo|akdn|airtel|airforce|airbus|aigo|aig|agency|agakhan|africa|afl|afamilycompany|aetna|aero|aeg|adult|ads|adac|actor|active|aco|accountants|accountant|accenture|academy|abudhabi|abogado|able|abc|abbvie|abbott|abb|abarth|aarp|aaa|onion)\b([-a-zA-Z0-9@:%_\+.~#?&/=]*)/gi;
2const hashtagRegex = /(#|#)([a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*[a-z_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f][a-z0-9_\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u00ff\u0100-\u024f\u0253-\u0254\u0256-\u0257\u0300-\u036f\u1e00-\u1eff\u0400-\u04ff\u0500-\u0527\u2de0-\u2dff\ua640-\ua69f\u0591-\u05bf\u05c1-\u05c2\u05c4-\u05c5\u05d0-\u05ea\u05f0-\u05f4\ufb12-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb40-\ufb41\ufb43-\ufb44\ufb46-\ufb4f\u0610-\u061a\u0620-\u065f\u066e-\u06d3\u06d5-\u06dc\u06de-\u06e8\u06ea-\u06ef\u06fa-\u06fc\u0750-\u077f\u08a2-\u08ac\u08e4-\u08fe\ufb50-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\u200c-\u200c\u0e01-\u0e3a\u0e40-\u0e4e\u1100-\u11ff\u3130-\u3185\ua960-\ua97f\uac00-\ud7af\ud7b0-\ud7ff\uffa1-\uffdc\u30a1-\u30fa\u30fc-\u30fe\uff66-\uff9f\uff10-\uff19\uff21-\uff3a\uff41-\uff5a\u3041-\u3096\u3099-\u309e\u3400-\u4dbf\u4e00-\u9fff\u20000-\u2a6df\u2a700-\u2b73f\u2b740-\u2b81f\u2f800-\u2fa1f]*)/gi;
3const rtlLanguages = ['ar', 'arc', 'dv', 'fa', 'ha', 'he', 'khw', 'ks', 'ku', 'ps', 'ur', 'yi'];
4
5function arrayBufferToBase64(buffer) {
6 let binary = '';
7 let bytes = new Uint8Array(buffer);
8 let len = bytes.byteLength;
9 for (let i = 0; i < len; i++) {
10 binary += String.fromCharCode(bytes[i]);
11 }
12 return window.btoa(binary);
13}
14function sleep(ms) {
15 return new Promise(resolve => setTimeout(resolve, ms));
16}
17function createModal(html, className, onclose, canclose) {
18 let modal = document.createElement('div');
19 modal.classList.add('modal');
20 let modal_content = document.createElement('div');
21 modal_content.classList.add('modal-content');
22 if(className) modal_content.classList.add(className);
23 modal_content.innerHTML = html;
24 modal.appendChild(modal_content);
25 let close = document.createElement('span');
26 close.classList.add('modal-close');
27 close.title = "ESC";
28 close.innerHTML = '×';
29 document.body.style.overflowY = 'hidden';
30 function removeModal() {
31 modal.remove();
32 let event = new Event('findActiveTweet');
33 document.dispatchEvent(event);
34 document.removeEventListener('keydown', escapeEvent);
35 if(onclose) onclose();
36 let modals = document.getElementsByClassName('modal');
37 if(modals.length === 0) {
38 document.body.style.overflowY = 'auto';
39 }
40 }
41 modal.removeModal = removeModal;
42 function escapeEvent(e) {
43 if(document.querySelector('.viewer-in')) return;
44 if(e.key === 'Escape' || (e.altKey && e.keyCode === 78)) {
45 removeModal();
46 }
47 }
48 close.addEventListener('click', removeModal);
49 modal.addEventListener('click', e => {
50 if(e.target === modal) {
51 if(!canclose || canclose()) removeModal();
52 }
53 });
54 document.addEventListener('keydown', escapeEvent);
55 modal_content.appendChild(close);
56 document.body.appendChild(modal);
57 return modal;
58}
59async function handleFiles(files, mediaArray, mediaContainer) {
60 let images = [];
61 let videos = [];
62 let gifs = [];
63 for (let i = 0; i < files.length; i++) {
64 let file = files[i];
65 if (file.type.includes('gif')) {
66 // max 15 mb
67 if (file.size > 15000000) {
68 return alert(LOC.gifs_max.message);
69 }
70 gifs.push(file);
71 } else if (file.type.includes('video')) {
72 // max 500 mb
73 if (file.size > 500000000) {
74 return alert(LOC.videos_max.message);
75 }
76 videos.push(file);
77 } else if (file.type.includes('image')) {
78 // max 5 mb
79 if (file.size > 5000000) {
80 // convert png to jpeg
81 await new Promise(resolve => {
82 let canvas = document.createElement('canvas');
83 let ctx = canvas.getContext('2d');
84 let img = new Image();
85 img.onload = function () {
86 canvas.width = img.width;
87 canvas.height = img.height;
88 ctx.drawImage(img, 0, 0);
89 let dataURL = canvas.toDataURL('image/jpeg', 0.9);
90 let blobBin = atob(dataURL.split(',')[1]);
91 let array = [];
92 for (let i = 0; i < blobBin.length; i++) {
93 array.push(blobBin.charCodeAt(i));
94 }
95 file = new Blob([new Uint8Array(array)], { type: 'image/jpeg' });
96 resolve();
97 };
98 img.src = URL.createObjectURL(file);
99 });
100 if(file.size > 5000000) {
101 return alert(LOC.images_max.message);
102 }
103 }
104 images.push(file);
105 }
106 }
107 // either up to 4 images or 1 video or 1 gif
108 if (images.length > 0) {
109 if (images.length > 4) {
110 images = images.slice(0, 4);
111 }
112 if (videos.length > 0 || gifs.length > 0) {
113 return alert(LOC.max_count.message);
114 }
115 }
116 if (videos.length > 0) {
117 if (images.length > 0 || gifs.length > 0 || videos.length > 1) {
118 return alert(LOC.max_count.message);
119 }
120 }
121 if (gifs.length > 0) {
122 if (images.length > 0 || videos.length > 0 || gifs.length > 1) {
123 return alert(LOC.max_count.message);
124 }
125 }
126 // get base64 data
127 let media = [...images, ...videos, ...gifs];
128 let base64Data = [];
129 for (let i = 0; i < media.length; i++) {
130 let file = media[i];
131 let reader = new FileReader();
132 reader.readAsArrayBuffer(file);
133 reader.onload = () => {
134 base64Data.push(reader.result);
135 if (base64Data.length === media.length) {
136 while (mediaArray.length >= 4) {
137 mediaArray.pop();
138 mediaContainer.lastChild.remove();
139 }
140 base64Data.forEach(data => {
141 let div = document.createElement('div');
142 let img = document.createElement('img');
143 div.title = file.name;
144 div.id = `new-tweet-media-img-${Date.now()}${Math.random()}`.replace('.', '-');
145 div.className = "new-tweet-media-img-div";
146 img.className = "new-tweet-media-img";
147 let progress = document.createElement('span');
148 progress.hidden = true;
149 progress.className = "new-tweet-media-img-progress";
150 let remove = document.createElement('span');
151 remove.className = "new-tweet-media-img-remove";
152 let alt;
153 if (!file.type.includes('video')) {
154 alt = document.createElement('span');
155 alt.className = "new-tweet-media-img-alt";
156 alt.innerText = "ALT";
157 alt.addEventListener('click', () => {
158 mediaObject.alt = prompt(LOC.alt_text.message, mediaObject.alt || '');
159 });
160 }
161 let cw = document.createElement('span');
162 cw.className = "new-tweet-media-img-cw";
163 cw.innerText = "CW";
164 cw.addEventListener('click', () => {
165 createModal(`
166 <div class="cw-modal" style="color:var(--almost-black)">
167 <h2 class="nice-header">${LOC.content_warnings.message}</h2>
168 <br>
169 <input type="checkbox" id="cw-modal-graphic_violence"${mediaObject.cw.includes('graphic_violence') ? ' checked' : ''}> <label for="cw-modal-graphic_violence">${LOC.graphic_violence.message}</label><br>
170 <input type="checkbox" id="cw-modal-adult_content"${mediaObject.cw.includes('adult_content') ? ' checked' : ''}> <label for="cw-modal-adult_content">${LOC.adult_content.message}</label><br>
171 <input type="checkbox" id="cw-modal-other"${mediaObject.cw.includes('other') ? ' checked' : ''}> <label for="cw-modal-other">${LOC.sensitive_content.message}</label><br>
172 </div>
173 `);
174 let graphic_violence = document.getElementById('cw-modal-graphic_violence');
175 let adult_content = document.getElementById('cw-modal-adult_content');
176 let sensitive_content = document.getElementById('cw-modal-other');
177 [graphic_violence, adult_content, sensitive_content].forEach(checkbox => {
178 checkbox.addEventListener('change', () => {
179 if (checkbox.checked) {
180 mediaObject.cw.push(checkbox.id.slice(9));
181 } else {
182 let index = mediaObject.cw.indexOf(checkbox.id.slice(9));
183 if (index > -1) {
184 mediaObject.cw.splice(index, 1);
185 }
186 }
187 });
188 });
189 });
190
191 let dataBase64 = arrayBufferToBase64(data);
192 let mediaObject = {
193 div, img,
194 id: img.id,
195 data: data,
196 dataBase64: dataBase64,
197 type: file.type,
198 cw: [],
199 category: file.type.includes('gif') ? 'tweet_gif' : file.type.includes('video') ? 'tweet_video' : 'tweet_image'
200 };
201 mediaArray.push(mediaObject);
202 img.src = file.type.includes('video') ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAWUSURBVHhe7Z1pqG5THMbPNV1jul1TJEOZuqYMRZEpoRARvlw+uIjwASlRFIkMHwzJ8AVfZMhYOGRKESlDkciQyJhknj3PXu9b3nP2sPba9x3Wfp5f/dpr77p1zl7Ped+11l77f5fMz8/PGV3WGByNKA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOG3eC1gGl4ammXF+h9+HZj0xAdgC3gwPhw5AHjAAL8Kz4Re8UEVTANaCT8HDijOTGy9B9t1fxVkJTWOAneAhoWky5ADIPqykKQCbQA8U84V9xz6spKlzlwyOJl9q+9B/3eI4AOI0zQIOhs+H5iJeh3fBP4qzcjaDF8DNizPTls/gDfCH4qycDeBZcLfibDEcxL8QmotJDQA7fVf4QXFWz8nwvtA0LTkJPhCatewM34LrFGej1AYg9SvgF/hNaDby8eBo2vPp4NjEl5B90hqPAcRxAMRxAMRxAMRxAMRxAMRJDcCaA2NYe3A07Ym9d236Y4TUAGwET4VlCw//Z124MjRNAmfADUOzEnb8iZB90pouS8H/QC5A1C0FMwDcUWTS4YLbz6FZCgOwFaz6Yx7LUrDJh7EsBZue0KcA/Av/Dk0TS18CwIcm/KjbEV4Nf4Qmgr4E4ErIbdAfwUvhXvB+WLkb1gS6BICzAG5Y+KTG2EfGXVn42PRDeAo8AnLjSs5wplV2b4dy3z/7IokuATgHbtfg9vBuOA04JngOHgjPhJ/D3Lgdlt3XhV4Ek0gNAL9jH4RNg66f4J2hOTX4lgx/hj3gdbBuTj1r3At/C81KuA5zD0wa96QGgB0fO+L+c3CcNt/Bi+G+8BGYw4wh9t616Y8R+jIIbMN78AR4NHyTF5RRDADhoInvPO4Pz4NfQUlUAzCE36+3wN0h34D+FUqhHoAhX8Pz4X7wSZg8rcoNB2CUt+Ex8Hj4Li/0HQdgMRxNPwY5W+D8+lvYW1IDsD6Mfc6/zeCYG3zRgq9lcf3gDsj1hEnDRZ4YNoXsk9Z02Q/wDuRKVd3CysbwQrh1cTY+WL7m2dAcG/vAa+ChcFKvzXN2ciPkGKUK7spaBfmJVYbEhpBJBICwZA7HB1dBPnnMAW8IWY3w6SJf1twb3soLueMApMFnHJfBqFJss4wDkE4vyuc4AGlwqzafLLJ4ZtY4AO0Y7sF/A57OC7nTZRYwSyViJjEL4MDvWjjJaaBLxEQyzgBsCS+Hp8FJl8p1iZgpwpU1LmLxxnJL2TTqJLtEzBTg9/yx8DV4PayttJk7DsAo3BfwOHwYruCFvuMABDhYvQm+Co+CMvdFPQB8e/lcyH0A3Bq2HpRCNQD8vY+Er0BuBZOtZKoYgF3gQ/AJuCcvKJMaAI6UaQyzUiJmOeTyLRewjoOxP/80cYmY1QDn7yy1wvk8t3hx5SwXXCImkrKVQC7XchWMu3iqdsvkwFhLxHQZA/Dfcpl02xonVR9o4d65HSCXn5+GOXc+4X6/sns7lNvtkvuxSwBmiSsgV+/4QIQFIvi0juvo3MJlauhLAPhJ9CjkfP4SmPR9qEhfAmAScQDE6RKAWSoR02dcIkYYl4gRxyVixHGJGDNeHABxHABxHABxHABxHABxUgOgUCJmFuAiTwzyJWL6ikvEmM6MbUeQ6QEOgDhNAeB/umDyprYPmwLAKpkydXN7CPuuttJpUwDehy+HpskQDuDZh5U0zQIIN1zeBg+C0yiSYNrDsrbPQL7wyh1FlcQEYAgrYjkAecAARNUwbBMA00M8DRTHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHAZBmbu4/x6swK3hIFr4AAAAASUVORK5CYII=' : `data:${file.type};base64,${dataBase64}`;
203 remove.addEventListener('click', () => {
204 div.remove();
205 for (let i = mediaArray.length - 1; i >= 0; i--) {
206 let m = mediaArray[i];
207 if (m.id === img.id) mediaArray.splice(i, 1);
208 }
209 });
210 div.append(img, progress, remove);
211 if (!file.type.includes('video')) {
212 img.addEventListener('click', () => {
213 if(!img.src.endsWith('?name=orig') && !img.src.startsWith('data:')) {
214 img.src += '?name=orig';
215 }
216 new Viewer(mediaContainer, {
217 transition: false
218 });
219 });
220 div.append(alt);
221 } else {
222 cw.style.marginLeft = '-53px';
223 }
224 div.append(cw);
225 mediaContainer.append(div);
226 });
227 }
228 }
229 }
230}
231let isURL = (str) => {
232 try {
233 new URL(str);
234 return true;
235 } catch (_) {
236 return false;
237 }
238}
239function handleDrop(event, mediaArray, mediaContainer) {
240 let text = event.dataTransfer.getData("Text").trim();
241 if(text.length <= 1) {
242 event.stopPropagation();
243 event.preventDefault();
244 let files = event.dataTransfer.files;
245 handleFiles(files, mediaArray, mediaContainer);
246 }
247}
248function getMedia(mediaArray, mediaContainer) {
249 let input = document.createElement('input');
250 input.type = 'file';
251 input.multiple = true;
252 input.accept = 'image/png,image/jpeg,image/gif,video/mp4,video/mov';
253 input.addEventListener('change', () => {
254 handleFiles(input.files, mediaArray, mediaContainer);
255 });
256 input.click();
257};
258function getDMMedia(mediaArray, mediaContainer, modalElement) {
259 let input = document.createElement('input');
260 input.type = 'file';
261 input.accept = 'image/*';
262 input.addEventListener('change', async () => {
263 let files = input.files;
264 let images = [];
265 let gifs = [];
266 for (let i = 0; i < files.length; i++) {
267 let file = files[i];
268 if (file.type.includes('gif')) {
269 // max 15 mb
270 if (file.size > 15000000) {
271 return alert(LOC.gifs_max.message);
272 }
273 gifs.push(file);
274 } else if (file.type.includes('image')) {
275 // max 5 mb
276 if (file.size > 5000000) {
277 return alert(LOC.images_max.message);
278 }
279 images.push(file);
280 }
281 }
282 // get base64 data
283 let media = [...images, ...gifs];
284 let base64Data = [];
285 for (let i = 0; i < media.length; i++) {
286 let file = media[i];
287 let reader = new FileReader();
288 reader.readAsArrayBuffer(file);
289 reader.onload = () => {
290 base64Data.push(reader.result);
291 if (base64Data.length === media.length) {
292 mediaContainer.innerHTML = '';
293 while (mediaArray.length > 0) {
294 mediaArray.pop();
295 }
296 base64Data.forEach(data => {
297 let div = document.createElement('div');
298 let img = document.createElement('img');
299 div.title = file.name;
300 div.id = `new-tweet-media-img-${Date.now()}${Math.random()}`.replace('.', '-');
301 div.className = "new-tweet-media-img-div";
302 img.className = "new-tweet-media-img";
303 let progress = document.createElement('span');
304 progress.hidden = true;
305 progress.className = "new-tweet-media-img-progress";
306 let remove = document.createElement('span');
307 remove.className = "new-tweet-media-img-remove";
308 let dataBase64 = arrayBufferToBase64(data);
309 let mediaObject = {
310 div, img,
311 id: img.id,
312 data: data,
313 dataBase64: dataBase64,
314 type: file.type,
315 category: file.type.includes('gif') ? 'tweet_gif' : file.type.includes('video') ? 'tweet_video' : 'tweet_image'
316 };
317 mediaArray.push(mediaObject);
318 img.src = file.type.includes('video') ? 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAWUSURBVHhe7Z1pqG5THMbPNV1jul1TJEOZuqYMRZEpoRARvlw+uIjwASlRFIkMHwzJ8AVfZMhYOGRKESlDkciQyJhknj3PXu9b3nP2sPba9x3Wfp5f/dpr77p1zl7Ped+11l77f5fMz8/PGV3WGByNKA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOG3eC1gGl4ammXF+h9+HZj0xAdgC3gwPhw5AHjAAL8Kz4Re8UEVTANaCT8HDijOTGy9B9t1fxVkJTWOAneAhoWky5ADIPqykKQCbQA8U84V9xz6spKlzlwyOJl9q+9B/3eI4AOI0zQIOhs+H5iJeh3fBP4qzcjaDF8DNizPTls/gDfCH4qycDeBZcLfibDEcxL8QmotJDQA7fVf4QXFWz8nwvtA0LTkJPhCatewM34LrFGej1AYg9SvgF/hNaDby8eBo2vPp4NjEl5B90hqPAcRxAMRxAMRxAMRxAMRxAMRJDcCaA2NYe3A07Ym9d236Y4TUAGwET4VlCw//Z124MjRNAmfADUOzEnb8iZB90pouS8H/QC5A1C0FMwDcUWTS4YLbz6FZCgOwFaz6Yx7LUrDJh7EsBZue0KcA/Av/Dk0TS18CwIcm/KjbEV4Nf4Qmgr4E4ErIbdAfwUvhXvB+WLkb1gS6BICzAG5Y+KTG2EfGXVn42PRDeAo8AnLjSs5wplV2b4dy3z/7IokuATgHbtfg9vBuOA04JngOHgjPhJ/D3Lgdlt3XhV4Ek0gNAL9jH4RNg66f4J2hOTX4lgx/hj3gdbBuTj1r3At/C81KuA5zD0wa96QGgB0fO+L+c3CcNt/Bi+G+8BGYw4wh9t616Y8R+jIIbMN78AR4NHyTF5RRDADhoInvPO4Pz4NfQUlUAzCE36+3wN0h34D+FUqhHoAhX8Pz4X7wSZg8rcoNB2CUt+Ex8Hj4Li/0HQdgMRxNPwY5W+D8+lvYW1IDsD6Mfc6/zeCYG3zRgq9lcf3gDsj1hEnDRZ4YNoXsk9Z02Q/wDuRKVd3CysbwQrh1cTY+WL7m2dAcG/vAa+ChcFKvzXN2ciPkGKUK7spaBfmJVYbEhpBJBICwZA7HB1dBPnnMAW8IWY3w6SJf1twb3soLueMApMFnHJfBqFJss4wDkE4vyuc4AGlwqzafLLJ4ZtY4AO0Y7sF/A57OC7nTZRYwSyViJjEL4MDvWjjJaaBLxEQyzgBsCS+Hp8FJl8p1iZgpwpU1LmLxxnJL2TTqJLtEzBTg9/yx8DV4PayttJk7DsAo3BfwOHwYruCFvuMABDhYvQm+Co+CMvdFPQB8e/lcyH0A3Bq2HpRCNQD8vY+Er0BuBZOtZKoYgF3gQ/AJuCcvKJMaAI6UaQyzUiJmOeTyLRewjoOxP/80cYmY1QDn7yy1wvk8t3hx5SwXXCImkrKVQC7XchWMu3iqdsvkwFhLxHQZA/Dfcpl02xonVR9o4d65HSCXn5+GOXc+4X6/sns7lNvtkvuxSwBmiSsgV+/4QIQFIvi0juvo3MJlauhLAPhJ9CjkfP4SmPR9qEhfAmAScQDE6RKAWSoR02dcIkYYl4gRxyVixHGJGDNeHABxHABxHABxHABxHABxUgOgUCJmFuAiTwzyJWL6ikvEmM6MbUeQ6QEOgDhNAeB/umDyprYPmwLAKpkydXN7CPuuttJpUwDehy+HpskQDuDZh5U0zQIIN1zeBg+C0yiSYNrDsrbPQL7wyh1FlcQEYAgrYjkAecAARNUwbBMA00M8DRTHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHAZBmbu4/x6swK3hIFr4AAAAASUVORK5CYII=' : `data:${file.type};base64,${dataBase64}`;
319 remove.addEventListener('click', () => {
320 div.remove();
321 for (let i = mediaArray.length - 1; i >= 0; i--) {
322 let m = mediaArray[i];
323 if (m.id === img.id) mediaArray.splice(i, 1);
324 }
325 });
326 div.append(img, progress, remove);
327 if (!file.type.includes('video')) {
328 img.addEventListener('click', () => {
329 if(!img.src.endsWith('?name=orig') && !img.src.startsWith('data:')) {
330 img.src += '?name=orig';
331 }
332 new Viewer(mediaContainer, {
333 transition: false
334 });
335 });
336 }
337 mediaContainer.append(div);
338 setTimeout(() => modalElement.scrollTop = modalElement.scrollHeight, 50);
339 });
340 }
341 }
342 }
343 });
344 input.click();
345};
346function timeElapsed(targetTimestamp) {
347 let currentDate = new Date();
348 let currentTimeInms = currentDate.getTime();
349 let targetDate = new Date(targetTimestamp);
350 let targetTimeInms = targetDate.getTime();
351 let elapsed = Math.floor((currentTimeInms - targetTimeInms) / 1000);
352 const MonthNames = [
353 LOC.january.message,
354 LOC.february.message,
355 LOC.march.message,
356 LOC.april.message,
357 LOC.may.message,
358 LOC.june.message,
359 LOC.july.message,
360 LOC.august.message,
361 LOC.september.message,
362 LOC.october.message,
363 LOC.november.message,
364 LOC.december.message
365 ];
366 if (elapsed < 1) {
367 return LOC.s.message.replace('$NUMBER$', 0);
368 }
369 if (elapsed < 60) { //< 60 sec
370 return LOC.s.message.replace('$NUMBER$', elapsed);
371 }
372 if (elapsed < 3600) { //< 60 minutes
373 return LOC.m.message.replace('$NUMBER$', Math.floor(elapsed / (60)));
374 }
375 if (elapsed < 86400) { //< 24 hours
376 return LOC.h.message.replace('$NUMBER$', Math.floor(elapsed / (3600)));
377 }
378 if (elapsed < 604800) { //<7 days
379 return LOC.d.message.replace('$NUMBER$', Math.floor(elapsed / (86400)));
380 }
381 if (elapsed < 2628000) { //<1 month
382 return MonthNames[targetDate.getMonth()].replace('$NUMBER$', targetDate.getDate());
383 }
384 return `${MonthNames[targetDate.getMonth()].replace('$NUMBER$', targetDate.getDate())}, ${targetDate.getFullYear()}`; //more than a monh
385}
386function openInNewTab(href) {
387 Object.assign(document.createElement('a'), {
388 target: '_blank',
389 rel: 'noopener noreferrer',
390 href: href,
391 }).click();
392}
393function onVisibilityChange(callback) {
394 var visible = true;
395
396 if (!callback) {
397 throw new Error('no callback given');
398 }
399
400 function focused() {
401 if (!visible) {
402 callback(visible = true);
403 }
404 }
405
406 function unfocused() {
407 if (visible) {
408 callback(visible = false);
409 }
410 }
411
412 // Standards:
413 if ('hidden' in document) {
414 visible = !document.hidden;
415 document.addEventListener('visibilitychange',
416 function () { (document.hidden ? unfocused : focused)() });
417 }
418 if ('mozHidden' in document) {
419 visible = !document.mozHidden;
420 document.addEventListener('mozvisibilitychange',
421 function () { (document.mozHidden ? unfocused : focused)() });
422 }
423 if ('webkitHidden' in document) {
424 visible = !document.webkitHidden;
425 document.addEventListener('webkitvisibilitychange',
426 function () { (document.webkitHidden ? unfocused : focused)() });
427 }
428 if ('msHidden' in document) {
429 visible = !document.msHidden;
430 document.addEventListener('msvisibilitychange',
431 function () { (document.msHidden ? unfocused : focused)() });
432 }
433 // IE 9 and lower:
434 if ('onfocusin' in document) {
435 document.onfocusin = focused;
436 document.onfocusout = unfocused;
437 }
438 // All others:
439 window.onpageshow = window.onfocus = focused;
440 window.onpagehide = window.onblur = unfocused;
441};
442function escapeHTML(unsafe) {
443 return unsafe
444 .replace(/</g, "<")
445 .replace(/>/g, ">")
446 .replace(/"/g, """)
447 .replace(/'/g, "’");
448}
449async function renderTweetBodyHTML(full_text, entities, display_text_range, is_quote_tweet=false) {
450 let result = "",
451 last_pos = 0,
452 index_map = {}; // {start_position: [end_position, replacer_func]}
453 hashflags = [];
454
455 if (vars.enableHashflags) {
456 hashflags = await API.discover.getHashflagsV2();
457 }
458
459 full_text_array = Array.from(full_text);
460
461 if (is_quote_tweet) { // for quoted tweet we need only hashflags and readable urls
462 if (entities.hashtags) {
463 entities.hashtags.forEach(hashtag => {
464 let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
465 index_map[hashtag.indices[0]] = [hashtag.indices[1], text =>
466 `#${escapeHTML(hashtag.text)}`+
467 `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`
468 ];
469 });
470 }
471
472 if (entities.urls) {
473 entities.urls.forEach(url => {
474 index_map[url.indices[0]] = [url.indices[1], text => `${escapeHTML(url.display_url)}`];
475 });
476 }
477 } else {
478 if (entities.hashtags) {
479 entities.hashtags.forEach(hashtag => {
480 let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
481 index_map[hashtag.indices[0]] = [hashtag.indices[1], text => `<a href="https://twitter.com/hashtag/${escapeHTML(hashtag.text)}">`+
482 `#${escapeHTML(hashtag.text)}`+
483 `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`+
484 `</a>`];
485 });
486 }
487
488 if (entities.symbols) {
489 entities.symbols.forEach(symbol => {
490 index_map[symbol.indices[0]] = [symbol.indices[1], text => `<a href="https://twitter.com/search?q=%24${escapeHTML(symbol.text)}">`+
491 `$${escapeHTML(symbol.text)}`+
492 `</a>`];
493 });
494 }
495
496 if (entities.urls) {
497 entities.urls.forEach(url => {
498 index_map[url.indices[0]] = [url.indices[1], text =>
499 `<a href="${escapeHTML(url.expanded_url)}" title="${escapeHTML(url.expanded_url)}" target="_blank" rel="noopener noreferrer">`+
500 `${escapeHTML(url.display_url)}</a>`];
501 });
502 }
503
504 if (entities.user_mentions) {
505 entities.user_mentions.forEach(user => {
506 index_map[user.indices[0]] = [user.indices[1], text => `<a href="https://twitter.com/${escapeHTML(user.screen_name)}">${escapeHTML(text)}</a>`];
507 });
508 }
509 }
510
511 let display_start = display_text_range !== undefined ? display_text_range[0] : 0;
512 let display_end = display_text_range !== undefined ? display_text_range[1] : full_text_array.length;
513 for (let [current_pos, _] of full_text_array.entries()) {
514 if (current_pos < display_start) { // do not render first part of message
515 last_pos = current_pos + 1; // to start copy from next symbol
516 continue;
517 }
518 if (current_pos == display_end || // reached the end of visible part
519 current_pos == full_text_array.length - 1) { // reached the end of tweet itself
520 if (display_end == full_text_array.length) current_pos++; // dirty hack to include last element of slice
521 result += escapeHTML(full_text_array.slice(last_pos, current_pos).join(''));
522 break;
523 }
524 if (current_pos > display_end) {
525 break; // do not render last part of message
526 }
527
528 if (current_pos in index_map) {
529 let [end, func] = index_map[current_pos];
530
531 if (current_pos > last_pos) {
532 result += escapeHTML(full_text_array.slice(last_pos, current_pos).join('')); // store chunk of untouched text
533 }
534 result += func(full_text_array.slice(current_pos, end).join('')); // run replacer func on corresponding range
535 last_pos = end;
536 }
537 }
538 return result
539}
540function arrayInsert(arr, index, value) {
541 return [...arr.slice(0, index), value, ...arr.slice(index)];
542}
543function generatePoll(tweet, tweetElement, user) {
544 let pollElement = tweetElement.getElementsByClassName('tweet-card')[0];
545 pollElement.innerHTML = '';
546 let poll = tweet.card.binding_values;
547 let choices = Object.keys(poll).filter(key => key.endsWith('label')).map((key, i) => ({
548 label: poll[key].string_value,
549 count: poll[key.replace('label', 'count')] ? +poll[key.replace('label', 'count')].string_value : 0,
550 id: i+1
551 }));
552 let voteCount = choices.reduce((acc, cur) => acc + cur.count, 0);
553 if(poll.selected_choice || user.id_str === tweet.user.id_str || (poll.counts_are_final && poll.counts_are_final.boolean_value)) {
554 for(let i in choices) {
555 let choice = choices[i];
556 if(user.id_str !== tweet.user.id_str && poll.selected_choice && choice.id === +poll.selected_choice.string_value) {
557 choice.selected = true;
558 }
559 choice.percentage = Math.round(choice.count / voteCount * 100);
560 let choiceElement = document.createElement('div');
561 choiceElement.classList.add('choice');
562 choiceElement.innerHTML = `
563 <div class="choice-bg" style="width:${choice.percentage}%" data-percentage="${choice.percentage}"></div>
564 <div class="choice-label">
565 <span>${escapeHTML(choice.label)}</span>
566 ${choice.selected ? `<span class="choice-selected"></span>` : ''}
567 </div>
568 ${isFinite(choice.percentage) ? `<div class="choice-count">${choice.count} (${choice.percentage}%)</div>` : '<div class="choice-count">0</div>'}
569 `;
570 pollElement.append(choiceElement);
571 }
572 } else {
573 for(let i in choices) {
574 let choice = choices[i];
575 let choiceElement = document.createElement('div');
576 choiceElement.classList.add('choice', 'choice-unselected');
577 choiceElement.innerHTML = `
578 <div class="choice-bg" style="width:100%"></div>
579 <div class="choice-label">${escapeHTML(choice.label)}</div>
580 `;
581 choiceElement.addEventListener('click', async () => {
582 let newCard = await API.tweet.vote(poll.api.string_value, tweet.id_str, tweet.card.url, tweet.card.name, choice.id);
583 tweet.card = newCard.card;
584 generateCard(tweet, tweetElement, user);
585 });
586 pollElement.append(choiceElement);
587 }
588 }
589 if(tweet.card.url.startsWith('card://')) {
590 let footer = document.createElement('span');
591 footer.classList.add('poll-footer');
592 let endsAtMessage;
593 if(LOC.ends_at.message.includes("$DATE$")) {
594 endsAtMessage = LOC.ends_at.message.replace('$DATE$', new Date(poll.end_datetime_utc.string_value).toLocaleString());
595 } else {
596 endsAtMessage = `${LOC.ends_at.message} ${new Date(poll.end_datetime_utc.string_value).toLocaleString()}`;
597 }
598 footer.innerHTML = `${voteCount} ${voteCount === 1 ? LOC.vote.message : LOC.votes.message}${(!poll.counts_are_final || !poll.counts_are_final.boolean_value) && poll.end_datetime_utc ? ` ・ ${endsAtMessage}` : ''}`;
599 pollElement.append(footer);
600 }
601}
602function generateCard(tweet, tweetElement, user) {
603 if(!tweet.card) return;
604 if(tweet.card.name === 'promo_image_convo' || tweet.card.name === 'promo_video_convo') {
605 let vals = tweet.card.binding_values;
606 let a = document.createElement('a');
607 a.href = vals.thank_you_url ? vals.thank_you_url.string_value : "#";
608 a.target = '_blank';
609 a.title = vals.thank_you_text.string_value;
610 let img = document.createElement('img');
611 let imgValue = vals.promo_image;
612 if(!imgValue) {
613 imgValue = vals.cover_promo_image_original;
614 }
615 if(!imgValue) {
616 imgValue = vals.cover_promo_image_large;
617 }
618 if(!imgValue) {
619 return;
620 }
621 img.src = imgValue.image_value.url;
622 img.width = sizeFunctions[1](imgValue.image_value.width, imgValue.image_value.height)[0];
623 img.height = sizeFunctions[1](imgValue.image_value.width, imgValue.image_value.height)[1];
624 img.className = 'tweet-media-element';
625 let ctas = [];
626 if(vals.cta_one) {
627 ctas.push([vals.cta_one, vals.cta_one_tweet]);
628 }
629 if(vals.cta_two) {
630 ctas.push([vals.cta_two, vals.cta_two_tweet]);
631 }
632 if(vals.cta_three) {
633 ctas.push([vals.cta_three, vals.cta_three_tweet]);
634 }
635 if(vals.cta_four) {
636 ctas.push([vals.cta_four, vals.cta_four_tweet]);
637 }
638 let buttonGroup = document.createElement('div');
639 buttonGroup.classList.add('tweet-button-group');
640 for(let b of ctas) {
641 let button = document.createElement('button');
642 button.className = `nice-button tweet-app-button`;
643 button.innerText = `${LOC.tweet_verb.message} ${b[0].string_value}`;
644 button.addEventListener('click', async () => {
645 let modal = createModal(`
646 <p style="color:var(--almost-black);margin-top:0">${LOC.do_you_want_to_tweet.message.replace("$TWEET_TEXT$", b[1].string_value)}</p>
647 <button class="nice-button">${LOC.tweet_verb.message}</button>
648 `);
649 modal.getElementsByClassName('nice-button')[0].addEventListener('click', async () => {
650 modal.removeModal();
651 try {
652 await API.tweet.postV2({
653 "text": b[1].string_value,
654 "card_uri": tweet.card.url,
655 });
656 } catch(e) {
657 console.error(e);
658 alert(String(e));
659 }
660 });
661 });
662 buttonGroup.append(button);
663 }
664 a.append(img);
665 tweetElement.getElementsByClassName('tweet-card')[0].append(a);
666 tweetElement.getElementsByClassName('tweet-card')[0].append(buttonGroup);
667 } else if(tweet.card.name === "player") {
668 let iframe = document.createElement('iframe');
669 iframe.src = tweet.card.binding_values.player_url.string_value.replace("autoplay=true", "autoplay=false");
670 iframe.classList.add('tweet-player');
671 iframe.width = 450;
672 iframe.height = 250;
673 tweetElement.getElementsByClassName('tweet-card')[0].innerHTML = '';
674 tweetElement.getElementsByClassName('tweet-card')[0].append(iframe);
675 } else if(tweet.card.name === "unified_card") {
676 let uc = JSON.parse(tweet.card.binding_values.unified_card.string_value);
677 for(let cn of uc.components) {
678 let co = uc.component_objects[cn];
679 if(co.type === "media") {
680 let media = uc.media_entities[co.data.id];
681 let video = document.createElement('video');
682 video.className = 'tweet-media-element tweet-media-element-one';
683 let [w, h] = sizeFunctions[1](media.original_info.width, media.original_info.height);
684 video.width = w;
685 video.height = h;
686 video.crossOrigin = 'anonymous';
687 video.loading = 'lazy';
688 video.controls = true;
689 if(!media.video_info) {
690 console.log(`bug found in ${tweet.id_str}, please report this message to https://github.com/dimdenGD/OldTwitter/issues`, tweet);
691 continue;
692 };
693 let variants = media.video_info.variants.sort((a, b) => {
694 if(!b.bitrate) return -1;
695 return b.bitrate-a.bitrate;
696 });
697 if(typeof(vars.savePreferredQuality) !== 'boolean') {
698 chrome.storage.sync.set({
699 savePreferredQuality: true
700 }, () => {});
701 vars.savePreferredQuality = true;
702 }
703 if(localStorage.preferredQuality && vars.savePreferredQuality) {
704 let closestQuality = variants.filter(v => v.bitrate).reduce((prev, curr) => {
705 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);
706 });
707 let preferredQualityVariantIndex = variants.findIndex(v => v.url === closestQuality.url);
708 if(preferredQualityVariantIndex !== -1) {
709 let preferredQualityVariant = variants[preferredQualityVariantIndex];
710 variants.splice(preferredQualityVariantIndex, 1);
711 variants.unshift(preferredQualityVariant);
712 }
713 }
714 for(let v in variants) {
715 let source = document.createElement('source');
716 source.src = variants[v].url;
717 source.type = variants[v].content_type;
718 video.append(source);
719 }
720 tweetElement.getElementsByClassName('tweet-card')[0].append(video, document.createElement('br'));
721 } else if(co.type === "app_store_details") {
722 let app = uc.app_store_data[uc.destination_objects[co.data.destination].data.app_id][0];
723 let appElement = document.createElement('div');
724 appElement.classList.add('tweet-app-info');
725 appElement.innerHTML = `
726 <h3>${escapeHTML(app.title.content)}</h3>
727 <span>${escapeHTML(app.category.content)}</span>
728 <br><br>
729 `;
730 tweetElement.getElementsByClassName('tweet-card')[0].append(appElement);
731 } else if(co.type === "button_group") {
732 let buttonGroup = document.createElement('div');
733 buttonGroup.classList.add('tweet-button-group');
734 for(let b of co.data.buttons) {
735 let app = uc.app_store_data[uc.destination_objects[b.destination].data.app_id][0];
736 let button = document.createElement('a');
737 button.href = `http://play.google.com/store/apps/details?id=${app.id}`;
738 button.target = '_blank';
739 button.className = `nice-button tweet-app-button tweet-app-button-${b.style}`
740 button.innerText = b.action[0].toUpperCase() + b.action.slice(1);
741 buttonGroup.append(button);
742 }
743 tweetElement.getElementsByClassName('tweet-card')[0].append(buttonGroup);
744 }
745 }
746 } else if(tweet.card.name === "summary" || tweet.card.name === "summary_large_image") {
747 let vals = tweet.card.binding_values;
748 let a = document.createElement('a');
749 let url = vals.card_url.string_value;
750 if(tweet.entities && tweet.entities.urls) {
751 let urlEntity = tweet.entities.urls.find(u => u.url === url);
752 if(urlEntity) {
753 url = urlEntity.expanded_url;
754 }
755 }
756 a.target = '_blank';
757 a.href = url;
758 a.className = 'tweet-card-link box';
759 a.innerHTML = `
760 ${vals.thumbnail_image ? `<img src="${vals.thumbnail_image.image_value.url}" class="tweet-card-link-thumbnail">` : ''}
761 <div class="tweet-card-link-text">
762 ${vals.vanity_url ? `<span class="tweet-card-link-vanity">${escapeHTML(vals.vanity_url.string_value)}</span><br>` : ''}
763 ${vals.title ? `<h3 class="tweet-card-link-title">${escapeHTML(vals.title.string_value)}</h3>` : ''}
764 ${vals.description ? `<span class="tweet-card-link-description">${escapeHTML(vals.description.string_value)}</span>` : ''}
765 </div>
766 `;
767 tweetElement.getElementsByClassName('tweet-card')[0].append(a);
768 } else if(tweet.card.url.startsWith('card://')) {
769 generatePoll(tweet, tweetElement, user);
770 }
771}
772function createEmojiPicker(container, input, style = {}) {
773 let picker = new EmojiPicker({
774 i18n: {
775 "categories": {
776 "custom": LOC.custom.message,
777 "smileys-emotion": LOC.smileys_emotion.message,
778 "people-body": LOC.people_body.message,
779 "animals-nature": LOC.animals_nature.message,
780 "food-drink": LOC.food_drink.message,
781 "travel-places": LOC.travel_places.message,
782 "activities": LOC.activities.message,
783 "objects": LOC.objects.message,
784 "symbols": LOC.symbols.message,
785 "flags": LOC.flags.message
786 },
787 "categoriesLabel": LOC.categories.message,
788 "emojiUnsupportedMessage": LOC.unsupported_emoji.message,
789 "favoritesLabel": LOC.favorites.message,
790 "loadingMessage": LOC.loading.message,
791 "networkErrorMessage": LOC.cant_load_emoji.message,
792 "regionLabel": LOC.emoji_picker.message,
793 "searchDescription": LOC.emoji_search_description.message,
794 "searchLabel": LOC.search.message,
795 "searchResultsLabel": LOC.search_results.message,
796 "skinToneDescription": "When expanded, press up or down to select and enter to choose.",
797 "skinToneLabel": LOC.skin_tone_label.message.replace("$SKIN_TONE$", "{skinTone}"),
798 "skinTones": [
799 "Default",
800 "Light",
801 "Medium-Light",
802 "Medium",
803 "Medium-Dark",
804 "Dark"
805 ],
806 "skinTonesLabel": LOC.skin_tones_label.message
807 }
808 });
809 for(let i in style) {
810 picker.style[i] = style[i];
811 }
812 picker.className = isDarkModeEnabled ? 'dark' : 'light';
813 picker.addEventListener('emoji-click', e => {
814 let pos = input.selectionStart;
815 let text = input.value;
816 input.value = text.slice(0, pos) + e.detail.unicode + text.slice(pos);
817 input.selectionStart = pos + e.detail.unicode.length;
818 });
819 container.append(picker);
820
821 let observer;
822 if(vars.enableTwemoji) {
823 const style = document.createElement('style');
824 style.textContent = `.twemoji {
825 width: var(--emoji-size);
826 height: var(--emoji-size);
827 pointer-events: none;
828 }`;
829 picker.shadowRoot.appendChild(style);
830
831 observer = new MutationObserver(() => {
832 for (const emoji of picker.shadowRoot.querySelectorAll('.emoji')) {
833 // Avoid infinite loops of MutationObserver
834 if (!emoji.querySelector('.twemoji')) {
835 // Do not use default 'emoji' class name because it conflicts with emoji-picker-element's
836 twemoji.parse(emoji, { className: 'twemoji' })
837 }
838 }
839 })
840 observer.observe(picker.shadowRoot, {
841 subtree: true,
842 childList: true
843 });
844 }
845
846 setTimeout(() => {
847 function oc (e) {
848 if (picker.contains(e.target)) return;
849 if(observer) {
850 observer.disconnect();
851 }
852 picker.remove();
853 document.removeEventListener('click', oc);
854 picker.database.close();
855 }
856 document.addEventListener('click', oc);
857 }, 100);
858
859 return picker;
860}
861
862const RED = 0.2126;
863const GREEN = 0.7152;
864const BLUE = 0.0722;
865
866const GAMMA = 2.4;
867
868function luminance(r, g, b) {
869 const a = [r, g, b].map(v => {
870 v /= 255;
871 return v <= 0.03928
872 ? v / 12.92
873 : Math.pow((v + 0.055) / 1.055, GAMMA);
874 });
875 return a[0] * RED + a[1] * GREEN + a[2] * BLUE;
876}
877
878function contrast(rgb1, rgb2) {
879 const lum1 = luminance(...rgb1);
880 const lum2 = luminance(...rgb2);
881 const brightest = Math.max(lum1, lum2);
882 const darkest = Math.min(lum1, lum2);
883 return (brightest + 0.05) / (darkest + 0.05);
884}
885function hex2rgb(hex) {
886 if(!hex.startsWith('#')) hex = `#${hex}`;
887 const r = parseInt(hex.slice(1, 3), 16)
888 const g = parseInt(hex.slice(3, 5), 16)
889 const b = parseInt(hex.slice(5, 7), 16)
890 return [r, g, b];
891}
892function rgb2hex(r, g, b) {
893 return `#${r.toString(16).padStart(2, '0')}${g.toString(16).padStart(2, '0')}${b.toString(16).padStart(2, '0')}`;
894}
895function rgbToHsl(r, g, b) {
896 r /= 255; g /= 255; b /= 255;
897 let max = Math.max(r, g, b);
898 let min = Math.min(r, g, b);
899 let d = max - min;
900 let h;
901 if (d === 0) h = 0;
902 else if (max === r) h = ((((g - b) / d) % 6)+6)%6;
903 else if (max === g) h = (b - r) / d + 2;
904 else if (max === b) h = (r - g) / d + 4;
905 let l = (min + max) / 2;
906 let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
907 return [h * 60, s, l];
908}
909
910function hslToRgb(h, s, l) {
911 let c = (1 - Math.abs(2 * l - 1)) * s;
912 let hp = h / 60.0;
913 let x = c * (1 - Math.abs((hp % 2) - 1));
914 let rgb1;
915 if (isNaN(h)) rgb1 = [0, 0, 0];
916 else if (hp <= 1) rgb1 = [c, x, 0];
917 else if (hp <= 2) rgb1 = [x, c, 0];
918 else if (hp <= 3) rgb1 = [0, c, x];
919 else if (hp <= 4) rgb1 = [0, x, c];
920 else if (hp <= 5) rgb1 = [x, 0, c];
921 else if (hp <= 6) rgb1 = [c, 0, x];
922 let m = l - c * 0.5;
923 return [
924 Math.round(255 * (rgb1[0] + m)),
925 Math.round(255 * (rgb1[1] + m)),
926 Math.round(255 * (rgb1[2] + m))
927 ];
928}
929function getBackgroundColor() {
930 let root = document.documentElement;
931 let bg_color = getComputedStyle(root).getPropertyValue('--background-color');
932 if(bg_color === 'white') {
933 bg_color = '#ffffff';
934 } else if(bg_color === 'black') {
935 bg_color = '#000000';
936 } else if(bg_color.startsWith('rgb(')) {
937 let rgb = bg_color.slice(4, -1).split(',').map(v => parseInt(v));
938 bg_color = rgb2hex(...rgb);
939 }
940 if(!bg_color) bg_color = '#ffffff';
941 return bg_color;
942}
943function makeSeeableColor(color, bg_color = getBackgroundColor()) {
944 let bg_rgb = hex2rgb(bg_color);
945 let rgb = hex2rgb(color);
946 let c = contrast(bg_rgb, rgb);
947 let hsl = rgbToHsl(...rgb);
948 let bg_hsl = rgbToHsl(...bg_rgb);
949 if(c < 4.5) {
950 if(bg_hsl[2] > 0.7) {
951 if(hsl[2] > 0.7) {
952 hsl[2] = 0.4;
953 if(hsl[1] >= 0.1) hsl[1] -= 0.1;
954 }
955 }
956 if(bg_hsl[2] < 0.4) {
957 if(c < 2.9) {
958 if(hsl[2] <= 0.6) {
959 hsl[2] = 0.6;
960 if(hsl[1] >= 0.1) hsl[1] -= 0.1;
961 }
962 }
963 }
964 }
965 return rgb2hex(...hslToRgb(...hsl));
966}
967
968const getLinkColors = ids => {
969 if(typeof ids === "string") ids = ids.split(",");
970 ids = [...new Set(ids)];
971 return new Promise(async (resolve, reject) => {
972 chrome.storage.local.get(["linkColors"], async data => {
973 let linkColors = data.linkColors || {};
974 let toFetch = [];
975 let fetched = [];
976 for(let id of ids) {
977 if(typeof linkColors[id] === "undefined") {
978 toFetch.push(id);
979 } else {
980 if(linkColors[id]) fetched.push({id, color: linkColors[id]});
981 }
982 }
983 if(toFetch.length === 0) {
984 return resolve(fetched);
985 }
986
987 try {
988 let res = await fetch("https://dimden.dev/services/twitter_link_colors/v2/get_multiple/"+toFetch.join(","));
989 let json = await res.json();
990 for(let id in json) {
991 if(json[id] === 'none' || json[id] === '4595b5') {
992 continue;
993 }
994 fetched.push({id, color: json[id]});
995 linkColors[id] = json[id];
996 }
997 for(let id of ids) {
998 if(typeof linkColors[id] === "undefined") {
999 linkColors[id] = 0;
1000 }
1001 }
1002 let keys = Object.keys(linkColors);
1003 if(keys.length > 20000) {
1004 chrome.storage.local.set({linkColors: {}}, () => {});
1005 } else {
1006 chrome.storage.local.set({linkColors}, () => {});
1007 }
1008 return resolve(fetched);
1009 } catch(e) {
1010 return resolve(fetched);
1011 }
1012 });
1013 });
1014}
1015
1016function getOtAuthToken(cache = true) {
1017 return new Promise((resolve, reject) => {
1018 chrome.storage.local.get(['otPrivateTokens'], async data => {
1019 if(!data.otPrivateTokens) {
1020 data.otPrivateTokens = {};
1021 }
1022 if(data.otPrivateTokens[user.id_str] && cache) {
1023 resolve(data.otPrivateTokens[user.id_str]);
1024 } else {
1025 let tokens = await fetch(`https://dimden.dev/services/twitter_link_colors/v2/request_token`, {method: 'post'}).then(r => r.json());
1026 let tweet;
1027 try {
1028 tweet = await API.tweet.postV2({
1029 status: `otauth=${tokens.public_token}`
1030 });
1031 let res = await fetch(`https://dimden.dev/services/twitter_link_colors/v2/verify_token`, {
1032 method: 'POST',
1033 headers: {
1034 'Content-Type': 'application/json'
1035 },
1036 body: JSON.stringify({
1037 tweet,
1038 public_token: tokens.public_token,
1039 private_token: tokens.private_token
1040 })
1041 }).then(i => i.text());
1042 if(res === 'success') {
1043 data.otPrivateTokens[user.id_str] = tokens.private_token;
1044 chrome.storage.local.set({otPrivateTokens: data.otPrivateTokens}, () => {
1045 resolve(tokens.private_token);
1046 });
1047 } else {
1048 console.error(res);
1049 alert(res);
1050 reject(res);
1051 }
1052 } catch(e) {
1053 console.error(e);
1054 alert(e);
1055 reject(e);
1056 } finally {
1057 API.tweet.delete(tweet.id_str).catch(e => {
1058 console.error(e);
1059 setTimeout(() => {
1060 API.tweet.delete(tweet.id_str);
1061 }, 1000);
1062 });
1063 }
1064 }
1065 });
1066 });
1067}
1068
1069function isProfilePath(path) {
1070 path = path.split('?')[0].split('#')[0];
1071 if(path.endsWith('/')) path = path.slice(0, -1);
1072 if(path.split('/').length > 2) return false;
1073 if(path.length <= 1) return false;
1074 if(['/home', '/notifications', '/messages', '/settings', '/explore', '/login', '/register', '/signin', '/signup', '/logout', '/i', '/old', '/search', '/donate'].includes(path)) return false;
1075 return /^\/[A-z-0-9-_]{1,15}$/.test(path);
1076}
1077function isSticky(el) {
1078 while(el !== document.body.parentElement) {
1079 let pos = getComputedStyle(el).position;
1080 if(pos === 'sticky' || pos === 'fixed') return true;
1081 el = el.parentElement;
1082 }
1083 return false;
1084}
1085function onVisible(element, callback) {
1086 new IntersectionObserver((entries, observer) => {
1087 entries.forEach(entry => {
1088 if(entry.intersectionRatio > 0) {
1089 callback(element);
1090 observer.disconnect();
1091 }
1092 });
1093 }).observe(element);
1094}
1095function updateUnfollows(res) {
1096 return new Promise(async (resolve, reject) => {
1097 let data = res[user.id_str];
1098 let cursor = "-1";
1099 let followers = [], following = [];
1100
1101 data.lastUpdate = Date.now();
1102 chrome.storage.local.set({unfollows: res});
1103
1104 while(cursor !== "0") {
1105 let data = await API.user.getFollowersIds(cursor);
1106 cursor = data.next_cursor_str;
1107 followers = followers.concat(data.ids);
1108 }
1109 cursor = "-1";
1110 while(cursor !== "0") {
1111 let data = await API.user.getFollowingIds(cursor);
1112 cursor = data.next_cursor_str;
1113 following = following.concat(data.ids);
1114 }
1115
1116 let unfollowers = data.followers.filter(f => !followers.includes(f));
1117 data.followers = followers;
1118 if(unfollowers.length > 0 && unfollowers.length < 100) {
1119 unfollowers = unfollowers.map(u => [u, Date.now()]);
1120 data.unfollowers = data.unfollowers.concat(unfollowers);
1121 if(data.unfollowers.length > 100) {
1122 data.unfollowers = data.unfollowers.slice(data.unfollowers.length - 100);
1123 }
1124 }
1125 let unfollowings = data.following.filter(f => !following.includes(f));
1126 data.following = following;
1127 if(unfollowings.length > 0 && unfollowings.length < 100) {
1128 unfollowings = unfollowings.map(u => [u, Date.now()]);
1129 data.unfollowings = data.unfollowings.concat(unfollowings);
1130 if(data.unfollowings.length > 100) {
1131 data.unfollowings = data.unfollowings.slice(data.unfollowings.length - 100);
1132 }
1133 }
1134 chrome.storage.local.set({unfollows: res}, () => resolve(res));
1135 });
1136}
1137function getTimeZone() {
1138 let offset = new Date().getTimezoneOffset(), o = Math.abs(offset);
1139 return (offset < 0 ? "+" : "-") + ("00" + Math.floor(o / 60)).slice(-2) + ":" + ("00" + (o % 60)).slice(-2);
1140}
1141
1142const mediaClasses = [
1143 undefined,
1144 'tweet-media-element-one',
1145 'tweet-media-element-two',
1146 'tweet-media-element-three',
1147 'tweet-media-element-two',
1148];
1149const sizeFunctions = [
1150 undefined,
1151 (w, h) => [w > 450 ? 450 : w < 150 ? 150 : w, h > 500 ? 500 : h < 150 ? 150 : h],
1152 (w, h) => [w > 200 ? 200 : w < 150 ? 150 : w, h > 400 ? 400 : h < 150 ? 150 : h],
1153 (w, h) => [150, h > 250 ? 250 : h < 150 ? 150 : h],
1154 // (w, h) => [w > 100 ? 100 : w, h > 150 ? 150 : h],
1155 (w, h) => [w > 200 ? 200 : w < 150 ? 150 : w, h > 400 ? 400 : h < 150 ? 150 : h],
1156];
1157const quoteSizeFunctions = [
1158 undefined,
1159 (w, h) => [w > 400 ? 400 : w, h > 400 ? 400 : h],
1160 (w, h) => [w > 200 ? 200 : w, h > 400 ? 400 : h],
1161 (w, h) => [w > 125 ? 125 : w, h > 200 ? 200 : h],
1162 (w, h) => [w > 100 ? 100 : w, h > 150 ? 150 : h],
1163];
1164
1165async function renderTrends(compact = false, cache = true) {
1166 if(vars.hideTrends) return;
1167 let [trendsData, hashflags] = await Promise.allSettled([API.discover[vars.disablePersonalizedTrends ? 'getTrends' : 'getTrendsV2'](cache), API.discover.getHashflags()]);
1168 let trends = trendsData.value.modules;
1169 hashflags = hashflags.value ? hashflags.value : [];
1170 let trendsContainer = document.getElementById('trends-list');
1171 trendsContainer.innerHTML = '';
1172 let max = 7;
1173 if(innerHeight < 650) max = 3;
1174 trends.slice(0, max).forEach(({ trend }) => {
1175 let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === trend.name.slice(1).toLowerCase());
1176 let trendDiv = document.createElement('div');
1177 trendDiv.className = 'trend' + (compact ? ' compact-trend' : '');
1178 trendDiv.innerHTML = compact ? /*html*/`<a href="https://twitter.com/search?q=${escapeHTML(trend.name)}" class="trend-name">${escapeHTML(trend.name)}</a>` : /*html*/`
1179 <b>
1180 <a href="https://twitter.com/search?q=${escapeHTML(trend.name)}" class="trend-name">
1181 ${escapeHTML(trend.name)}
1182 ${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag" width="16" height="16">` : ''}
1183 </a>
1184 </b><br>
1185 <span class="trend-description">${trend.meta_description ? escapeHTML(trend.meta_description) : ''}</span>
1186 `;
1187 trendsContainer.append(trendDiv);
1188 if(vars.enableTwemoji) twemoji.parse(trendDiv);
1189 });
1190}
1191async function renderDiscovery(cache = true) {
1192 if(vars.hideWtf) return;
1193 let discover = await API.discover.getPeople(cache);
1194 let discoverContainer = document.getElementById('wtf-list');
1195 discoverContainer.innerHTML = '';
1196 try {
1197 let usersData = discover.globalObjects.users;
1198 let max = 6;
1199 if(innerHeight < 700) max = 5;
1200 if(innerHeight < 650) max = 3;
1201 let usersSuggestions = discover.timeline.instructions[0].addEntries.entries[0].content.timelineModule.items.map(s => s.entryId.slice('user-'.length)).slice(0, max); // why is it so deep
1202 usersSuggestions.forEach(userId => {
1203 let userData = usersData[userId];
1204 if (!userData) return;
1205 if(vars.twitterBlueCheckmarks && userData.ext && userData.ext.isBlueVerified && userData.ext.isBlueVerified.r && userData.ext.isBlueVerified.r.ok) {
1206 userData.verified_type = "Blue";
1207 }
1208 if(userData.ext && userData.ext.verifiedType && userData.ext.verifiedType.r && userData.ext.verifiedType.r.ok) {
1209 userData.verified_type = userData.ext.verifiedType.r.ok;
1210 }
1211 let udiv = document.createElement('div');
1212 udiv.className = 'wtf-user';
1213 udiv.dataset.userId = userId;
1214 udiv.innerHTML = `
1215 <a class="tweet-avatar-link" href="https://twitter.com/${userData.screen_name}"><img src="${`${(userData.default_profile_image && vars.useOldDefaultProfileImage) ? chrome.runtime.getURL(`images/default_profile_images/default_profile_${Number(userData.id_str) % 7}_normal.png`): userData.profile_image_url_https}`.replace("_normal", "_bigger")}" alt="${escapeHTML(userData.name)}" class="tweet-avatar" width="48" height="48"></a>
1216 <div class="tweet-header wtf-header">
1217 <a class="tweet-header-info wtf-user-link" href="https://twitter.com/${userData.screen_name}">
1218 <b class="tweet-header-name wtf-user-name${userData.verified || userData.verified_type ? ' user-verified' : userData.id_str === '1123203847776763904' ? ' user-verified user-verified-dimden' : ''} ${userData.verified_type === 'Government' ? 'user-verified-gray' : userData.verified_type === 'Business' ? 'user-verified-yellow' : userData.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(userData.name)}</b>
1219 <span class="tweet-header-handle wtf-user-handle">@${userData.screen_name}</span>
1220 </a>
1221 <br>
1222 <button class="nice-button discover-follow-btn ${userData.following ? 'following' : 'follow'}" style="position:relative;bottom: 1px;">${userData.following ? LOC.following_btn.message : LOC.follow.message}</button>
1223 </div>
1224 `;
1225 const followBtn = udiv.querySelector('.discover-follow-btn');
1226 followBtn.addEventListener('click', async () => {
1227 if (followBtn.className.includes('following')) {
1228 try {
1229 await API.user.unfollow(userData.screen_name);
1230 } catch(e) {
1231 console.error(e);
1232 alert(e);
1233 return;
1234 }
1235 followBtn.classList.remove('following');
1236 followBtn.classList.add('follow');
1237 followBtn.innerText = LOC.follow.message;
1238 userData.following = false;
1239 } else {
1240 try {
1241 await API.user.follow(userData.screen_name);
1242 } catch(e) {
1243 console.error(e);
1244 alert(e);
1245 return;
1246 }
1247 followBtn.classList.add('following');
1248 followBtn.classList.remove('follow');
1249 followBtn.innerText = LOC.following_btn.message;
1250 userData.following = true;
1251 }
1252 chrome.storage.local.set({
1253 discoverData: {
1254 date: Date.now(),
1255 data: discover
1256 }
1257 }, () => { })
1258 });
1259 discoverContainer.append(udiv);
1260 if(vars.enableTwemoji) twemoji.parse(udiv);
1261 });
1262 } catch (e) {
1263 console.warn(e);
1264 }
1265}
1266
1267function renderMedia(t) {
1268 let html = '';
1269 if(!t.extended_entities || !t.extended_entities.media) return '';
1270
1271 let cws = [];
1272
1273 for(let i = 0; i < t.extended_entities.media.length; i++) {
1274 let m = t.extended_entities.media[i];
1275 let toCensor = !vars.displaySensitiveContent && t.possibly_sensitive;
1276 if(m.sensitive_media_warning) {
1277 if(m.sensitive_media_warning.graphic_violence) {
1278 cws.push(LOC.graphic_violence.message);
1279 toCensor = !vars.uncensorGraphicViolenceAutomatically;
1280 }
1281 if(m.sensitive_media_warning.adult_content) {
1282 cws.push(LOC.adult_content.message);
1283 toCensor = !vars.uncensorAdultContentAutomatically;
1284 }
1285 if(m.sensitive_media_warning.other) {
1286 cws.push(LOC.sensitive_content.message);
1287 toCensor = !vars.uncensorSensitiveContentAutomatically;
1288 }
1289 }
1290 if(m.type === 'photo') {
1291 html += /*html*/`
1292 <img
1293 ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
1294 crossorigin="anonymous"
1295 width="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}"
1296 height="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}"
1297 loading="lazy"
1298 src="${m.media_url_https + (vars.showOriginalImages && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : '')}"
1299 class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
1300 >`;
1301 } else if(m.type === 'animated_gif') {
1302 html += /*html*/`
1303 <video
1304 ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
1305 crossorigin="anonymous"
1306 width="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}"
1307 height="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}"
1308 loop
1309 onclick="if(this.paused) this.play(); else this.pause()"
1310 ${vars.disableGifAutoplay ? '' : 'autoplay'}
1311 muted
1312 class="tweet-media-element tweet-media-gif ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
1313 >
1314 ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
1315 ${LOC.unsupported_video.message}
1316 </video>
1317 `;
1318 } else if(m.type === 'video') {
1319 html += /*html*/`
1320 <video
1321 ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
1322 crossorigin="anonymous"
1323 width="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}"
1324 height="${sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}"
1325 preload="none"
1326 ${t.extended_entities.media.length > 1 ? 'controls' : ''}
1327 poster="${m.media_url_https}"
1328 class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
1329 >
1330 ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
1331 ${LOC.unsupported_video.message}
1332 </video>
1333 `;
1334 }
1335 if(i === 1 && t.extended_entities.media.length > 3) {
1336 html += '<br>';
1337 }
1338 }
1339
1340 if(cws.length > 0) {
1341 cws = [...new Set(cws)];
1342 cws = LOC.content_warning.message.replace('$WARNINGS$', cws.join(', '));
1343 html += `<br><div class="tweet-media-cws">${cws}</div>`;
1344 }
1345 return html;
1346}
1347
1348async function appendUser(u, container, label) {
1349 let userElement = document.createElement('div');
1350 userElement.classList.add('user-item');
1351 if(vars.twitterBlueCheckmarks && u.ext && u.ext.isBlueVerified && u.ext.isBlueVerified.r && u.ext.isBlueVerified.r.ok) {
1352 u.verified_type = "Blue";
1353 }
1354 if(u.ext && u.ext.verifiedType && u.ext.verifiedType.r && u.ext.verifiedType.r.ok) {
1355 u.verified_type = u.ext.verifiedType.r.ok;
1356 }
1357 userElement.innerHTML = `
1358 <div>
1359 <a href="https://twitter.com/${u.screen_name}" class="user-item-link">
1360 <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="user-item-avatar tweet-avatar" width="48" height="48">
1361 <div class="user-item-text">
1362 <span${u.id_str === '1123203847776763904' ? ' title="Old Twitter Layout extension developer"' : ''} class="tweet-header-name user-item-name${u.protected ? ' user-protected' : ''}${u.verified || u.verified_type ? ' user-verified' : u.id_str === '1123203847776763904' ? ' user-verified user-verified-dimden' : ''} ${u.verified_type === 'Government' ? 'user-verified-gray' : u.verified_type === 'Business' ? 'user-verified-yellow' : u.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(u.name)}</span><br>
1363 <span class="tweet-header-handle">@${u.screen_name}</span>
1364 ${u.followed_by ? `<span class="follows-you-label">${LOC.follows_you.message}</span>` : ''}
1365 ${label ? `<br><span class="user-item-additional">${escapeHTML(label)}</span>` : ''}
1366 </div>
1367 </a>
1368 </div>
1369 <div>
1370 <button class="user-item-btn nice-button ${u.following ? 'following' : 'follow'}">${u.following ? LOC.following_btn.message : LOC.follow.message}</button>
1371 </div>
1372 `;
1373
1374 let followButton = userElement.querySelector('.user-item-btn');
1375 followButton.addEventListener('click', async () => {
1376 if (followButton.classList.contains('following')) {
1377 try {
1378 await API.user.unfollow(u.screen_name);
1379 } catch(e) {
1380 console.error(e);
1381 alert(e);
1382 return;
1383 }
1384 followButton.classList.remove('following');
1385 followButton.classList.add('follow');
1386 followButton.innerText = LOC.follow.message;
1387 } else {
1388 try {
1389 await API.user.follow(u.screen_name);
1390 } catch(e) {
1391 console.error(e);
1392 alert(e);
1393 return;
1394 }
1395 followButton.classList.remove('follow');
1396 followButton.classList.add('following');
1397 followButton.innerText = LOC.following_btn.message;
1398 }
1399 });
1400
1401 container.appendChild(userElement);
1402 if(vars.enableTwemoji) twemoji.parse(userElement);
1403}
1404
1405let lastTweetErrorDate = 0;
1406async function appendTweet(t, timelineContainer, options = {}) {
1407 if(typeof t !== 'object') {
1408 console.error('Tweet is undefined', t, timelineContainer, options);
1409 return;
1410 }
1411 if(typeof t.user !== 'object') {
1412 console.error('Tweet user is undefined', t, timelineContainer, options);
1413 return;
1414 }
1415 try {
1416 if(typeof seenReplies !== 'undefined') {
1417 if(seenReplies.includes(t.id_str)) return;
1418 seenReplies.push(t.id_str);
1419 }
1420 if(typeof seenThreads !== 'undefined') {
1421 if(seenThreads.includes(t.id_str)) return;
1422 }
1423 // if(t.entities && t.entities.urls) {
1424 // let webUrl = t.entities.urls.find(u => u.expanded_url.startsWith('https://twitter.com/i/web/status/'));
1425 // if(webUrl) {
1426 // try {
1427 // let source = t.source;
1428 // t = await API.tweet.getV2(t.id_str);
1429 // t.source = source;
1430 // } catch(e) {}
1431 // }
1432 // }
1433 if(t.socialContext) {
1434 options.top = {};
1435 if(t.socialContext.description) {
1436 options.top.text = `<a target="_blank" href="https://twitter.com/i/topics/${t.socialContext.topic_id}">${t.socialContext.name}</a>`;
1437 options.top.icon = "\uf008";
1438 options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF";
1439 } else if(t.socialContext.contextType === "Like") {
1440 options.top.text = `<${t.socialContext.landingUrl.url.split('=')[1] ? `a href="https://twitter.com/i/user/${t.socialContext.landingUrl.url.split('=')[1]}"` : 'span'}>${!vars.heartsNotStars ? t.socialContext.text.replace(' liked', ' favorited') : t.socialContext.text}</a>`;
1441 if(vars.heartsNotStars) {
1442 options.top.icon = "\uf015";
1443 options.top.color = "rgb(249, 24, 128)";
1444 } else {
1445 options.top.icon = "\uf001";
1446 options.top.color = "#ffac33";
1447 }
1448 } else if(t.socialContext.contextType === "Follow") {
1449 options.top.text = t.socialContext.text;
1450 options.top.icon = "\uf002";
1451 options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF";
1452 } else if(t.socialContext.contextType === "Conversation") {
1453 options.top.text = t.socialContext.text;
1454 options.top.icon = "\uf005";
1455 options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF";
1456 } else {
1457 console.log(t.socialContext);
1458 }
1459 }
1460 if(vars.twitterBlueCheckmarks && t.user.ext && t.user.ext.isBlueVerified && t.user.ext.isBlueVerified.r && t.user.ext.isBlueVerified.r.ok) {
1461 t.user.verified_type = "Blue";
1462 }
1463 if(t.user && t.user.ext && t.user.ext.verifiedType && t.user.ext.verifiedType.r && t.user.ext.verifiedType.r.ok) {
1464 t.user.verified_type = t.user.ext.verifiedType.r.ok;
1465 }
1466 if(typeof tweets !== 'undefined') tweets.push(['tweet', t, options]);
1467 const tweet = document.createElement('div');
1468 t.element = tweet;
1469 t.options = options;
1470
1471 if(!options.mainTweet && typeof mainTweetLikers !== 'undefined' && !location.pathname.includes("retweets/with_comments")) {
1472 tweet.addEventListener('click', async e => {
1473 if(e.target.className && (e.target.className.startsWith('tweet tweet-id-') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact')) {
1474 document.getElementById('loading-box').hidden = false;
1475 savePageData();
1476 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`);
1477 updateSubpage();
1478 mediaToUpload = [];
1479 linkColors = {};
1480 cursor = undefined;
1481 seenReplies = [];
1482 mainTweetLikers = [];
1483 let restored = await restorePageData();
1484 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
1485 if(subpage === 'tweet' && !restored) {
1486 updateReplies(id);
1487 } else if(subpage === 'likes') {
1488 updateLikes(id);
1489 } else if(subpage === 'retweets') {
1490 updateRetweets(id);
1491 } else if(subpage === 'retweets_with_comments') {
1492 updateRetweetsWithComments(id);
1493 }
1494 renderDiscovery();
1495 renderTrends();
1496 currentLocation = location.pathname;
1497 }
1498 });
1499 tweet.addEventListener('mousedown', e => {
1500 if(e.button === 1) {
1501 e.preventDefault();
1502 if(e.target.className && (e.target.className.startsWith('tweet tweet-id-') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact')) {
1503 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`);
1504 }
1505 }
1506 });
1507 } else {
1508 if(!options.mainTweet) {
1509 tweet.addEventListener('click', e => {
1510 if(e.target.className && (e.target.className.startsWith('tweet tweet-id-') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact')) {
1511 let tweetData = t;
1512 if(tweetData.retweeted_status) tweetData = tweetData.retweeted_status;
1513 tweet.classList.add('tweet-preload');
1514 new TweetViewer(user, tweetData);
1515 }
1516 });
1517 tweet.addEventListener('mousedown', e => {
1518 if(e.button === 1) {
1519 e.preventDefault();
1520 if(e.target.className && (e.target.className.startsWith('tweet tweet-id-') || e.target.classList.contains('tweet-body') || e.target.classList.contains('tweet-reply-to') || e.target.className === 'tweet-interact')) {
1521 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}`);
1522 }
1523 }
1524 });
1525 }
1526 }
1527 tweet.tabIndex = -1;
1528 tweet.className = `tweet tweet-id-${t.id_str} ${options.mainTweet ? 'tweet-main' : location.pathname.includes('/status/') ? 'tweet-replying' : ''}`;
1529 tweet.dataset.tweetId = t.id_str;
1530 tweet.dataset.userId = t.user.id_str;
1531 try {
1532 if(!activeTweet) {
1533 tweet.classList.add('tweet-active');
1534 activeTweet = tweet;
1535 }
1536 } catch(e) {};
1537
1538 if(t.nonReply) {
1539 tweet.classList.add('tweet-non-reply');
1540 }
1541
1542 if(t.threadContinuation) {
1543 options.threadContinuation = true;
1544 }
1545 if(t.noTop) {
1546 options.noTop = true;
1547 }
1548 if (options.threadContinuation) tweet.classList.add('tweet-self-thread-continuation');
1549 if (options.selfThreadContinuation) tweet.classList.add('tweet-self-thread-continuation');
1550
1551 if (options.noTop) tweet.classList.add('tweet-no-top');
1552 if(vars.linkColorsInTL && typeof linkColors !== 'undefined') {
1553 if(linkColors[t.user.id_str]) {
1554 let sc = makeSeeableColor(linkColors[t.user.id_str]);
1555 tweet.style.setProperty('--link-color', sc);
1556 } else {
1557 if(t.user.profile_link_color && t.user.profile_link_color !== '1DA1F2') {
1558 let sc = makeSeeableColor(t.user.profile_link_color);
1559 tweet.style.setProperty('--link-color', sc);
1560 }
1561 }
1562 }
1563 let full_text = t.full_text ? t.full_text : '';
1564 let strippedDownText = full_text
1565 .replace(/(?:https?|ftp):\/\/[\n\S]+/g, '') //links
1566 .replace(/(?<!\w)@([\w+]{1,15}\b)/g, '') //mentions
1567 .replace(/[\p{Extended_Pictographic}]/gu, '') //emojis (including ones that arent colored)
1568 .replace(/[\u200B-\u200D\uFE0E\uFE0F]/g, '') //sometimes emojis leave these behind
1569 .replace(/\d+/g, '') //numbers
1570 .trim();
1571 let detectedLanguage = strippedDownText.length < 1 ? {languages:[{language:LANGUAGE, percentage:100}]} : await chrome.i18n.detectLanguage(strippedDownText);
1572 if(!detectedLanguage.languages[0]) detectedLanguage = {languages:[{language:t.lang, percentage:100}]}; //fallback to what twitter says
1573 let isEnglish = detectedLanguage.languages[0] && detectedLanguage.languages[0].percentage > 60 && detectedLanguage.languages[0].language.startsWith(LANGUAGE);
1574 let videos = t.extended_entities && t.extended_entities.media && t.extended_entities.media.filter(m => m.type === 'video');
1575 if(!videos || videos.length === 0) {
1576 videos = undefined;
1577 }
1578 if(videos) {
1579 for(let v of videos) {
1580 if(!v.video_info) continue;
1581 v.video_info.variants = v.video_info.variants.sort((a, b) => {
1582 if(!b.bitrate) return -1;
1583 return b.bitrate-a.bitrate;
1584 });
1585 if(typeof(vars.savePreferredQuality) !== 'boolean') {
1586 chrome.storage.sync.set({
1587 savePreferredQuality: true
1588 }, () => {});
1589 vars.savePreferredQuality = true;
1590 }
1591 if(localStorage.preferredQuality && vars.savePreferredQuality) {
1592 let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => {
1593 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);
1594 });
1595 let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url);
1596 if(preferredQualityVariantIndex !== -1) {
1597 let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex];
1598 v.video_info.variants.splice(preferredQualityVariantIndex, 1);
1599 v.video_info.variants.unshift(preferredQualityVariant);
1600 }
1601 }
1602 }
1603 }
1604 if(full_text.includes("Learn more")) {
1605 console.log(t);
1606 }
1607 if(t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) {
1608 full_text = "";
1609 }
1610 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)
1611 try {
1612 if(t.quoted_status_result && t.quoted_status_result.result.tweet) {
1613 t.quoted_status = t.quoted_status_result.result.tweet.legacy;
1614 t.quoted_status.user = t.quoted_status_result.result.tweet.core.user_results.result.legacy;
1615 } else {
1616 t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str);
1617 }
1618 } catch {
1619 t.quoted_status = undefined;
1620 }
1621 }
1622 let followUserText, unfollowUserText, blockUserText, unblockUserText;
1623 let mentionedUserText = ``;
1624 let quoteMentionedUserText = ``;
1625 if(
1626 LOC.follow_user.message.includes('$SCREEN_NAME$') && LOC.unfollow_user.message.includes('$SCREEN_NAME$') &&
1627 LOC.block_user.message.includes('$SCREEN_NAME$') && LOC.unblock_user.message.includes('$SCREEN_NAME$')
1628 ) {
1629 followUserText = `${LOC.follow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`;
1630 unfollowUserText = `${LOC.unfollow_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`;
1631 blockUserText = `${LOC.block_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`;
1632 unblockUserText = `${LOC.unblock_user.message.replace('$SCREEN_NAME$', t.user.screen_name)}`;
1633 } else {
1634 followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`;
1635 unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`;
1636 blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`;
1637 unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`;
1638 }
1639 if(t.in_reply_to_screen_name && t.display_text_range) {
1640 t.entities.user_mentions.forEach(user_mention => {
1641 if(user_mention.indices[0] < t.display_text_range[0]){
1642 mentionedUserText += `<a href="https://twitter.com/${user_mention.screen_name}">@${user_mention.screen_name}</a> `
1643 }
1644 //else this is not reply but mention
1645 });
1646 }
1647 if(t.quoted_status && t.quoted_status.in_reply_to_screen_name && t.display_text_range) {
1648 t.quoted_status.entities.user_mentions.forEach(user_mention => {
1649 if(user_mention.indices[0] < t.display_text_range[0]){
1650 quoteMentionedUserText += `@${user_mention.screen_name} `
1651 }
1652 //else this is not reply but mention
1653 });
1654 }
1655 // i fucking hate this thing
1656 tweet.innerHTML = /*html*/`
1657 <div class="tweet-top" hidden></div>
1658 <a class="tweet-avatar-link" href="https://twitter.com/${t.user.screen_name}">
1659 <img
1660 onerror="this.src = '${vars.useOldDefaultProfileImage ? chrome.runtime.getURL(`images/default_profile_images/default_profile_bigger.png`) : 'https://abs.twimg.com/sticky/default_profile_images/default_profile_bigger.png'}'"
1661 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.")}"
1662 alt="${t.user.name}"
1663 class="tweet-avatar"
1664 width="48"
1665 height="48"
1666 >
1667 </a>
1668 <div class="tweet-header ${options.mainTweet ? 'tweet-header-main' : ''}">
1669 <a class="tweet-header-info ${options.mainTweet ? 'tweet-header-info-main' : ''}" href="https://twitter.com/${t.user.screen_name}">
1670 <b
1671 ${t.user.id_str === '1123203847776763904' ? 'title="Old Twitter Layout extension developer" ' : ''}
1672 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' : ''}"
1673 >${escapeHTML(t.user.name)}</b>
1674 <span class="tweet-header-handle">@${t.user.screen_name}</span>
1675 </a>
1676 <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>
1677 ${location.pathname.split("?")[0].split("#")[0] === '/i/bookmarks' ? `<span class="tweet-delete-bookmark${!isEnglish ? ' tweet-delete-bookmark-lower' : ''}">×</span>` : ''}
1678 ${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>` : ''}
1679 ${!options.mainTweet && !isEnglish ? `<span class="tweet-translate-after">${`${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 ? LOC.view_translation.message : ''}</span>` : ''}
1680 </div>
1681 ${mentionedUserText !== `` &&
1682 !options.threadContinuation &&
1683 !options.noTop &&
1684 !location.pathname.includes('/status/') &&
1685 !vars.useOldStyleReply ? /*html*/`
1686 <div class="tweet-reply-to"><span>${LOC.replying_to_user.message.replace('$SCREEN_NAME$', mentionedUserText.trim().replaceAll(`> <`, `>${LOC.replying_to_comma.message}<`).replace(`>${LOC.replying_to_comma.message}<`, `>${LOC.replying_to_and.message}<`))}</span></div>
1687 `: ''}
1688 <div class="tweet-body ${options.mainTweet ? 'tweet-body-main' : ''}">
1689 <span class="tweet-body-text ${vars.noBigFont || t.full_text.length > 280 || !options.bigFont || (!options.mainTweet && location.pathname.includes('/status/')) ? '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>
1690 ${!isEnglish && options.mainTweet ? /*html*/`
1691 <br>
1692 <span class="tweet-translate">${LOC.view_translation.message}</span>
1693 ` : ``}
1694 ${t.extended_entities && t.extended_entities.media ? /*html*/`
1695 <div class="tweet-media">
1696 ${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? /*html*/`
1697 <div class="tweet-media-video-overlay">
1698 <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play">
1699 <g>
1700 <path class="svg-play-path" d="M8 5v14l11-7z"></path>
1701 <path d="M0 0h24v24H0z" fill="none"></path>
1702 </g>
1703 </svg>
1704 </div>
1705 ` : ''}
1706 ${renderMedia(t)}
1707 </div>
1708 ${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? /*html*/`<div class="tweet-media-controls">GIF</div>` : ''}
1709 ${videos ? /*html*/`
1710 <div class="tweet-media-controls">
1711 ${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> •
1712 ${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(" / ")}
1713 </div>
1714 ` : ``}
1715 <span class="tweet-media-data"></span>
1716 ` : ``}
1717 ${t.card ? `<div class="tweet-card"></div>` : ''}
1718 ${t.quoted_status ? /*html*/`
1719 <a class="tweet-body-quote" target="_blank" href="https://twitter.com/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}">
1720 <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">
1721 <div class="tweet-header-quote">
1722 <span class="tweet-header-info-quote">
1723 <b class="tweet-header-name-quote ${t.quoted_status.user.verified ? 'user-verified' : t.quoted_status.user.id_str === '1123203847776763904' ? 'user-verified user-verified-dimden' : ''} ${t.quoted_status.user.protected ? 'user-protected' : ''}">${escapeHTML(t.quoted_status.user.name)}</b>
1724 <span class="tweet-header-handle-quote">@${t.quoted_status.user.screen_name}</span>
1725 </span>
1726 </div>
1727 <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>
1728 ${quoteMentionedUserText !== `` && !vars.useOldStyleReply ? /*html*/`
1729 <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>
1730 ` : ''}
1731 <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>
1732 ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? `
1733 <div class="tweet-media-quote">
1734 ${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')}
1735 </div>
1736 ` : ''}
1737 </a>
1738 ` : ``}
1739 ${t.limited_actions === 'limit_trusted_friends_tweet' && (options.mainTweet || !location.pathname.includes('/status/')) ? /*html*/`
1740 <div class="tweet-limited">
1741 ${LOC.circle_limited_tweet.message}
1742 <a href="https://help.twitter.com/en/using-twitter/twitter-circle" target="_blank">${LOC.learn_more.message}</a>
1743 </div>
1744 `.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) : ''}
1745 ${t.tombstone ? `<div class="tweet-warning">${t.tombstone}</div>` : ''}
1746 ${((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>` : ''}
1747 ${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>` : ''}
1748 ${options.mainTweet ? /*html*/`
1749 <div class="tweet-footer">
1750 <div class="tweet-footer-stats">
1751 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}" class="tweet-footer-stat tweet-footer-stat-o">
1752 <span class="tweet-footer-stat-text">${LOC.replies.message}</span>
1753 <b class="tweet-footer-stat-count tweet-footer-stat-replies">${Number(t.reply_count).toLocaleString().replace(/\s/g, ',')}</b>
1754 </a>
1755 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets" class="tweet-footer-stat tweet-footer-stat-r">
1756 <span class="tweet-footer-stat-text">${LOC.retweets.message}</span>
1757 <b class="tweet-footer-stat-count tweet-footer-stat-retweets">${Number(t.retweet_count).toLocaleString().replace(/\s/g, ',')}</b>
1758 </a>
1759 <a href="https://twitter.com/${t.user.screen_name}/status/${t.id_str}/likes" class="tweet-footer-stat tweet-footer-stat-f">
1760 <span class="tweet-footer-stat-text">${vars.heartsNotStars ? LOC.likes.message : LOC.favorites.message}</span>
1761 <b class="tweet-footer-stat-count tweet-footer-stat-favorites">${Number(t.favorite_count).toLocaleString().replace(/\s/g, ',')}</b>
1762 </a>
1763 </div>
1764 <div class="tweet-footer-favorites"></div>
1765 </div>
1766 ` : ''}
1767 <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>
1768 <div class="tweet-interact">
1769 <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>
1770 <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>
1771 <div class="tweet-interact-retweet-menu dropdown-menu" hidden>
1772 <span class="tweet-interact-retweet-menu-retweet">${t.retweeted ? LOC.unretweet.message : LOC.retweet.message}${!vars.disableHotkeys ? ' (T)' : ''}</span>
1773 <span class="tweet-interact-retweet-menu-quote">${LOC.quote_tweet.message}${!vars.disableHotkeys ? ' (Q)' : ''}</span>
1774 ${options.mainTweet ? /*html*/`
1775 <span class="tweet-interact-retweet-menu-quotes">${LOC.see_quotes_big.message}</span>
1776 <span class="tweet-interact-retweet-menu-retweeters">${LOC.see_retweeters.message}</span>
1777 ` : ''}
1778 </div>
1779 <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>
1780 ${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>` : ''}
1781 ${t.bookmark_count && vars.showBookmarkCount && options.mainTweet ?
1782 /*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>` :
1783 ''}
1784 <span class="tweet-interact-more"></span>
1785 <div class="tweet-interact-more-menu dropdown-menu" hidden>
1786 <span class="tweet-interact-more-menu-copy">${LOC.copy_link.message}</span>
1787 <span class="tweet-interact-more-menu-embed">${LOC.embed_tweet.message}</span>
1788 ${navigator.canShare ? `<span class="tweet-interact-more-menu-share">${LOC.share_tweet.message}</span>` : ''}
1789 <span class="tweet-interact-more-menu-newtwitter">${LOC.open_tweet_newtwitter.message}</span>
1790 ${t.user.id_str === user.id_str ? /*html*/`
1791 <hr>
1792 <span class="tweet-interact-more-menu-analytics">${LOC.tweet_analytics.message}</span>
1793 <span class="tweet-interact-more-menu-delete">${LOC.delete_tweet.message}</span>
1794 ${typeof pageUser !== 'undefined' && pageUser.id_str === user.id_str ? /*html*/`<span class="tweet-interact-more-menu-pin">${pinnedTweet && pinnedTweet.id_str === t.id_str ? LOC.unpin_tweet.message : LOC.pin_tweet.message}</span>` : ''}
1795 ` : ''}
1796 ${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*/`
1797 <span class="tweet-interact-more-menu-hide">${t.moderated ? LOC.unhide_tweet.message : LOC.hide_tweet.message}</span>
1798 `: ''}
1799 ${t.hasModeratedReplies ? /*html*/`
1800 <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>
1801 ` : ''}
1802 <hr>
1803 ${t.user.id_str !== user.id_str && !options.mainTweet ? /*html*/`
1804 <span class="tweet-interact-more-menu-follow"${t.user.blocking ? ' hidden' : ''}>${t.user.following ? unfollowUserText : followUserText}</span>
1805 ` : ''}
1806 ${t.user.id_str !== user.id_str ? /*html*/`
1807 <span class="tweet-interact-more-menu-block">${t.user.blocking ? unblockUserText : blockUserText}</span>
1808 ` : ''}
1809 ${!location.pathname.startsWith('/i/bookmarks') ? /*html*/`<span class="tweet-interact-more-menu-bookmark">${t.bookmarked ? LOC.remove_bookmark.message : LOC.bookmark_tweet.message}</span>` : ''}
1810 <span class="tweet-interact-more-menu-mute">${t.conversation_muted ? LOC.unmute_convo.message : LOC.mute_convo.message}</span>
1811 <hr>
1812 ${t.feedback ? t.feedback.map((f, i) => /*html*/`<span class="tweet-interact-more-menu-feedback" data-index="${i}">${f.prompt ? f.prompt : LOC.topic_not_interested.message}</span>`).join("\n") : ''}
1813 <span class="tweet-interact-more-menu-refresh">${LOC.refresh_tweet.message}</span>
1814 ${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>` : ``}
1815 ${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') : ''}
1816 ${t.extended_entities && t.extended_entities.media.length > 0 ? /*html*/`<span class="tweet-interact-more-menu-download">${LOC.download_media.message}</span>` : ``}
1817 ${vars.developerMode ? /*html*/`
1818 <hr>
1819 <span class="tweet-interact-more-menu-copy-user-id">${LOC.copy_user_id.message}</span>
1820 <span class="tweet-interact-more-menu-copy-tweet-id">${LOC.copy_tweet_id.message}</span>
1821 <span class="tweet-interact-more-menu-log">Log tweet object</span>
1822 ` : ''}
1823 </div>
1824 ${options.selfThreadButton && t.self_thread && t.self_thread.id_str && !options.threadContinuation && !location.pathname.includes('/status/') ? /*html*/`<a class="tweet-self-thread-button tweet-thread-right" target="_blank" href="https://twitter.com/${t.user.screen_name}/status/${t.self_thread.id_str}">${LOC.show_this_thread.message}</a>` : ``}
1825 ${!options.noTop && !options.selfThreadButton && t.in_reply_to_status_id_str && !(options.threadContinuation || (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str)) && !location.pathname.includes('/status/') ? `<a class="tweet-self-thread-button tweet-thread-right" target="_blank" href="https://twitter.com/${t.in_reply_to_screen_name}/status/${t.in_reply_to_status_id_str}">${LOC.show_this_thread.message}</a>` : ``}
1826 </div>
1827 <div class="tweet-reply" hidden>
1828 <br>
1829 <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>
1830 <span class="tweet-reply-error" style="color:red"></span>
1831 <textarea maxlength="1000" class="tweet-reply-text" placeholder="${LOC.reply_example.message}"></textarea>
1832 <button title="CTRL+ENTER" class="tweet-reply-button nice-button">${LOC.reply.message}</button><br>
1833 <span class="tweet-reply-char">0/280</span><br>
1834 <div class="tweet-reply-media" style="padding-bottom: 10px;"></div>
1835 </div>
1836 <div class="tweet-quote" hidden>
1837 <br>
1838 <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>
1839 <span class="tweet-quote-error" style="color:red"></span>
1840 <textarea maxlength="1000" class="tweet-quote-text" placeholder="${LOC.quote_example.message}"></textarea>
1841 <button title="CTRL+ENTER" class="tweet-quote-button nice-button">${LOC.quote.message}</button><br>
1842 <span class="tweet-quote-char">0/280</span><br>
1843 <div class="tweet-quote-media" style="padding-bottom: 10px;"></div>
1844 </div>
1845 <div class="tweet-self-thread-div" ${(options.threadContinuation || (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str)) ? '' : 'hidden'}>
1846 ${options.selfThreadContinuation && t.self_thread && t.self_thread.id_str && !location.pathname.includes('/status/') ? /*html*/`<br>
1847 <a class="tweet-self-thread-button" target="_blank" href="https://twitter.com/${t.user.screen_name}/status/${t.self_thread.id_str}">
1848 ${LOC.show_this_thread.message}
1849 </a>
1850 <span class="tweet-self-thread-line" style="margin-left: -108px;margin-top: -5px;"></span>
1851 <div class="tweet-self-thread-line-dots" style="margin-left: -120px;margin-top: -3px;"></div>
1852 ` : /*html*/`
1853 ${location.pathname.includes('/status/') ? `<br><br>` : ''}
1854 <span ${location.pathname.includes('/status/') ? `style="margin-top:-10px;" ` : ''}class="tweet-self-thread-line"></span>
1855 <div ${location.pathname.includes('/status/') ? `style="margin-top:-8px;" ` : ''}class="tweet-self-thread-line-dots"></div>
1856 `}
1857 </div>
1858 </div>
1859 `;
1860 // video
1861 let vidOverlay = tweet.getElementsByClassName('tweet-media-video-overlay')[0];
1862 if(vidOverlay) {
1863 vidOverlay.addEventListener('click', () => {
1864 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
1865 vid.play();
1866 vid.controls = true;
1867 vid.classList.remove('tweet-media-element-censor');
1868 vidOverlay.style.display = 'none';
1869 });
1870 }
1871 if(videos) {
1872 let videoErrors = 0;
1873 let vids = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO');
1874 vids[0].addEventListener('error', () => {
1875 if(videoErrors >= 3) return;
1876 videoErrors++;
1877 setTimeout(() => {
1878 vids[0].load();
1879 }, 25);
1880 })
1881 vids[0].onloadstart = () => {
1882 let src = vids[0].currentSrc;
1883 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => {
1884 if(el.dataset.url === src) el.classList.add('tweet-video-quality-current');
1885 });
1886 tweet.getElementsByClassName('tweet-video-reload')[0].addEventListener('click', () => {
1887 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
1888 let time = vid.currentTime;
1889 let paused = vid.paused;
1890 vid.load();
1891 vid.onloadstart = () => {
1892 let src = vid.currentSrc;
1893 vid.currentTime = time;
1894 if(!paused) vid.play();
1895 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => {
1896 if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current');
1897 else el.classList.remove('tweet-video-quality-current');
1898 });
1899 }
1900 });
1901 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => el.addEventListener('click', () => {
1902 if(el.className.includes('tweet-video-quality-current')) return;
1903 localStorage.preferredQuality = parseInt(el.innerText);
1904 let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
1905 let time = vid.currentTime;
1906 let paused = vid.paused;
1907 for(let v of videos) {
1908 let closestQuality = v.video_info.variants.filter(v => v.bitrate).reduce((prev, curr) => {
1909 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);
1910 });
1911 let preferredQualityVariantIndex = v.video_info.variants.findIndex(v => v.url === closestQuality.url);
1912 if(preferredQualityVariantIndex !== -1) {
1913 let preferredQualityVariant = v.video_info.variants[preferredQualityVariantIndex];
1914 v.video_info.variants.splice(preferredQualityVariantIndex, 1);
1915 v.video_info.variants.unshift(preferredQualityVariant);
1916 }
1917 }
1918 tweet.getElementsByClassName('tweet-media')[0].innerHTML = /*html*/`
1919 ${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' ? `
1920 ${m.video_info.variants.map(v => `<source src="${v.url}&ttd=${Date.now()}" type="${v.content_type}">`).join('\n')}
1921 ${LOC.unsupported_video.message}
1922 </video>` : ''}`).join('\n')}
1923 `;
1924 vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
1925 vid.onloadstart = () => {
1926 let src = vid.currentSrc;
1927 vid.currentTime = time;
1928 if(!paused) vid.play();
1929 Array.from(tweet.getElementsByClassName('tweet-video-quality')).forEach(el => {
1930 if(el.dataset.url === src.split('&ttd=')[0]) el.classList.add('tweet-video-quality-current');
1931 else el.classList.remove('tweet-video-quality-current');
1932 });
1933 }
1934 vid.addEventListener('mousedown', e => {
1935 if(e.button === 1) {
1936 e.preventDefault();
1937 window.open(vid.currentSrc, '_blank');
1938 }
1939 });
1940 }));
1941 };
1942 for(let vid of vids) {
1943 if(typeof vars.volume === 'number') {
1944 vid.volume = vars.volume;
1945 }
1946 vid.onvolumechange = () => {
1947 chrome.storage.sync.set({
1948 volume: vid.volume
1949 }, () => { });
1950 let allVids = document.getElementsByTagName('video');
1951 for(let i = 0; i < allVids.length; i++) {
1952 allVids[i].volume = vid.volume;
1953 }
1954 };
1955 vid.addEventListener('mousedown', e => {
1956 if(e.button === 1) {
1957 e.preventDefault();
1958 window.open(vid.currentSrc, '_blank');
1959 }
1960 });
1961 }
1962 }
1963
1964 let footerFavorites = tweet.getElementsByClassName('tweet-footer-favorites')[0];
1965 if(t.card) {
1966 generateCard(t, tweet, user);
1967 }
1968 if (options.top) {
1969 tweet.querySelector('.tweet-top').hidden = false;
1970 const icon = document.createElement('span');
1971 icon.innerText = options.top.icon;
1972 icon.classList.add('tweet-top-icon');
1973 icon.style.color = options.top.color;
1974
1975 const span = document.createElement("span");
1976 span.classList.add("tweet-top-text");
1977 span.innerHTML = options.top.text;
1978 if(options.top.class) {
1979 span.classList.add(options.top.class);
1980 tweet.classList.add(`tweet-top-${options.top.class}`);
1981 }
1982 tweet.querySelector('.tweet-top').append(icon, span);
1983 }
1984 if(options.mainTweet) {
1985 let likers = mainTweetLikers.slice(0, 8);
1986 for(let i in likers) {
1987 let liker = likers[i];
1988 let a = document.createElement('a');
1989 a.href = `https://twitter.com/${liker.screen_name}`;
1990 let likerImg = document.createElement('img');
1991 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}`;
1992 likerImg.classList.add('tweet-footer-favorites-img');
1993 likerImg.title = liker.name + ' (@' + liker.screen_name + ')';
1994 likerImg.width = 24;
1995 likerImg.height = 24;
1996 a.dataset.id = liker.id_str;
1997 a.appendChild(likerImg);
1998 footerFavorites.appendChild(a);
1999 }
2000 let likesLink = tweet.getElementsByClassName('tweet-footer-stat-f')[0];
2001 likesLink.addEventListener('click', e => {
2002 e.preventDefault();
2003 document.getElementById('loading-box').hidden = false;
2004 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/likes`);
2005 updateSubpage();
2006 mediaToUpload = [];
2007 linkColors = {};
2008 cursor = undefined;
2009 seenReplies = [];
2010 mainTweetLikers = [];
2011 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2012 updateLikes(id);
2013 renderDiscovery();
2014 renderTrends();
2015 currentLocation = location.pathname;
2016 });
2017 let retweetsLink = tweet.getElementsByClassName('tweet-footer-stat-r')[0];
2018 retweetsLink.addEventListener('click', e => {
2019 e.preventDefault();
2020 document.getElementById('loading-box').hidden = false;
2021 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`);
2022 updateSubpage();
2023 mediaToUpload = [];
2024 linkColors = {};
2025 cursor = undefined;
2026 seenReplies = [];
2027 mainTweetLikers = [];
2028 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2029 updateRetweets(id);
2030 renderDiscovery();
2031 renderTrends();
2032 currentLocation = location.pathname;
2033 });
2034 let repliesLink = tweet.getElementsByClassName('tweet-footer-stat-o')[0];
2035 repliesLink.addEventListener('click', e => {
2036 e.preventDefault();
2037 if(location.href === `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`) return;
2038 document.getElementById('loading-box').hidden = false;
2039 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`);
2040 updateSubpage();
2041 mediaToUpload = [];
2042 linkColors = {};
2043 cursor = undefined;
2044 seenReplies = [];
2045 mainTweetLikers = [];
2046 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2047 updateReplies(id);
2048 renderDiscovery();
2049 renderTrends();
2050 currentLocation = location.pathname;
2051 });
2052 }
2053 if(options.mainTweet && t.user.id_str !== user.id_str) {
2054 const tweetFollow = tweet.getElementsByClassName('tweet-header-follow')[0];
2055 tweetFollow.addEventListener('click', async () => {
2056 if(t.user.following) {
2057 try {
2058 await API.user.unfollow(t.user.screen_name);
2059 } catch(e) {
2060 console.error(e);
2061 alert(e);
2062 return;
2063 }
2064 tweetFollow.innerText = LOC.follow.message;
2065 tweetFollow.classList.remove('following');
2066 tweetFollow.classList.add('follow');
2067 t.user.following = false;
2068 } else {
2069 try {
2070 await API.user.follow(t.user.screen_name);
2071 } catch(e) {
2072 console.error(e);
2073 alert(e);
2074 return;
2075 }
2076 tweetFollow.innerText = LOC.unfollow.message;
2077 tweetFollow.classList.remove('follow');
2078 tweetFollow.classList.add('following');
2079 t.user.following = true;
2080 }
2081 });
2082 }
2083 const tweetBody = tweet.getElementsByClassName('tweet-body')[0];
2084 const tweetBodyText = tweet.getElementsByClassName('tweet-body-text')[0];
2085 const tweetTranslate = tweet.getElementsByClassName('tweet-translate')[0];
2086 const tweetTranslateAfter = tweet.getElementsByClassName('tweet-translate-after')[0];
2087 const tweetBodyQuote = tweet.getElementsByClassName('tweet-body-quote')[0];
2088 const tweetMediaQuote = tweet.getElementsByClassName('tweet-media-quote')[0];
2089 const tweetBodyQuoteText = tweet.getElementsByClassName('tweet-body-text-quote')[0];
2090 const tweetDeleteBookmark = tweet.getElementsByClassName('tweet-delete-bookmark')[0];
2091
2092 const tweetReplyCancel = tweet.getElementsByClassName('tweet-reply-cancel')[0];
2093 const tweetReplyUpload = tweet.getElementsByClassName('tweet-reply-upload')[0];
2094 const tweetReplyAddEmoji = tweet.getElementsByClassName('tweet-reply-add-emoji')[0];
2095 const tweetReply = tweet.getElementsByClassName('tweet-reply')[0];
2096 const tweetReplyButton = tweet.getElementsByClassName('tweet-reply-button')[0];
2097 const tweetReplyError = tweet.getElementsByClassName('tweet-reply-error')[0];
2098 const tweetReplyText = tweet.getElementsByClassName('tweet-reply-text')[0];
2099 const tweetReplyChar = tweet.getElementsByClassName('tweet-reply-char')[0];
2100 const tweetReplyMedia = tweet.getElementsByClassName('tweet-reply-media')[0];
2101
2102 const tweetInteract = tweet.getElementsByClassName('tweet-interact')[0];
2103 const tweetInteractReply = tweet.getElementsByClassName('tweet-interact-reply')[0];
2104 const tweetInteractRetweet = tweet.getElementsByClassName('tweet-interact-retweet')[0];
2105 const tweetInteractFavorite = tweet.getElementsByClassName('tweet-interact-favorite')[0];
2106 const tweetInteractBookmark = tweet.getElementsByClassName('tweet-interact-bookmark')[0];
2107 const tweetInteractMore = tweet.getElementsByClassName('tweet-interact-more')[0];
2108
2109 const tweetFooter = tweet.getElementsByClassName('tweet-footer')[0];
2110 const tweetFooterReplies = tweet.getElementsByClassName('tweet-footer-stat-replies')[0];
2111 const tweetFooterRetweets = tweet.getElementsByClassName('tweet-footer-stat-retweets')[0];
2112 const tweetFooterFavorites = tweet.getElementsByClassName('tweet-footer-stat-favorites')[0];
2113
2114 const tweetQuote = tweet.getElementsByClassName('tweet-quote')[0];
2115 const tweetQuoteCancel = tweet.getElementsByClassName('tweet-quote-cancel')[0];
2116 const tweetQuoteUpload = tweet.getElementsByClassName('tweet-quote-upload')[0];
2117 const tweetQuoteAddEmoji = tweet.getElementsByClassName('tweet-quote-add-emoji')[0];
2118 const tweetQuoteButton = tweet.getElementsByClassName('tweet-quote-button')[0];
2119 const tweetQuoteError = tweet.getElementsByClassName('tweet-quote-error')[0];
2120 const tweetQuoteText = tweet.getElementsByClassName('tweet-quote-text')[0];
2121 const tweetQuoteChar = tweet.getElementsByClassName('tweet-quote-char')[0];
2122 const tweetQuoteMedia = tweet.getElementsByClassName('tweet-quote-media')[0];
2123
2124 const tweetInteractRetweetMenu = tweet.getElementsByClassName('tweet-interact-retweet-menu')[0];
2125 const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweet')[0];
2126 const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName('tweet-interact-retweet-menu-quote')[0];
2127 const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName('tweet-interact-retweet-menu-quotes')[0];
2128 const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName('tweet-interact-retweet-menu-retweeters')[0];
2129
2130 const tweetInteractMoreMenu = tweet.getElementsByClassName('tweet-interact-more-menu')[0];
2131 const tweetInteractMoreMenuCopy = tweet.getElementsByClassName('tweet-interact-more-menu-copy')[0];
2132 const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-tweet-id')[0];
2133 const tweetInteractMoreMenuLog = tweet.getElementsByClassName('tweet-interact-more-menu-log')[0];
2134 const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName('tweet-interact-more-menu-copy-user-id')[0];
2135 const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName('tweet-interact-more-menu-embed')[0];
2136 const tweetInteractMoreMenuShare = tweet.getElementsByClassName('tweet-interact-more-menu-share')[0];
2137 const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName('tweet-interact-more-menu-newtwitter')[0];
2138 const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName('tweet-interact-more-menu-analytics')[0];
2139 const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName('tweet-interact-more-menu-refresh')[0];
2140 const tweetInteractMoreMenuMute = tweet.getElementsByClassName('tweet-interact-more-menu-mute')[0];
2141 const tweetInteractMoreMenuDownload = tweet.getElementsByClassName('tweet-interact-more-menu-download')[0];
2142 const tweetInteractMoreMenuDownloadGifs = Array.from(tweet.getElementsByClassName('tweet-interact-more-menu-download-gif'));
2143 const tweetInteractMoreMenuDelete = tweet.getElementsByClassName('tweet-interact-more-menu-delete')[0];
2144 const tweetInteractMoreMenuPin = tweet.getElementsByClassName('tweet-interact-more-menu-pin')[0];
2145 const tweetInteractMoreMenuFollow = tweet.getElementsByClassName('tweet-interact-more-menu-follow')[0];
2146 const tweetInteractMoreMenuBlock = tweet.getElementsByClassName('tweet-interact-more-menu-block')[0];
2147 const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName('tweet-interact-more-menu-bookmark')[0];
2148 const tweetInteractMoreMenuFeedbacks = Array.from(tweet.getElementsByClassName('tweet-interact-more-menu-feedback'));
2149 const tweetInteractMoreMenuHide = tweet.getElementsByClassName('tweet-interact-more-menu-hide')[0];
2150
2151 if(tweetInteractMoreMenuLog) tweetInteractMoreMenuLog.addEventListener('click', () => {
2152 console.log(t);
2153 });
2154
2155 // moderating tweets
2156 if(tweetInteractMoreMenuHide) tweetInteractMoreMenuHide.addEventListener('click', async () => {
2157 if(t.moderated) {
2158 try {
2159 await API.tweet.unmoderate(t.id_str);
2160 } catch(e) {
2161 console.error(e);
2162 alert(e);
2163 return;
2164 }
2165 tweetInteractMoreMenuHide.innerText = LOC.hide_tweet.message;
2166 t.moderated = false;
2167 } else {
2168 let sure = confirm(LOC.hide_tweet_sure.message);
2169 if(!sure) return;
2170 try {
2171 await API.tweet.moderate(t.id_str);
2172 } catch(e) {
2173 console.error(e);
2174 alert(e);
2175 return;
2176 }
2177 tweetInteractMoreMenuHide.innerText = LOC.unhide_tweet.message;
2178 t.moderated = true;
2179 }
2180 });
2181
2182 // community notes
2183 if(t.birdwatch && !vars.hideCommunityNotes) {
2184 if(t.birdwatch.subtitle) {
2185 let div = document.createElement('div');
2186 div.classList.add('tweet-birdwatch', 'box');
2187 let text = Array.from(escapeHTML(t.birdwatch.subtitle.text));
2188 for(let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) {
2189 let entity = t.birdwatch.subtitle.entities[e];
2190 if(!entity.ref) continue;
2191 text = arrayInsert(text, entity.toIndex, '</a>');
2192 text = arrayInsert(text, entity.fromIndex, `<a href="${entity.ref.url}" target="_blank">`);
2193 }
2194 text = text.join('');
2195
2196 div.innerHTML = /*html*/`
2197 <div class="tweet-birdwatch-header">
2198 <span class="tweet-birdwatch-title">${escapeHTML(t.birdwatch.title)}</span>
2199 </div>
2200 <div class="tweet-birdwatch-body">
2201 <span class="tweet-birdwatch-subtitle">${text}</span>
2202 </div>
2203 `;
2204
2205 if(tweetFooter) tweetFooter.before(div);
2206 else tweetInteract.before(div);
2207 }
2208 }
2209
2210 // rtl languages
2211 if(rtlLanguages.includes(t.lang)) {
2212 tweetBody.classList.add('rtl');
2213 }
2214
2215 // Quote body
2216 if(tweetMediaQuote) tweetMediaQuote.addEventListener('click', e => {
2217 if(e && e.target && e.target.tagName === "VIDEO") {
2218 e.preventDefault();
2219 e.stopPropagation();
2220 e.stopImmediatePropagation();
2221 if(e.target.paused) {
2222 e.target.play();
2223 } else {
2224 e.target.pause();
2225 }
2226 }
2227 });
2228 if(tweetBodyQuote) {
2229 if(typeof mainTweetLikers !== 'undefined') {
2230 tweetBodyQuote.addEventListener('click', e => {
2231 e.preventDefault();
2232 document.getElementById('loading-box').hidden = false;
2233 history.pushState({}, null, `https://twitter.com/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`);
2234 updateSubpage();
2235 mediaToUpload = [];
2236 linkColors = {};
2237 cursor = undefined;
2238 seenReplies = [];
2239 mainTweetLikers = [];
2240 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2241 if(subpage === 'tweet') {
2242 updateReplies(id);
2243 } else if(subpage === 'likes') {
2244 updateLikes(id);
2245 } else if(subpage === 'retweets') {
2246 updateRetweets(id);
2247 } else if(subpage === 'retweets_with_comments') {
2248 updateRetweetsWithComments(id);
2249 }
2250 renderDiscovery();
2251 renderTrends();
2252 currentLocation = location.pathname;
2253 });
2254 } else {
2255 tweetBodyQuote.addEventListener('click', e => {
2256 e.preventDefault();
2257 if(e.target.className && e.target.className.includes('tweet-media-element')) {
2258 if(!e.target.src.endsWith('?name=orig') && !e.target.src.startsWith('data:')) {
2259 e.target.src += '?name=orig';
2260 }
2261 new Viewer(e.target, {
2262 transition: false
2263 });
2264 e.target.click();
2265 return;
2266 }
2267 new TweetViewer(user, t.quoted_status);
2268 });
2269 }
2270 if(rtlLanguages.includes(t.quoted_status.lang)) {
2271 tweetBodyQuoteText.classList.add('rtl');
2272 } else {
2273 tweetBodyQuoteText.classList.add('ltr');
2274 }
2275 }
2276 if(tweetTranslate || tweetTranslateAfter) if(options.translate || vars.autotranslateProfiles.includes(t.user.id_str) || (typeof toAutotranslate !== 'undefined' && toAutotranslate)) {
2277 onVisible(tweet, () => {
2278 if(!t.translated) {
2279 if(tweetTranslate) tweetTranslate.click();
2280 else if(tweetTranslateAfter) tweetTranslateAfter.click();
2281 }
2282 })
2283 }
2284
2285 // Translate
2286 t.translated = false;
2287 if(tweetTranslate || tweetTranslateAfter) (tweetTranslate ? tweetTranslate : tweetTranslateAfter).addEventListener('click', async () => {
2288 if(t.translated) return;
2289 let translated = await API.tweet.translate(t.id_str);
2290 t.translated = true;
2291 (tweetTranslate ? tweetTranslate : tweetTranslateAfter).hidden = true;
2292 let translatedMessage;
2293 if(LOC.translated_from.message.includes("$LANGUAGE$")) {
2294 translatedMessage = LOC.translated_from.message.replace("$LANGUAGE$", `[${translated.translated_lang}]`);
2295 } else {
2296 translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`;
2297 }
2298 tweetBodyText.innerHTML += `<br>`+
2299 `<span style="font-size: 12px;color: var(--light-gray);">${translatedMessage}:</span>`+
2300 `<br>`+
2301 `<span class="tweet-translated-text">${await renderTweetBodyHTML(translated.text, translated.entities)}</span>`;
2302 if(vars.enableTwemoji) twemoji.parse(tweetBodyText);
2303 });
2304
2305 // Bookmarks
2306 let switchingBookmark = false;
2307 let switchBookmark = () => {
2308 if(switchingBookmark) return;
2309 switchingBookmark = true;
2310 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2311 if(t.bookmarked) {
2312 API.bookmarks.delete(t.id_str).then(() => {
2313 toast.info(LOC.unbookmarked_tweet.message);
2314 switchingBookmark = false;
2315 if(tweetDeleteBookmark) {
2316 tweet.remove();
2317 if(timelineContainer.children.length === 0) {
2318 timelineContainer.innerHTML = `<div style="color:var(--light-gray)">${LOC.empty.message}</div>`;
2319 document.getElementById('delete-all').hidden = true;
2320 }
2321 return;
2322 }
2323 t.bookmarked = false;
2324 t.bookmark_count--;
2325 tweetInteractMoreMenuBookmark.innerText = LOC.bookmark_tweet.message;
2326 if(tweetInteractBookmark) {
2327 tweetInteractBookmark.classList.remove('tweet-interact-bookmarked');
2328 if(vars.bookmarkButton !== 'show_all_no_count') {
2329 tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ',');
2330 }
2331 }
2332 }).catch(e => {
2333 switchingBookmark = false;
2334 console.error(e);
2335 alert(e);
2336 });
2337 } else {
2338 API.bookmarks.create(t.id_str).then(() => {
2339 toast.info(LOC.bookmarked_tweet.message);
2340 switchingBookmark = false;
2341 t.bookmarked = true;
2342 t.bookmark_count++;
2343 tweetInteractMoreMenuBookmark.innerText = LOC.remove_bookmark.message;
2344 if(tweetInteractBookmark) {
2345 tweetInteractBookmark.classList.add('tweet-interact-bookmarked');
2346 if(vars.bookmarkButton !== 'show_all_no_count') {
2347 tweetInteractBookmark.innerText = Number(t.bookmark_count).toLocaleString().replace(/\s/g, ',');
2348 }
2349 }
2350 }).catch(e => {
2351 switchingBookmark = false;
2352 console.error(e);
2353 alert(e);
2354 });
2355 }
2356 };
2357 if(tweetInteractBookmark) tweetInteractBookmark.addEventListener('click', switchBookmark);
2358 if(tweetInteractMoreMenuBookmark) tweetInteractMoreMenuBookmark.addEventListener('click', switchBookmark);
2359 if(tweetDeleteBookmark) tweetDeleteBookmark.addEventListener('click', async () => {
2360 await API.bookmarks.delete(t.id_str);
2361 tweet.remove();
2362 if(timelineContainer.children.length === 0) {
2363 timelineContainer.innerHTML = `<div style="color:var(--light-gray)">${LOC.empty.message}</div>`;
2364 document.getElementById('delete-all').hidden = true;
2365 }
2366 });
2367
2368 // Media
2369 if (t.extended_entities && t.extended_entities.media) {
2370 const tweetMedia = tweet.getElementsByClassName('tweet-media')[0];
2371 tweetMedia.addEventListener('click', e => {
2372 if (e.target.className && e.target.className.includes('tweet-media-element-censor')) {
2373 return e.target.classList.remove('tweet-media-element-censor');
2374 }
2375 if (e.target.tagName === 'IMG') {
2376 if(!e.target.src.endsWith('?name=orig') && !e.target.src.startsWith('data:')) {
2377 e.target.src += '?name=orig';
2378 }
2379 new Viewer(tweetMedia, {
2380 transition: false
2381 });
2382 e.target.click();
2383 }
2384 });
2385 if(typeof pageUser !== 'undefined' && !location.pathname.includes("/likes")) {
2386 let profileMediaDiv = document.getElementById('profile-media-div');
2387 if(!options || !options.top || !options.top.text || !options.top.text.includes('retweeted')) t.extended_entities.media.forEach(m => {
2388 if(profileMediaDiv.children.length >= 6) return;
2389 let ch = Array.from(profileMediaDiv.children);
2390 if(ch.find(c => c.src === m.media_url_https)) return;
2391 const media = document.createElement('img');
2392 media.classList.add('tweet-media-element', 'tweet-media-element-four', 'profile-media-preview');
2393 if(!vars.displaySensitiveContent && t.possibly_sensitive) media.classList.add('tweet-media-element-censor');
2394 media.src = m.media_url_https;
2395 if(m.ext_alt_text) media.alt = m.ext_alt_text;
2396 media.addEventListener('click', async () => {
2397 if(subpage !== 'profile' && subpage !== 'media') {
2398 document.getElementById('profile-stat-tweets-link').click();
2399 while(!document.getElementsByClassName('tweet-id-' + t.id_str)[0]) await sleep(100);
2400 }
2401 document.getElementsByClassName('tweet-id-' + t.id_str)[0].scrollIntoView({behavior: 'smooth', block: 'center'});
2402 });
2403 profileMediaDiv.appendChild(media);
2404 });
2405 }
2406 }
2407
2408 // Emojis
2409 [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach(e => {
2410 e.addEventListener('click', e => {
2411 let isReply = e.target.className === 'tweet-reply-add-emoji';
2412 createEmojiPicker(isReply ? tweetReply : tweetQuote, isReply ? tweetReplyText : tweetQuoteText, {});
2413 });
2414 });
2415
2416 // Reply
2417 tweetReplyCancel.addEventListener('click', () => {
2418 tweetReply.hidden = true;
2419 tweetInteractReply.classList.remove('tweet-interact-reply-clicked');
2420 });
2421 let replyMedia = [];
2422 tweetReply.addEventListener('drop', e => {
2423 handleDrop(e, replyMedia, tweetReplyMedia);
2424 });
2425 tweetReply.addEventListener('paste', event => {
2426 let items = (event.clipboardData || event.originalEvent.clipboardData).items;
2427 for (let index in items) {
2428 let item = items[index];
2429 if (item.kind === 'file') {
2430 let file = item.getAsFile();
2431 handleFiles([file], replyMedia, tweetReplyMedia);
2432 }
2433 }
2434 });
2435 tweetReplyUpload.addEventListener('click', () => {
2436 getMedia(replyMedia, tweetReplyMedia);
2437 tweetReplyText.focus();
2438 });
2439 tweetInteractReply.addEventListener('click', () => {
2440 if(options.mainTweet) {
2441 document.getElementById('new-tweet').click();
2442 document.getElementById('new-tweet-text').focus();
2443 return;
2444 }
2445 if (!tweetQuote.hidden) tweetQuote.hidden = true;
2446 if (tweetReply.hidden) {
2447 tweetInteractReply.classList.add('tweet-interact-reply-clicked');
2448 } else {
2449 tweetInteractReply.classList.remove('tweet-interact-reply-clicked');
2450 }
2451 tweetReply.hidden = !tweetReply.hidden;
2452 setTimeout(() => {
2453 tweetReplyText.focus();
2454 })
2455 });
2456 tweetReplyText.addEventListener('keydown', e => {
2457 if (e.key === 'Enter' && e.ctrlKey) {
2458 tweetReplyButton.click();
2459 }
2460 });
2461 tweetReplyText.addEventListener('input', e => {
2462 let text = tweetReplyText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim();
2463 tweetReplyChar.innerText = `${text.length}/280`;
2464 if(text.length > 265) {
2465 tweetReplyChar.style.color = "#c26363";
2466 } else {
2467 tweetReplyChar.style.color = "";
2468 }
2469 if (text.length > 280) {
2470 tweetReplyChar.style.color = "red";
2471 tweetReplyButton.disabled = true;
2472 } else {
2473 tweetReplyButton.disabled = false;
2474 }
2475 });
2476 tweetReplyButton.addEventListener('click', async () => {
2477 tweetReplyError.innerHTML = '';
2478 let text = tweetReplyText.value;
2479 if (text.length === 0 && replyMedia.length === 0) return;
2480 tweetReplyButton.disabled = true;
2481 let uploadedMedia = [];
2482 for (let i in replyMedia) {
2483 let media = replyMedia[i];
2484 try {
2485 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false;
2486 let mediaId = await API.uploadMedia({
2487 media_type: media.type,
2488 media_category: media.category,
2489 media: media.data,
2490 alt: media.alt,
2491 cw: media.cw,
2492 loadCallback: data => {
2493 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`;
2494 }
2495 });
2496 uploadedMedia.push(mediaId);
2497 } catch (e) {
2498 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true;
2499 console.error(e);
2500 alert(e);
2501 }
2502 }
2503 let tweetObject = {
2504 status: text,
2505 in_reply_to_status_id: t.id_str
2506 };
2507 if (uploadedMedia.length > 0) {
2508 tweetObject.media_ids = uploadedMedia.join(',');
2509 }
2510 let tweetData;
2511 try {
2512 tweetData = await API.tweet.postV2(tweetObject)
2513 } catch (e) {
2514 tweetReplyError.innerHTML = (e && e.message ? e.message : e) + "<br>";
2515 tweetReplyButton.disabled = false;
2516 return;
2517 }
2518 if (!tweetData) {
2519 tweetReplyButton.disabled = false;
2520 tweetReplyError.innerHTML = `${LOC.error_sending_tweet.message}<br>`;
2521 return;
2522 }
2523 tweetReplyChar.innerText = '0/280';
2524 tweetReplyText.value = '';
2525 tweetReply.hidden = true;
2526 tweetInteractReply.classList.remove('tweet-interact-reply-clicked');
2527 if(!options.mainTweet) {
2528 tweetInteractReply.dataset.val = parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1;
2529 tweetInteractReply.innerText = Number(parseInt(tweetInteractReply.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');
2530 } else {
2531 tweetFooterReplies.dataset.val = parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1;
2532 tweetFooterReplies.innerText = Number(parseInt(tweetFooterReplies.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');
2533 }
2534 tweetData._ARTIFICIAL = true;
2535 if(typeof timeline !== 'undefined') {
2536 timeline.data.unshift(tweetData);
2537 }
2538 if(tweet.getElementsByClassName('tweet-self-thread-div')[0]) tweet.getElementsByClassName('tweet-self-thread-div')[0].hidden = false;
2539 tweetReplyButton.disabled = false;
2540 tweetReplyMedia.innerHTML = [];
2541 replyMedia = [];
2542 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2543 appendTweet(tweetData, document.getElementById('timeline'), {
2544 noTop: true,
2545 after: tweet
2546 });
2547 });
2548
2549 // Retweet / Quote Tweet
2550 let retweetClicked = false;
2551 tweetQuoteCancel.addEventListener('click', () => {
2552 tweetQuote.hidden = true;
2553 });
2554 tweetInteractRetweet.addEventListener('click', async () => {
2555 if(tweetInteractRetweet.classList.contains('tweet-interact-retweet-disabled')) {
2556 return;
2557 }
2558 if (!tweetQuote.hidden) {
2559 tweetQuote.hidden = true;
2560 return;
2561 }
2562 if (tweetInteractRetweetMenu.hidden) {
2563 tweetInteractRetweetMenu.hidden = false;
2564 }
2565 if(retweetClicked) return;
2566 retweetClicked = true;
2567 setTimeout(() => {
2568 document.body.addEventListener('click', () => {
2569 retweetClicked = false;
2570 setTimeout(() => tweetInteractRetweetMenu.hidden = true, 50);
2571 }, { once: true });
2572 }, 50);
2573 });
2574 t.renderRetweetsUp = (tweetData) => {
2575 tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message;
2576 tweetInteractRetweet.classList.add('tweet-interact-retweeted');
2577 t.retweeted = true;
2578 t.newTweetId = tweetData.id_str;
2579 if(!options.mainTweet) {
2580 tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1;
2581 tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');
2582 } else {
2583 tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');
2584 }
2585 }
2586 t.renderRetweetsDown = () => {
2587 tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message;
2588 tweetInteractRetweet.classList.remove('tweet-interact-retweeted');
2589 t.retweeted = false;
2590 if(!options.mainTweet) {
2591 tweetInteractRetweet.dataset.val = parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1;
2592 tweetInteractRetweet.innerText = Number(parseInt(tweetInteractRetweet.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ',');
2593 } else {
2594 tweetFooterRetweets.innerText = Number(parseInt(tweetFooterRetweets.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ',');
2595 }
2596 delete t.newTweetId;
2597 }
2598 tweetInteractRetweetMenuRetweet.addEventListener('click', async () => {
2599 if (!t.retweeted) {
2600 let tweetData;
2601 try {
2602 tweetData = await API.tweet.retweet(t.id_str);
2603 } catch (e) {
2604 console.error(e);
2605 return;
2606 }
2607 if (!tweetData) {
2608 return;
2609 }
2610 t.renderRetweetsUp(tweetData);
2611 } else {
2612 let tweetData;
2613 try {
2614 tweetData = await API.tweet.unretweet(t.retweeted_status ? t.retweeted_status.id_str : t.id_str);
2615 } catch (e) {
2616 console.error(e);
2617 return;
2618 }
2619 if (!tweetData) {
2620 return;
2621 }
2622 if(t.current_user_retweet) {
2623 if(options.top && options.top.icon && options.top.icon === "\uf006") {
2624 tweet.remove();
2625 if(typeof timeline !== 'undefined') {
2626 let index = timeline.data.findIndex((tweet) => tweet.retweeted_status && tweet.retweeted_status.id_str === t.id_str && !tweet.current_user_retweet);
2627 if(index > -1) {
2628 timeline.data.splice(index, 1);
2629 let originalTweet = timeline.data.find((tweet) => tweet.id_str === t.id_str);
2630 if(originalTweet) {
2631 delete originalTweet.current_user_retweet;
2632 originalTweet.renderRetweetsDown();
2633 }
2634 }
2635 }
2636 } else {
2637 let retweetedElement = Array.from(document.getElementsByClassName('tweet')).find(te => te.dataset.tweetId === t.id_str && te.getElementsByClassName('retweet-label')[0]);
2638 if(retweetedElement) {
2639 retweetedElement.remove();
2640 }
2641 if(typeof timeline !== 'undefined') {
2642 let index = timeline.data.findIndex((tweet) => tweet.retweeted_status && tweet.retweeted_status.id_str === t.id_str && !tweet.current_user_retweet);
2643 if(index > -1) {
2644 timeline.data.splice(index, 1);
2645 }
2646 }
2647 }
2648 }
2649 t.renderRetweetsDown();
2650 }
2651 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2652 });
2653 if(options.mainTweet) {
2654 tweetInteractRetweetMenuQuotes.addEventListener('click', async () => {
2655 document.getElementById('loading-box').hidden = false;
2656 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`);
2657 updateSubpage();
2658 mediaToUpload = [];
2659 linkColors = {};
2660 cursor = undefined;
2661 seenReplies = [];
2662 mainTweetLikers = [];
2663 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2664 if(subpage === 'tweet') {
2665 updateReplies(id);
2666 } else if(subpage === 'likes') {
2667 updateLikes(id);
2668 } else if(subpage === 'retweets') {
2669 updateRetweets(id);
2670 } else if(subpage === 'retweets_with_comments') {
2671 updateRetweetsWithComments(id);
2672 }
2673 renderDiscovery();
2674 renderTrends();
2675 currentLocation = location.pathname;
2676 });
2677 tweetInteractRetweetMenuRetweeters.addEventListener('click', async () => {
2678 document.getElementById('loading-box').hidden = false;
2679 history.pushState({}, null, `https://twitter.com/${t.user.screen_name}/status/${t.id_str}/retweets`);
2680 updateSubpage();
2681 mediaToUpload = [];
2682 linkColors = {};
2683 cursor = undefined;
2684 seenReplies = [];
2685 mainTweetLikers = [];
2686 let id = location.pathname.match(/status\/(\d{1,32})/)[1];
2687 if(subpage === 'tweet') {
2688 updateReplies(id);
2689 } else if(subpage === 'likes') {
2690 updateLikes(id);
2691 } else if(subpage === 'retweets') {
2692 updateRetweets(id);
2693 } else if(subpage === 'retweets_with_comments') {
2694 updateRetweetsWithComments(id);
2695 }
2696 renderDiscovery();
2697 renderTrends();
2698 currentLocation = location.pathname;
2699 });
2700 }
2701 tweetInteractRetweetMenuQuote.addEventListener('click', async () => {
2702 if (!tweetReply.hidden) {
2703 tweetInteractReply.classList.remove('tweet-interact-reply-clicked');
2704 tweetReply.hidden = true;
2705 }
2706 tweetQuote.hidden = false;
2707 setTimeout(() => {
2708 tweetQuoteText.focus();
2709 })
2710 });
2711 let quoteMedia = [];
2712 tweetQuote.addEventListener('drop', e => {
2713 handleDrop(e, quoteMedia, tweetQuoteMedia);
2714 });
2715 tweetQuote.addEventListener('paste', event => {
2716 let items = (event.clipboardData || event.originalEvent.clipboardData).items;
2717 for (let index in items) {
2718 let item = items[index];
2719 if (item.kind === 'file') {
2720 let file = item.getAsFile();
2721 handleFiles([file], quoteMedia, tweetQuoteMedia);
2722 }
2723 }
2724 });
2725 tweetQuoteUpload.addEventListener('click', () => {
2726 getMedia(quoteMedia, tweetQuoteMedia);
2727 });
2728 tweetQuoteText.addEventListener('keydown', e => {
2729 if (e.key === 'Enter' && e.ctrlKey) {
2730 tweetQuoteButton.click();
2731 }
2732 });
2733 tweetQuoteText.addEventListener('input', e => {
2734 let text = tweetQuoteText.value.replace(linkRegex, ' https://t.co/xxxxxxxxxx').trim();
2735 tweetQuoteChar.innerText = `${text.length}/280`;
2736 if(text.length > 265) {
2737 tweetQuoteChar.style.color = "#c26363";
2738 } else {
2739 tweetQuoteChar.style.color = "";
2740 }
2741 if (text.length > 280) {
2742 tweetQuoteChar.style.color = "red";
2743 tweetQuoteButton.disabled = true;
2744 } else {
2745 tweetQuoteButton.disabled = false;
2746 }
2747 });
2748 tweetQuoteButton.addEventListener('click', async () => {
2749 let text = tweetQuoteText.value;
2750 tweetQuoteError.innerHTML = '';
2751 if (text.length === 0 && quoteMedia.length === 0) return;
2752 tweetQuoteButton.disabled = true;
2753 let uploadedMedia = [];
2754 for (let i in quoteMedia) {
2755 let media = quoteMedia[i];
2756 try {
2757 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = false;
2758 let mediaId = await API.uploadMedia({
2759 media_type: media.type,
2760 media_category: media.category,
2761 media: media.data,
2762 alt: media.alt,
2763 cw: media.cw,
2764 loadCallback: data => {
2765 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].innerText = `${data.text} (${data.progress}%)`;
2766 }
2767 });
2768 uploadedMedia.push(mediaId);
2769 } catch (e) {
2770 media.div.getElementsByClassName('new-tweet-media-img-progress')[0].hidden = true;
2771 console.error(e);
2772 alert(e);
2773 }
2774 }
2775 let tweetObject = {
2776 status: text,
2777 attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`
2778 };
2779 if (uploadedMedia.length > 0) {
2780 tweetObject.media_ids = uploadedMedia.join(',');
2781 }
2782 let tweetData;
2783 try {
2784 tweetData = await API.tweet.postV2(tweetObject)
2785 } catch (e) {
2786 tweetQuoteError.innerHTML = (e && e.message ? e.message : e) + "<br>";
2787 tweetQuoteButton.disabled = false;
2788 return;
2789 }
2790 if (!tweetData) {
2791 tweetQuoteError.innerHTML = `${LOC.error_sending_tweet}<br>`;
2792 tweetQuoteButton.disabled = false;
2793 return;
2794 }
2795 tweetQuoteText.value = '';
2796 tweetQuoteChar.innerText = '0/280';
2797 tweetQuote.hidden = true;
2798 tweetData._ARTIFICIAL = true;
2799 quoteMedia = [];
2800 tweetQuoteButton.disabled = false;
2801 tweetQuoteMedia.innerHTML = '';
2802 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2803 if(typeof timeline !== 'undefined') timeline.data.unshift(tweetData);
2804 else appendTweet(tweetData, timelineContainer, { prepend: true });
2805 });
2806
2807 // Favorite
2808 t.renderFavoritesDown = () => {
2809 t.favorited = false;
2810 t.favorite_count--;
2811 if(!options.mainTweet) {
2812 tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1;
2813 tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ',');;
2814 } else {
2815 if(mainTweetLikers.find(liker => liker.id_str === user.id_str)) {
2816 mainTweetLikers.splice(mainTweetLikers.findIndex(liker => liker.id_str === user.id_str), 1);
2817 let likerImg = footerFavorites.querySelector(`a[data-id="${user.id_str}"]`);
2818 if(likerImg) likerImg.remove()
2819 }
2820 tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) - 1).toLocaleString().replace(/\s/g, ',');
2821 }
2822 tweetInteractFavorite.classList.remove('tweet-interact-favorited');
2823 }
2824 t.renderFavoritesUp = () => {
2825 t.favorited = true;
2826 t.favorite_count++;
2827 if(!options.mainTweet) {
2828 tweetInteractFavorite.dataset.val = parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1;
2829 tweetInteractFavorite.innerText = Number(parseInt(tweetInteractFavorite.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');;
2830 } else {
2831 if(footerFavorites.children.length < 8 && !mainTweetLikers.find(liker => liker.id_str === user.id_str)) {
2832 let a = document.createElement('a');
2833 a.href = `https://twitter.com/${user.screen_name}`;
2834 let likerImg = document.createElement('img');
2835 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}`;
2836 likerImg.classList.add('tweet-footer-favorites-img');
2837 likerImg.title = user.name + ' (@' + user.screen_name + ')';
2838 likerImg.width = 24;
2839 likerImg.height = 24;
2840 a.dataset.id = user.id_str;
2841 a.appendChild(likerImg);
2842 footerFavorites.appendChild(a);
2843 mainTweetLikers.push(user);
2844 }
2845 tweetFooterFavorites.innerText = Number(parseInt(tweetFooterFavorites.innerText.replace(/\s/g, '').replace(/,/g, '').replace(/\./g, '')) + 1).toLocaleString().replace(/\s/g, ',');
2846 }
2847 tweetInteractFavorite.classList.add('tweet-interact-favorited');
2848 }
2849 tweetInteractFavorite.addEventListener('click', () => {
2850 if (t.favorited) {
2851 API.tweet.unfavorite(t.id_str).catch(e => {
2852 console.error(e);
2853 alert(e);
2854 t.renderFavoritesUp();
2855 });
2856 t.renderFavoritesDown();
2857 } else {
2858 API.tweet.favorite(t.id_str).catch(e => {
2859 console.error(e);
2860 alert(e);
2861 t.renderFavoritesDown();
2862 });
2863 t.renderFavoritesUp();
2864 }
2865 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2866 });
2867
2868 // More
2869 let moreClicked = false;
2870 tweetInteractMore.addEventListener('click', () => {
2871 if (tweetInteractMoreMenu.hidden) {
2872 tweetInteractMoreMenu.hidden = false;
2873 }
2874 if(moreClicked) return;
2875 moreClicked = true;
2876 setTimeout(() => {
2877 document.body.addEventListener('click', () => {
2878 moreClicked = false;
2879 setTimeout(() => tweetInteractMoreMenu.hidden = true, 50);
2880 }, { once: true });
2881 }, 50);
2882 });
2883 if(tweetInteractMoreMenuFollow) tweetInteractMoreMenuFollow.addEventListener('click', async () => {
2884 if (t.user.following) {
2885 try {
2886 await API.user.unfollow(t.user.screen_name);
2887 } catch(e) {
2888 console.error(e);
2889 alert(e);
2890 return;
2891 }
2892 t.user.following = false;
2893 tweetInteractMoreMenuFollow.innerText = followUserText;
2894 let event = new CustomEvent('tweetAction', { detail: {
2895 action: 'unfollow',
2896 tweet: t
2897 } });
2898 document.dispatchEvent(event);
2899 } else {
2900 try {
2901 await API.user.follow(t.user.screen_name);
2902 } catch(e) {
2903 console.error(e);
2904 alert(e);
2905 return;
2906 }
2907 t.user.following = true;
2908 tweetInteractMoreMenuFollow.innerText = unfollowUserText;
2909 let event = new CustomEvent('tweetAction', { detail: {
2910 action: 'follow',
2911 tweet: t
2912 } });
2913 document.dispatchEvent(event);
2914 }
2915 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2916 });
2917 if(tweetInteractMoreMenuBlock) tweetInteractMoreMenuBlock.addEventListener('click', async () => {
2918 if (t.user.blocking) {
2919 await API.user.unblock(t.user.id_str);
2920 t.user.blocking = false;
2921 if(LOC.block_user.message.includes("$SCREEN_NAME$")) {
2922 tweetInteractMoreMenuBlock.innerText = LOC.block_user.message.replace("$SCREEN_NAME$", t.user.screen_name);
2923 } else {
2924 tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`;
2925 }
2926 tweetInteractMoreMenuFollow.hidden = false;
2927 let event = new CustomEvent('tweetAction', { detail: {
2928 action: 'unblock',
2929 tweet: t
2930 } });
2931 document.dispatchEvent(event);
2932 } else {
2933 let blockMessage;
2934 if(LOC.block_sure.message.includes("$SCREEN_NAME$")) {
2935 blockMessage = LOC.block_sure.message.replace("$SCREEN_NAME$", t.user.screen_name);
2936 } else {
2937 blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`;
2938 }
2939 let c = confirm(blockMessage);
2940 if (!c) return;
2941 await API.user.block(t.user.id_str);
2942 t.user.blocking = true;
2943 if(LOC.unblock_user.message.includes("$SCREEN_NAME$")) {
2944 tweetInteractMoreMenuBlock.innerText = LOC.unblock_user.message.replace("$SCREEN_NAME$", t.user.screen_name);
2945 } else {
2946 tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`;
2947 }
2948 tweetInteractMoreMenuFollow.hidden = true;
2949 t.user.following = false;
2950 tweetInteractMoreMenuFollow.innerText = followUserText;
2951 let event = new CustomEvent('tweetAction', { detail: {
2952 action: 'block',
2953 tweet: t
2954 } });
2955 document.dispatchEvent(event);
2956 }
2957 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2958 });
2959 tweetInteractMoreMenuCopy.addEventListener('click', () => {
2960 navigator.clipboard.writeText(`https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}`);
2961 });
2962 if(tweetInteractMoreMenuCopyTweetId) tweetInteractMoreMenuCopyTweetId.addEventListener('click', () => {
2963 navigator.clipboard.writeText(t.id_str);
2964 });
2965 if(tweetInteractMoreMenuCopyUserId) tweetInteractMoreMenuCopyUserId.addEventListener('click', () => {
2966 navigator.clipboard.writeText(t.user.id_str);
2967 });
2968 if(tweetInteractMoreMenuShare) tweetInteractMoreMenuShare.addEventListener('click', () => {
2969 navigator.share({ url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}` });
2970 });
2971 tweetInteractMoreMenuNewtwitter.addEventListener('click', () => {
2972 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}?newtwitter=true`);
2973 });
2974 tweetInteractMoreMenuEmbed.addEventListener('click', () => {
2975 openInNewTab(`https://publish.twitter.com/?query=https://twitter.com/${t.user.screen_name}/status/${t.id_str}&widget=Tweet`);
2976 });
2977 if (t.user.id_str === user.id_str) {
2978 tweetInteractMoreMenuAnalytics.addEventListener('click', () => {
2979 openInNewTab(`https://twitter.com/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true`);
2980 });
2981 tweetInteractMoreMenuDelete.addEventListener('click', async () => {
2982 let sure = confirm(LOC.delete_sure.message);
2983 if (!sure) return;
2984 try {
2985 await API.tweet.delete(t.id_str);
2986 } catch (e) {
2987 alert(e);
2988 console.error(e);
2989 return;
2990 }
2991 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
2992 Array.from(document.getElementsByClassName(`tweet-id-${t.id_str}`)).forEach(tweet => {
2993 tweet.remove();
2994 });
2995 if(options.mainTweet) {
2996 let tweets = Array.from(document.getElementsByClassName('tweet'));
2997 if(tweets.length === 0) {
2998 location.href = 'https://twitter.com/home';
2999 } else {
3000 location.href = tweets[0].getElementsByClassName('tweet-time')[0].href;
3001 }
3002 }
3003 if(typeof timeline !== 'undefined') {
3004 timeline.data = timeline.data.filter(tweet => tweet.id_str !== t.id_str);
3005 }
3006 if(options.after && !options.disableAfterReplyCounter) {
3007 if(options.after.getElementsByClassName('tweet-self-thread-div')[0]) options.after.getElementsByClassName('tweet-self-thread-div')[0].hidden = true;
3008 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();
3009 else options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText = (+options.after.getElementsByClassName('tweet-footer-stat-replies')[0].innerText - 1).toString();
3010 }
3011 });
3012 if(tweetInteractMoreMenuPin) tweetInteractMoreMenuPin.addEventListener('click', async () => {
3013 if(pinnedTweet && pinnedTweet.id_str === t.id_str) {
3014 await API.tweet.unpin(t.id_str);
3015 pinnedTweet = null;
3016 tweet.remove();
3017 let tweetTime = new Date(t.created_at).getTime();
3018 let beforeTweet = Array.from(document.getElementsByClassName('tweet')).find(i => {
3019 let timestamp = +i.getElementsByClassName('tweet-time')[0].dataset.timestamp;
3020 return timestamp < tweetTime;
3021 });
3022 if(beforeTweet) {
3023 appendTweet(t, timelineContainer, { after: beforeTweet, disableAfterReplyCounter: true });
3024 }
3025 return;
3026 } else {
3027 await API.tweet.pin(t.id_str);
3028 pinnedTweet = t;
3029 let pinnedTweetElement = Array.from(document.getElementsByClassName('tweet')).find(i => {
3030 let topText = i.getElementsByClassName('tweet-top-text')[0];
3031 return (topText && topText.className.includes('pinned'));
3032 });
3033 if(pinnedTweetElement) {
3034 pinnedTweetElement.remove();
3035 }
3036 tweet.remove();
3037 appendTweet(t, timelineContainer, {
3038 prepend: true,
3039 top: {
3040 text: LOC.pinned_tweet.message,
3041 icon: "\uf003",
3042 color: "var(--link-color)",
3043 class: "pinned"
3044 }
3045 });
3046 return;
3047 }
3048 });
3049 }
3050 tweetInteractMoreMenuRefresh.addEventListener('click', async () => {
3051 let tweetData;
3052 try {
3053 tweetData = await API.tweet.getV2(t.id_str);
3054 } catch (e) {
3055 console.error(e);
3056 return;
3057 }
3058 if (!tweetData) {
3059 return;
3060 }
3061 if(typeof timeline !== 'undefined') {
3062 let tweetIndex = timeline.data.findIndex(tweet => tweet.id_str === t.id_str);
3063 if (tweetIndex !== -1) {
3064 timeline.data[tweetIndex] = tweetData;
3065 }
3066 }
3067 if (tweetInteractFavorite.className.includes('tweet-interact-favorited') && !tweetData.favorited) {
3068 tweetInteractFavorite.classList.remove('tweet-interact-favorited');
3069 }
3070 if (tweetInteractRetweet.className.includes('tweet-interact-retweeted') && !tweetData.retweeted) {
3071 tweetInteractRetweet.classList.remove('tweet-interact-retweeted');
3072 }
3073 if (!tweetInteractFavorite.className.includes('tweet-interact-favorited') && tweetData.favorited) {
3074 tweetInteractFavorite.classList.add('tweet-interact-favorited');
3075 }
3076 if (!tweetInteractRetweet.className.includes('tweet-interact-retweeted') && tweetData.retweeted) {
3077 tweetInteractRetweet.classList.add('tweet-interact-retweeted');
3078 }
3079 if(!options.mainTweet) {
3080 tweetInteractFavorite.innerText = tweetData.favorite_count;
3081 tweetInteractRetweet.innerText = tweetData.retweet_count;
3082 tweetInteractReply.innerText = tweetData.reply_count;
3083 }
3084 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
3085 });
3086 tweetInteractMoreMenuMute.addEventListener('click', async () => {
3087 if(t.conversation_muted) {
3088 await API.tweet.unmute(t.id_str);
3089 toast.info(LOC.unmuted_convo.message);
3090 t.conversation_muted = false;
3091 tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message;
3092 } else {
3093 await API.tweet.mute(t.id_str);
3094 toast.info(LOC.muted_convo.message);
3095 t.conversation_muted = true;
3096 tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message;
3097 }
3098 chrome.storage.local.set({tweetReplies: {}, tweetDetails: {}}, () => {});
3099 });
3100 let downloading = false;
3101 if (t.extended_entities && t.extended_entities.media.length > 0) {
3102 tweetInteractMoreMenuDownload.addEventListener('click', () => {
3103 if (downloading) return;
3104 downloading = true;
3105 t.extended_entities.media.forEach((item, index) => {
3106 let url = item.type === 'photo' ? item.media_url_https : item.video_info.variants[0].url;
3107 url = new URL(url);
3108 if (item.type === 'photo') {
3109 url.searchParams.set("name", "orig"); // force original resolution
3110 }
3111 fetch(url).then(res => res.blob()).then(blob => {
3112 downloading = false;
3113 let a = document.createElement('a');
3114 a.href = URL.createObjectURL(blob);
3115
3116 let ts = new Date(t.created_at).toISOString().split("T")[0];
3117 let extension = url.pathname.split('.').pop();
3118 let _index = t.extended_entities.media.length > 1 ? "_"+(index+1) : "";
3119 let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`;
3120 a.download = filename;
3121
3122 a.click();
3123 a.remove();
3124 }).catch(e => {
3125 downloading = false;
3126 console.error(e);
3127 });
3128 });
3129 });
3130 }
3131 if (t.extended_entities && t.extended_entities.media.some(m => m.type === 'animated_gif')) {
3132 tweetInteractMoreMenuDownloadGifs.forEach(dgb => dgb.addEventListener('click', e => {
3133 if (downloading) return;
3134 downloading = true;
3135 let n = parseInt(e.target.dataset.gifno)-1;
3136 let videos = Array.from(tweet.getElementsByClassName('tweet-media-gif'));
3137 let video = videos[n];
3138 let canvas = document.createElement('canvas');
3139 canvas.width = video.videoWidth;
3140 canvas.height = video.videoHeight;
3141 let ctx = canvas.getContext('2d');
3142 if (video.duration > 10 && !confirm(LOC.long_vid.message)) {
3143 return downloading = false;
3144 }
3145 let mde = tweet.getElementsByClassName('tweet-media-data')[0];
3146 mde.innerText = LOC.initialization.message + '...';
3147 let gif = new GIF({
3148 workers: 4,
3149 quality: 15,
3150 debug: true
3151 });
3152 video.currentTime = 0;
3153 video.loop = false;
3154 let isFirst = true;
3155 let interval = setInterval(async () => {
3156 if(isFirst) {
3157 video.currentTime = 0;
3158 isFirst = false;
3159 await sleep(5);
3160 }
3161 mde.innerText = `${LOC.initialization.message}... (${Math.round(video.currentTime/video.duration*100|0)}%)`;
3162 if (video.currentTime+0.1 >= video.duration) {
3163 clearInterval(interval);
3164 gif.on('working', (frame, frames) => {
3165 mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`;
3166 });
3167 gif.on('finished', blob => {
3168 mde.innerText = '';
3169 let a = document.createElement('a');
3170 a.href = URL.createObjectURL(blob);
3171 a.download = `${t.id_str}.gif`;
3172 document.body.append(a);
3173 a.click();
3174 a.remove();
3175 downloading = false;
3176 video.loop = true;
3177 video.play();
3178 });
3179 gif.render();
3180 return;
3181 }
3182 ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
3183 let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
3184 gif.addFrame(imgData, { delay: 100 });
3185 }, 100);
3186 }));
3187 }
3188 if(tweetInteractMoreMenuFeedbacks) tweetInteractMoreMenuFeedbacks.forEach(feedbackButton => {
3189 let feedback = t.feedback[feedbackButton.dataset.index];
3190 if (!feedback) return;
3191 feedbackButton.addEventListener('click', () => {
3192 chrome.storage.local.remove(["algoTimeline"], () => {});
3193 if(feedback.richBehavior && feedback.richBehavior.markNotInterestedTopic) {
3194 fetch(`https://twitter.com/i/api/graphql/OiKldXdrDrSjh36WO9_3Xw/TopicNotInterested`, {
3195 method: 'post',
3196 headers: {
3197 'content-type': 'application/json',
3198 'authorization': OLDTWITTER_CONFIG.public_token,
3199 "x-twitter-active-user": 'yes',
3200 "x-csrf-token": OLDTWITTER_CONFIG.csrf,
3201 "x-twitter-auth-type": 'OAuth2Session',
3202 },
3203 body: JSON.stringify({"variables":{"topicId": feedback.richBehavior.markNotInterestedTopic.topicId,"undo":false},"queryId":"OiKldXdrDrSjh36WO9_3Xw"}),
3204 credentials: 'include'
3205 }).then(i => i.json()).then(() => {});
3206 }
3207 fetch(`https://twitter.com/i/api${feedback.feedbackUrl}`, {
3208 method: 'post',
3209 headers: {
3210 'content-type': 'application/x-www-form-urlencoded',
3211 'authorization': OLDTWITTER_CONFIG.public_token,
3212 "x-twitter-active-user": 'yes',
3213 "x-csrf-token": OLDTWITTER_CONFIG.csrf,
3214 "x-twitter-auth-type": 'OAuth2Session',
3215 },
3216 body: `feedback_type=${feedback.feedbackType}&feedback_metadata=${t.feedbackMetadata}&undo=false`,
3217 credentials: 'include'
3218 }).then(i => i.json()).then(i => {
3219 alert(feedback.confirmation ? feedback.confirmation : LOC.feedback_thanks.message);
3220 tweet.remove();
3221 });
3222 });
3223 });
3224
3225 if(options.after) {
3226 options.after.after(tweet);
3227 } else if (options.before) {
3228 options.before.before(tweet);
3229 } else if (options.prepend) {
3230 timelineContainer.prepend(tweet);
3231 } else {
3232 timelineContainer.append(tweet);
3233 }
3234 if(vars.enableTwemoji) twemoji.parse(tweet);
3235 return tweet;
3236 } catch(e) {
3237 console.error(e);
3238 if(Date.now() - lastTweetErrorDate > 1000) {
3239 lastTweetErrorDate = Date.now();
3240 createModal(`
3241 <div style="max-width:700px">
3242 <span style="font-size:14px;color:var(--default-text-color)">
3243 <h2 style="margin-top: 0">${LOC.something_went_wrong.message}</h2>
3244 ${LOC.tweet_error.message}<br>
3245 ${LOC.error_instructions.message.replace('$AT1$', "<a target='_blank' href='https://github.com/dimdenGD/OldTwitter/issues'>").replace(/\$AT2\$/g, '</a>').replace("$AT3$", "<a target='_blank' href='mailto:admin@dimden.dev'>")}
3246 </span>
3247 <div class="box" style="font-family:monospace;line-break: anywhere;padding:5px;margin-top:5px;background:rgba(255, 0, 0, 0.1);color:#ff4545">
3248 ${escapeHTML(e.stack ? e.stack : String(e))} at ${t.id_str} (OldTwitter v${chrome.runtime.getManifest().version})
3249 </div>
3250 </div>
3251 `);
3252 }
3253 return null;
3254 }
3255}
3256
3257function replaceAll(str, find, replace) {
3258 return str.split(find).join(replace);
3259}