Extension to return old Twitter layout from 2015.
at master 190 kB view raw
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 = '&times;'; 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, "&lt;") 445 .replace(/>/g, "&gt;") 446 .replace(/"/g, "&quot;") 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' : ''}">&times;</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}