.
675 |
--------------------------------------------------------------------------------
/Twitter_AutoHD.user.js:
--------------------------------------------------------------------------------
1 | // ==UserScript==
2 | // @name Twitter AutoHD
3 | // @namespace Invertex
4 | // @version 2.92
5 | // @description Forces whole image to show on timeline with bigger layout for multi-image. Forces videos/images to show in highest quality and adds a download button and right-click for content that ensures an organized filename. As well as other improvements.
6 | // @author Invertex
7 | // @updateURL https://github.com/Invertex/Twitter-AutoHD/raw/master/Twitter_AutoHD.user.js
8 | // @downloadURL https://github.com/Invertex/Twitter-AutoHD/raw/master/Twitter_AutoHD.user.js
9 | // @icon https://i.imgur.com/M9oO8K9.png
10 | // @match https://*.twitter.com/*
11 | // @match https://*.twimg.com/media/*
12 | // @match https://*.x.com/*
13 | // @match https://*.fixupx.com/*
14 | // @match https://*.vxtwitter.com/*
15 | // @match https://*.fxtwitter.com/*
16 | // @noframes
17 | // @grant GM_xmlhttpRequest
18 | // @grant GM_download
19 | // @grant GM_openInTab
20 | // @grant GM_setClipboard
21 | // @grant unsafeWindow
22 | // @grant GM_setValue
23 | // @grant GM_getValue
24 | // @grant GM.setValue
25 | // @grant GM.getValue
26 | // @run-at document-start
27 | // @require https://ajax.googleapis.com/ajax/libs/jquery/2.1.0/jquery.min.js
28 | // ==/UserScript==
29 |
30 | const cooky = getCookie("ct0"); //Get our current Twitter session token so we can use Twitter API to request higher quality content
31 | const modifiedAttr = "THD_modified";
32 | const GM_OpenInTabMissing = (typeof GM_openInTab === 'undefined');
33 |
34 | var tweets = new Map(); //Cache intercepted tweets data
35 | ///
36 | const argsChildAndSub = { attributes: false, childList: true, subtree: true };
37 | const argsChildOnly = { attributes: false, childList: true, subtree: false };
38 | const argsChildAndAttr = { attributes: true, childList: true, subtree: false };
39 | const argsAll = { attributes: true, childList: true, subtree: true };
40 | const argsAttrOnly = { attributes: true, childList: false, subtree: false };
41 |
42 | const dlSVG = '' +
43 | '';
44 |
45 | const twitSVG = '';
49 |
50 | const bookmarkSVG = '';
51 | const unbookmarkSVG = '';
53 |
54 | addGlobalStyle(`@-webkit-keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
55 | div#thd_button_Download[downloading] {
56 | pointer-events: none !important;
57 | }
58 | div#thd_button_Download[downloading] svg {
59 | pointer-events: none !important;
60 | background-color: rgba(143, 44, 242, 0.5);
61 | border-radius: 12px;
62 | animation-iteration-count: infinite;
63 | animation-duration: 2s;
64 | animation-name: dl-animation;
65 | }
66 | div#thd_button_Download[downloading] svg > path {
67 | fill: rgba(255,255,255,0.2);
68 | }
69 | div[thd_customctx]:has(video[downloading]) {
70 | border-style: solid;
71 | border-color: cyan;
72 | border-width: 3px;
73 | border-radius: 0px 12px 12px 0px;
74 | animation-iteration-count: infinite;
75 | animation-duration: 2s;
76 | animation-name: dl-animation;
77 | }
78 | @keyframes dl-animation
79 | {
80 | 0%
81 | {
82 | border-color: cyan;
83 | background-color: cyan;
84 | }
85 | 33%
86 | {
87 | border-color: magenta;
88 | background-color: magenta;
89 | }
90 | 66%
91 | {
92 | border-color: yellow;
93 | background-color: yellow;
94 | }
95 | 100%
96 | {
97 | border-color: cyan;
98 | background-color: cyan;
99 | }
100 | }
101 | @keyframes spin { 0% { -webkit-transform: rotate(0deg); transform: rotate(0deg); } 100% { -webkit-transform: rotate(360deg); transform: rotate(360deg); } }
102 | .loader { border: 16px solid #f3f3f373; display: -webkit-box; display: -ms-flexbox; display: flex; margin: auto; border-top: 16px solid #3498db99; border-radius: 50%; width: 120px; height: 120px; -webkit-animation: spin 2s linear infinite; animation: spin 2s linear infinite;}
103 | .context-menu { position: absolute; text-align: center; margin: 0px; background: #040404; border: 1px solid #0e0e0e; border-radius: 5px;}
104 | .context-menu ul { padding: 0px; margin: 0px; min-width: 190px; list-style: none;}
105 | .context-menu ul li { padding-bottom: 7px; padding-top: 7px; border: 1px solid #0e0e0e; color:#c1bcbc; font-family: sans-serif; -webkit-user-select: none; -moz-user-select: none; -ms-user-select: none; user-select: none;}
106 | .context-menu ul li:hover { background: #202020;}
107 | a[aria-label="Grok"], div > aside[aria-label*="Premium"], div[data-testid="inlinePrompt"]:has(div > a[href^="/i/premium_sign_up"]),
108 | div:has(> div > div[data-testid="bookmark"], > div > div[data-testid="removeBookmark"]) > div#thd_button_Bookmark { display: none !important; }
109 | .thd_settings_collapsible { background-color:rgb(28, 30, 34); color:rgb(180, 183, 173); cursor:pointer; width:100%; border:none; text-align:left; outline:none; font-size:15px; border-radius:11px 11px 0 0; padding: 8px 14px 8px}
110 | .thd_settings_collapsible:after { content: "Show \u2795"; font-size: 13px; float: right; margin-left: 5px; }
111 | .thd_settings_active, .thd_settings_collapsible:hover { background-color:rgb(38, 40, 44); }
112 | .thd_settings_active:after { content: "Hide \u2796"; }
113 | .thd_settings_content{ overflow:hidden; background-color:rgb(22, 24, 28); -webkit-box-orient: vertical; -webkit-box-direction: normal; -ms-flex-flow: column; flex-flow: column; display: -webkit-box; display: -ms-flexbox; display: flex; padding-bottom: 6px; }
114 | .thd_settings_content_closed { display: none !important; }
115 | .thd_settings_toggle { margin: 0.01em 0.4em 0.01em; border-style: solid; border-width: 0.015em; border-color: #101010; background-color: #202020; border-radius:6px; color: rgb(100, 100, 100); }
116 | .thd_settings_toggle:hover { background-color: #393838; border-width: 0.045em; border-color: #505050; cursor:pointer; color: rgb(210, 210 210); }
117 | .thd_settings_toggle_enabled { background-color: #292828; border-width: 0.045em; border-color: #404040; cursor:pointer; color: rgb(190, 190, 190); }
118 | #thd_toggleTrending { width: 100% !important; margin: 0px !important; }
119 | `);
120 |
121 | //Greasemonkey does not have this functionality, so helpful way to check which function to use
122 | const isGM = (typeof GM_addValueChangeListener === 'undefined');
123 |
124 | //<--> TWEET PROCESSING <-->//
125 | function StringBuilder(value)
126 | {
127 | this.strings = new Array();
128 | this.append(value);
129 | }
130 | StringBuilder.prototype.append = function (value)
131 | {
132 | if (value)
133 | {
134 | this.strings.push(value);
135 | }
136 | }
137 | StringBuilder.prototype.clear = function ()
138 | {
139 | this.strings.length = 0;
140 | }
141 | StringBuilder.prototype.toString = function ()
142 | {
143 | return this.strings.join("");
144 | }
145 |
146 | const sb = new StringBuilder("");
147 |
148 | const filterVideoSources = function (m3u8)
149 | {
150 | const regex = /(?<=,RESOLUTION=)(.*)(?=x)/gm;
151 | const regexAudio = /(?<=GROUP-ID="audio-)(.*)(?=",)/gm;
152 | let bestLine = 0;
153 | let bestAudioLine = 0;
154 | let bestResolution = 0;
155 | let bestAudioBandwith = 0;
156 | let avc1FallbackVideo = 0;
157 |
158 | sb.clear();
159 |
160 | let lines = m3u8.split('#');
161 |
162 | sb.append(lines[0]);
163 |
164 | for (let i = 1; i < lines.length; i++)
165 | {
166 | let line = lines[i];
167 |
168 | if (line.includes('STREAM-INF:'))
169 | {
170 | let resolution = parseInt(line.match(regex));
171 | if (resolution > bestResolution)
172 | {
173 | bestResolution = resolution;
174 | bestLine = i;
175 | }
176 | else if (bestLine === 0) { bestLine = i; } //failsafe in case something breaks with parsing down the line
177 | //Twitter serves HEVC now and leaves just one low res avc1 fallback, so don't remove that fallback just in case...
178 | if(line.includes('/avc1/')) { avc1FallbackVideo = i; }
179 | }
180 | else if (lines[i].includes('EXT-X-MEDIA:NAME="Audio"'))
181 | {
182 | let bandwidth = parseInt(line.match(regexAudio));
183 |
184 | if (bandwidth > bestAudioBandwith)
185 | {
186 | bestAudioBandwith = bandwidth;
187 | bestAudioLine = i;
188 | }
189 | else if (bestAudioLine === 0) { bestAudioLine = i; } //failsafe in case something breaks with parsing down the line
190 | }
191 | else
192 | {
193 | sb.append('#' + lines[i]);
194 | }
195 | }
196 |
197 | let doAVC1Fallback = bestLine > 0 && !lines[bestLine].includes('/avc1/') && avc1FallbackVideo > 0;
198 |
199 | if (bestAudioLine > 0)
200 | {
201 | sb.append('#' + lines[bestAudioLine]);
202 |
203 | if (doAVC1Fallback)
204 | {
205 | let avcLine = lines[avc1FallbackVideo];
206 | let index = avcLine.indexOf('AUDIO="') + 7;
207 |
208 | if(index > 8)
209 | {
210 | let audioGroup = avcLine.substr(index).split('"')[0];
211 | for (let i = 1; i < lines.length; i++)
212 | {
213 | let line = lines[i];
214 | if(line.includes('TYPE=AUDIO') && line.includes(audioGroup))
215 | {
216 | sb.append('#' + line);
217 | break;
218 | }
219 | }
220 | }
221 | }
222 | }
223 |
224 |
225 | if (bestLine > 0)
226 | {
227 | sb.append('#' + lines[bestLine]);
228 | if (doAVC1Fallback) { sb.append('#' + lines[avc1FallbackVideo]); }
229 | }
230 |
231 | return sb.toString();
232 | };
233 |
234 | const changeToTwitter = function(url)
235 | {
236 | return url.replace('/x.com/', '/twitter.com/').replace('.x.com/', '.twitter.com/');
237 | }
238 |
239 |
240 | // Intercept relevant keys used by API so we can use the API too
241 | var transactID = "";
242 | var authy = "";
243 |
244 | var oldReqHead = unsafeWindow.XMLHttpRequest.prototype.setRequestHeader;
245 | unsafeWindow.XMLHttpRequest.prototype.setRequestHeader = exportFunction(function(name, value)
246 | {
247 | if(name == "X-Client-Transaction-Id")
248 | {
249 | transactID = value;
250 | }
251 | else if(name == "authorization")
252 | {
253 | authy = value;
254 | }
255 | oldReqHead.call(this, name, value);
256 | }, unsafeWindow);
257 |
258 |
259 | //Intercept the Timeline to pre-cache information about tweets and filter out unwanted tweets
260 | var openOpen = unsafeWindow.XMLHttpRequest.prototype.open;
261 | unsafeWindow.XMLHttpRequest.prototype.open = exportFunction(function(method, url)
262 | {
263 | //url = changeToTwitter(url);
264 | processXMLOpen(this, method, url);
265 | openOpen.call(this, method, url);
266 | }, unsafeWindow);
267 |
268 |
269 | function processXMLOpen(thisRef, method, url)
270 | {
271 | if(prefsLoaded === false) {
272 | loadToggleValues();
273 | }
274 |
275 | if ((url.includes('video.twimg.com') || url.includes('master_dynamic')) && url.includes('.m3u8?'))
276 | {
277 | thisRef.addEventListener('readystatechange', function (e)
278 | {
279 | if (toggleHQVideo.enabled && thisRef.readyState === 4)
280 | {
281 | const m3uText = e.target.responseText;
282 | if(!m3uText.includes('#EXT-X-MEDIA-SEQUENCE'))
283 | {
284 | const m3u = filterVideoSources(m3uText);
285 |
286 | Object.defineProperty(thisRef, 'response', { writable: true });
287 | Object.defineProperty(thisRef, 'responseText', { writable: true });
288 | thisRef.response = thisRef.responseText = m3u;
289 | }
290 | }
291 | });
292 | }
293 | else if(url.includes('show.json?'))
294 | {
295 | thisRef.addEventListener('readystatechange', function (e)
296 | {
297 | if (toggleHQVideo.enabled && thisRef.readyState === 4)
298 | {
299 | let json = JSON.parse(e.target.response);
300 | let vidInfo = json.extended_entities?.media?.video_info ?? null;
301 |
302 | if(vidInfo != null && vidInfo.variants != null && vidInfo.variants.length > 2)
303 | {
304 | vidInfo.variants = stripVariants(vidInfo.variants, true);
305 | Object.defineProperty(thisRef, 'response', { writable: true });
306 | Object.defineProperty(thisRef, 'responseText', { writable: true });
307 |
308 | thisRef.response = thisRef.responseText = JSON.stringify(json);
309 | }
310 | }
311 | });
312 | }
313 | else if(url.includes("/graphql/"))
314 | {
315 | url = url.replace('includePromotedContent%22%3Atrue', 'includePromotedContent%22%3Afalse');
316 | url = url.replace('phone_label_enabled%22%3Afalse', 'phone_label_enabled%22%3Atrue');
317 | url = url.replace('reach_fetch_enabled%22%3Atrue', 'reach_fetch_enabled%22%3Afalse');
318 | url = url.replace('withQuickPromoteEligibilityTweetFields%22%3Atrue', 'withQuickPromoteEligibilityTweetFields%22%3Afalse');
319 | url = url.replace('article_tweet_consumption_enabled%22%3Atrue', 'article_tweet_consumption_enabled%22%3Afalse');
320 | url = url.replace('count%22%3A20', 'count%22%3A30');
321 |
322 | thisRef.addEventListener('readystatechange', function (req)
323 | {
324 | if (thisRef.readyState === 4)
325 | {
326 | let json = null;
327 |
328 | try {
329 | json = JSON.parse(req.target.response);
330 | } catch(e) {
331 | if(req.target.status >= 400 && (url.includes('TweetDetail?')))
332 | {
333 | window.location.href = window.location.href;
334 | }
335 | return;
336 | }
337 |
338 | if(json?.data != null)
339 | {
340 | processTimelineData(json);
341 | Object.defineProperty(thisRef, 'response', { writable: true });
342 | Object.defineProperty(thisRef, 'responseText', { writable: true });
343 |
344 | thisRef.response = thisRef.responseText = JSON.stringify(json);
345 | }
346 | }
347 | });
348 | }
349 | else if(url.includes('/videoads/'))
350 | {
351 | thisRef.addEventListener('readystatechange', function (e)
352 | {
353 | if (thisRef.readyState === 4)
354 | {
355 | Object.defineProperty(thisRef, 'responseText', { writable: true });
356 | thisRef.responseText = "{}";
357 | }
358 | });
359 | }
360 | else if(url.includes('all.json?') && window.location.href.endsWith('/notifications'))
361 | {
362 | thisRef.addEventListener('readystatechange', function (e)
363 | {
364 | if(thisRef.readyState === 4)
365 | {
366 | let json = JSON.parse(e.target.response);
367 |
368 | let notifs = json?.timeline?.instructions ?? null;
369 | if(notifs != null)
370 | {
371 | notifs = processNotificationsData(notifs);
372 |
373 | Object.defineProperty(thisRef, 'response', { writable: true });
374 | Object.defineProperty(thisRef, 'responseText', { writable: true });
375 |
376 | thisRef.response = thisRef.responseText = JSON.stringify(json);
377 | }
378 |
379 | }
380 | });
381 | }
382 | /* else if(url.includes('guide.json')) //Explore
383 | {
384 | this.addEventListener('readystatechange', function (e)
385 | {
386 | if (this.readyState === 4)
387 | {
388 | let json = JSON.parse(e.target.response);
389 | processExploreData(json?.globalObjects?.tweets);
390 |
391 | Object.defineProperty(this, 'responseText', { writable: true });
392 | this.responseText = JSON.stringify(json);
393 | }
394 |
395 | });
396 | }*/
397 | }
398 |
399 | function stripVariants(variants, keepM3U = false)
400 | {
401 | if(variants == null) { return null; }
402 | let bestQuality = variants.reduce((a, b) => ((a?.bitrate ?? 0) > (b?.bitrate ?? 0) ? a : b));
403 | if(keepM3U)
404 | {
405 | let m3u = variants.find((entry) => entry.url.includes('.m3u8'));
406 | if(m3u === undefined) { return [bestQuality]; }
407 | return [bestQuality, m3u];
408 | }
409 |
410 | return bestQuality;
411 | }
412 |
413 | function processMediaResponseData(mediasJson)
414 | {
415 | let hqVideo = toggleHQVideo.enabled;
416 | let hqImg = toggleHQImg.enabled;
417 |
418 | mediasJson.forEach((mediaItem) =>
419 | {
420 | if(hqVideo === true && mediaItem.type === 'video')
421 | {
422 | mediaItem.video_info.variants = stripVariants(mediaItem.video_info.variants, true);
423 | }
424 | else if(hqImg === true && mediaItem.type == 'photo')
425 | {
426 | mediaItem.media_url_https = getHighQualityImage(mediaItem.media_url_https);
427 | }
428 | });
429 | }
430 |
431 | class Tweet
432 | {
433 | constructor(tweetResult)
434 | {
435 | let data = tweetResult;
436 | if(data?.tweet != null){ data = data.tweet; } //If tweet is limited by who can comment, need to do this
437 |
438 | this.isRetweet = data?.legacy?.retweeted_status_result?.result != null;
439 | if(this.isRetweet)
440 | {
441 | data = data.legacy.retweeted_status_result.result;
442 | if(data?.tweet) { data = data.tweet; }
443 |
444 | }
445 |
446 | this.id = data?.rest_id ?? data?.conversation_id;
447 | tweets.set(this.id, this);
448 |
449 | let legacy = data?.legacy ?? data; //Swapping to handle odd Explore page data
450 |
451 | this.media = legacy?.extended_entities?.media;
452 | this.hasMedia = this.media != null;
453 |
454 | this.quote = data?.quoted_status_result?.result;
455 | this.isQuote = this.quote != null;
456 | if(this.isQuote) { this.quote = new Tweet(this.quote); }
457 | this.quoteHasMedia = this.isQuote && this.quote.hasMedia;
458 |
459 | this.username = data?.core?.user_results?.result.legacy?.screen_name;
460 | this.url = "https://twitter.com/" + this.username + "/" + this.id;
461 |
462 | if(this.hasMedia === true)
463 | {
464 | if(this.media != null) { processMediaResponseData(this.media);}
465 | if(this.isQuote === true && this.quoteHasMedia === true) { processMediaResponseData(this.quote.media); }
466 | }
467 | else if(data?.card?.legacy != null)
468 | {
469 | let cardLegacy = data.card.legacy;
470 | if(cardLegacy.binding_values != null)
471 | {
472 | for(let i = 0; i < cardLegacy.binding_values.length; i++)
473 | {
474 | let bv = cardLegacy.binding_values[i];
475 | if(bv.key === "unified_card")
476 | {
477 | let cardValue = bv.value;
478 | if(cardValue?.type != "STRING") { break; }
479 |
480 | let valueJson = JSON.parse(cardValue.string_value);
481 |
482 | valueJson.type = "video"; //Preventing clicking on video opening up a new tab, blocks recent exploits and overall better UX
483 | if(valueJson.components.length > 0)
484 | {
485 | this.media = [];
486 | let hqVideo = toggleHQVideo.enabled;
487 | valueJson.components.forEach((comp) => {
488 | if(comp.startsWith("media_"))
489 | {
490 | let compData = valueJson.component_objects[comp].data;
491 | let mediaId = compData.id;
492 | let destination = valueJson.destination_objects[compData.destination].data.url_data;
493 | destination.vanity = destination.url.substr(0, 40);
494 | let mediaData = valueJson.media_entities[mediaId];
495 | if(hqVideo === true && mediaData.video_info != null)
496 | {
497 | mediaData.video_info.variants = stripVariants(mediaData.video_info.variants, true);
498 | }
499 | this.media.push(mediaData);
500 | }
501 | if(comp.startsWith("details_"))
502 | {
503 | let compData = valueJson.component_objects[comp].data;
504 |
505 | let destination = valueJson.destination_objects[compData.destination].data.url_data;
506 | destination.vanity = destination.url.substr(0, 90);
507 | }
508 | });
509 |
510 | this.hasMedia = this.media.length > 0;
511 | cardValue.string_value = JSON.stringify(valueJson);
512 | }
513 | }
514 | else if(bv.key === "photo_image_full_size_original")
515 | {
516 | let card_img = bv.value?.image_value;
517 |
518 | if(card_img)
519 | {
520 |
521 | let card_img_id = card_img.url.split('?')[0].split('/').at(-1);
522 | this.media = [{type: "photo", media_url_https: card_img.url, id_str: card_img_id, expanded_url: this.url, original_info: {width: card_img.width, height: card_img.height}}];
523 | this.hasMedia = true;
524 | break;
525 | }
526 | }
527 | }
528 | }
529 | }
530 |
531 |
532 | if(data?.edit_control?.edit_tweet_ids != null)
533 | {
534 | data?.edit_control?.edit_tweet_ids.forEach((edit_id) => { tweets.set(edit_id, this); });
535 | }
536 |
537 | this.isBookmarked = legacy?.bookmarked ?? false;
538 | }
539 |
540 | get bookmarked() { return this.isBookmarked; };
541 | bookmark() { this.isBookmarked = true; }
542 | unbookmark() { this.isBookmarked = false; }
543 |
544 | getMediaData = function(index)
545 | {
546 | if(this.media == null || this.media.length <= index) { return null; }
547 |
548 | let mediaItem = this.media[index];
549 |
550 | let username = this.username;
551 | let id = this.id;
552 | let media_id = mediaItem.id_str;
553 | let media_url = mediaItem.media_url_https;
554 | let url = mediaItem.expanded_url;
555 | if(!url.includes('card_img/'))
556 | {
557 | let comIndex = url.indexOf('.com/');
558 | let urlParts = url.substring(comIndex < 0 ? 0 : comIndex + 5).split('/');
559 | if(urlParts.length > 3)
560 | {
561 | username = urlParts[0];
562 | id = urlParts[2];
563 | }
564 | }
565 | let isVideo = mediaItem.type == 'video' || media_url.includes('/tweet_video_thumb/');
566 | let isPhoto = mediaItem.type == 'photo';
567 | let counter = this.media.length < 2 ? -1 : index + 1;
568 |
569 | return {
570 | username: username,
571 | id: id,
572 | media_id: media_id,
573 | media_url: media_url,
574 | mediaNum: counter,
575 | isVideo: isVideo,
576 | isPhoto: isPhoto,
577 | getContentURL: () => {
578 | if(isVideo) {
579 | return stripVariants(mediaItem.video_info.variants).url; }
580 | return getHighQualityImage(media_url);
581 | },
582 | type: mediaItem.type,
583 | width: mediaItem.original_info.width,
584 | height: mediaItem.original_info.height
585 | };
586 | }
587 | }
588 |
589 | function processNotificationsData(notifs)
590 | {
591 | if(notifs.length > 0)
592 | {
593 | for(let i = 0; i < notifs.length; i++)
594 | {
595 | if('addEntries' in notifs[i])
596 | {
597 | let entries = notifs[i].addEntries?.entries ?? null;
598 |
599 | if(entries != null && entries.length > 0)
600 | {
601 | let entryCnt = entries.length;
602 | for(let e = entryCnt - 1; e >= 0; e--)
603 | {
604 | let entry = entries[e];
605 | if(entry?.content?.item?.clientEventInfo?.element?.startsWith("generic_magic"))
606 | {
607 | entries.splice(e, 1);
608 | }
609 | }
610 | }
611 | }
612 | }
613 | }
614 | }
615 |
616 | function processExploreData(exploreTweets)
617 | {
618 | for(let i = 0; i < exploreTweets.length; i++)
619 | {
620 | let exploreTweet = exploreTweets[i];
621 | let tweet = new Tweet(exploreTweet);
622 | }
623 | }
624 |
625 | function processTimelineItem(item)
626 | {
627 | if(item?.promotedMetadata != null) { return false; }
628 | let result = item?.tweet_results?.result;
629 |
630 | if(result)
631 | {
632 | let tweet = new Tweet(result);
633 | }
634 |
635 | return true;
636 | }
637 |
638 | function processTimelineEntry(entry)
639 | {
640 | if(entry?.content?.clientEventInfo?.component == "suggest_promoted")
641 | {
642 | entry.content = {};
643 | return;
644 | }
645 | let items = entry?.content?.items;
646 | if(items != null)
647 | {
648 | for(let i = 0; i < items.length; i++)
649 | {
650 | if(!processTimelineItem(items[i].item.itemContent))
651 | {
652 | entry.content.items.splice(i, 1);
653 | }
654 | }
655 | }
656 | else if (entry?.content?.itemContent)
657 | {
658 | if(!processTimelineItem(entry.content.itemContent))
659 | {
660 | entry.content = {};
661 | }
662 | }
663 | else if (entry?.item?.itemContent)
664 | {
665 | if(!processTimelineItem(entry.item.itemContent))
666 | {
667 | entry.item = {};
668 | }
669 | }
670 | }
671 |
672 | function processTimelineEntries(entries)
673 | {
674 | // entries.filter(entry => entry.content.clientEventInfo?.component != "suggest_promoted");
675 | for(let i = 0; i < entries.length; i++)
676 | {
677 | processTimelineEntry(entries[i]);
678 | }
679 | }
680 | function processTimelineModuleEntries(moduleEntries)
681 | {
682 | for(let i = 0; i < moduleEntries.length; i++)
683 | {
684 | processTimelineEntry(moduleEntries[i]);
685 | }
686 | }
687 |
688 | function processTimelineData(json)
689 | {
690 | let instructions = json?.data?.home?.home_timeline_urt?.instructions;
691 | if(instructions == null) { instructions = json.data?.user?.result?.timeline_v2?.timeline?.instructions; }
692 | if(instructions == null) { instructions = json.data?.user?.result?.timeline?.timeline?.instructions; }
693 | if(instructions == null) { instructions = json.data?.threaded_conversation_with_injections_v2?.instructions; }
694 | if(instructions == null) { instructions = json.data?.bookmark_timeline_v2?.timeline?.instructions; }
695 | if(instructions == null) { instructions = json.data?.list?.tweets_timeline?.timeline?.instructions; }
696 | if(instructions == null) { instructions = json.data?.search_by_raw_query?.search_timeline?.timeline?.instructions; }
697 | if(instructions == null) { instructions = json.data?.communityResults?.result?.ranked_community_timeline?.timeline?.instructions; }
698 | if(instructions == null) { return; }
699 |
700 | for(let inst = 0; inst < instructions.length; inst++)
701 | {
702 | let instruction = instructions[inst];
703 | if(instruction.type == "TimelineAddEntries")
704 | {
705 | processTimelineEntries(instruction.entries);
706 | }
707 | else if(instruction.type == "TimelinePinEntry")
708 | {
709 | processTimelineEntry(instruction.entry); //Pinned tweet
710 | }
711 | else if(instruction.type == "TimelineAddToModule")
712 | {
713 | processTimelineModuleEntries(instruction.moduleItems); //Pinned tweet
714 | }
715 | }
716 | }
717 |
718 | var firstRun = true;
719 |
720 | function processTweetsQuery(entries)
721 | {
722 | for(let i = entries.length - 1; i >= 0; i--)
723 | {
724 | let entry = entries[i];
725 | let content = entry.content;
726 | if(content == null) { continue; }
727 |
728 | if(content.items)
729 | {
730 | content = content.items[0].item.itemContent;
731 | }
732 | else
733 | {
734 | content = content.itemContent;
735 | }
736 |
737 | if(content == null || content.tweet_results == null) { continue; }
738 | if(firstRun && entries.length <= 4) //Avoid the timeline freezing from not enough initial entries
739 | {
740 | continue;
741 | }
742 |
743 | if(content.promotedMetadata && content.promotedMetadata.advertiser_results)
744 | {
745 | entries.splice(i, 1);
746 | }
747 | else if(content.socialContext)
748 | {
749 |
750 | let contextType = content.socialContext.contextType;
751 |
752 | if((!toggleLiked.enabled && contextType == "Like") || (!toggleFollowed.enabled && contextType == "Follow") || (!toggleTopics.enabled && content.socialContext.type == "TimelineTopicContext"))
753 | {
754 | entries.splice(i, 1);
755 | }
756 |
757 | }
758 | else if(!toggleRetweet.enabled
759 | && content.tweet_results.result.legacy != null
760 | && content.tweet_results.result.legacy.retweeted_status_result != null
761 | && content.tweet_results.result.legacy.retweeted_status_result.result.core.user_results.result.legacy.following == false) //Only hide the Retweet if it's not the user's own tweet
762 | {
763 |
764 | entries.splice(i, 1);
765 | }
766 | }
767 |
768 | firstRun = false;
769 | return entries;
770 | }
771 |
772 |
773 | function getPostButtonCopy(tweet, name, svg, svgViewBox, color, bgColor, onHovering, onNotHovering)
774 | {
775 | let getButtonToDupe = function (btnGrp)
776 | {
777 | let lastChd = btnGrp.lastChild;
778 | //let bm = btnGrp.querySelector('div > div[data-testid="bookmark"], div > div[data-testid="removeBookmark"]');
779 | //if(bm != null) { lastChd = bm.parentElement; }
780 | let newNode = lastChd.cloneNode(true);
781 | lastChd.className = btnGrp.childNodes.item(2).className;
782 | return {btn: newNode, origBtn: lastChd}
783 | };
784 |
785 | let isIframe = false;
786 | let id = "thd_button_" + name;
787 |
788 | let buttonGrp = tweet.closest('article[role="article"]')?.querySelector('div[role="group"][id^="id__"]');
789 | if (buttonGrp == null) //Try iframe version
790 | {
791 | buttonGrp = tweet.querySelector('div a[href*="like?"]')?.parentElement;
792 | if (buttonGrp != null)
793 | {
794 | isIframe = true;
795 | getButtonToDupe = function (btnGrp)
796 | {
797 | let orig = btnGrp.querySelector('a:nth-child(2)');
798 | return { btn: orig.cloneNode(true), origBtn: orig };
799 | };
800 | }
801 | }
802 | if (buttonGrp == null || buttonGrp.querySelector("div#" + id) != null) { return null; } //Button group doesn't exist or we already processed this element and added a DL button
803 |
804 | buttonGrp.style.maxWidth = "100%";
805 |
806 | /*if(!toggleAnalyticsDisplay.enabled)
807 | {
808 | let analBtn = buttonGrp.querySelector('a[href$="/analytics"]');
809 | if(analBtn) { analBtn.parentElement.style.display = "none"; }
810 | }*/
811 |
812 | let btnDupe = getButtonToDupe(buttonGrp);
813 |
814 | if(btnDupe.btn != null)
815 | {
816 | let btn = btnDupe.btn;
817 | buttonGrp.insertBefore(btn, btnDupe.origBtn);
818 | // let shareBtn = btnDupe.origBtn.querySelector('[aria-label^="Share"]');
819 | // if(shareBtn)
820 | // {
821 | // buttonGrp.appendChild(btnDupe.origBtn);
822 | btnDupe.origBtn.style += " -webkit-flex-grow: 0.1; flex-grow: 0.1 !important;";
823 | // }
824 |
825 | btn.id = id;
826 | btn.style.marginRight = "8px";
827 | btn.style.marginLeft = "8px";
828 | $(btn.parentNode).addClass(btn.className);
829 | btn.setAttribute('aria-label', name);
830 | btn.title = name;
831 | const iconDiv = isIframe ? btn.querySelector('div[dir="auto"]') : btn.querySelector('div[dir="ltr"]');
832 | const svgElem = btn.querySelector('svg');
833 | const bg = isIframe ? svgElem.parentElement : iconDiv.firstElementChild.firstElementChild;
834 |
835 | svgElem.innerHTML = svg;
836 |
837 | svgElem.setAttribute('viewBox', svgViewBox);
838 |
839 | const oldBGColor = $(bg).css("background-color");
840 | const oldIconColor = $(iconDiv).css("color");
841 |
842 | let hover = function()
843 | {
844 | $(bg).css("background-color", bgColor);
845 | $(bg).css("border-radius", "20px");
846 | $(svgElem).css("color", color);
847 | // onHovering({btn: btn, inIframe: isIframe, svg: svgElem});
848 | };
849 |
850 | let unhover = function()
851 | {
852 | $(bg).css("background-color", oldBGColor);
853 | $(svgElem).css("color", oldIconColor);
854 | if(onNotHovering) onNotHovering(svgElem);
855 | };
856 |
857 | let updateColor = function()
858 | {
859 | if(btn.matches(":hover")) { unhover(); }
860 | else { hover(); }
861 | };
862 |
863 | //Emulate Twitter hover color change
864 | $(btn).hover(() => { hover(); }, () => { unhover(); });
865 |
866 | $(bg).css("background-color", oldBGColor);
867 | $(svgElem).css("color", oldIconColor);
868 |
869 |
870 | return {btn: btn, origBtn: btnDupe.origBtn, inIframe: isIframe, svg: svgElem, doHover: hover, doUnhover: unhover, updateCol: updateColor};
871 | }
872 |
873 | return null;
874 | }
875 |
876 | function addBookmarkButton(tweetElem, tweetData)
877 | {
878 | return;
879 | if(tweetElem == null) { return; }
880 |
881 | let id = tweetData.id;
882 | let existingBookmark = tweetElem.querySelector('div[data-testid="bookmark"]');
883 |
884 | if(existingBookmark != null)
885 | {
886 | return;
887 | existingBookmark = existingBookmark.parentElement;
888 | existingBookmark.style += " -webkit-flex: 0.6 1.0; flex: 0.6 1.0;";;
889 | let otherBtn = existingBookmark.closest('[role="group"]').childNodes.item(2);
890 | $(existingBookmark).removeClass().addClass(otherBtn.className);
891 | return;
892 | }
893 |
894 | let onHoverStopped = function(svgElem, tweetID)
895 | {
896 | let tweetData = tweets.get(tweetID);
897 | if(tweetData && tweetData.bookmarked) {
898 | $(svgElem).css("color", "#1c9bf0FF");
899 | }
900 | }
901 |
902 | const btnCopy = getPostButtonCopy(tweetElem, "Bookmark", bookmarkSVG, "0 0 24 24", "#1c9bf0", "#1c9bf01a", (data)=>{}, (svgElem) => {onHoverStopped(svgElem, id);});
903 |
904 | if(btnCopy == null || btnCopy.btn == null) { return; }
905 | let btn = btnCopy.btn;
906 |
907 | onHoverStopped(btnCopy.svg, id);
908 |
909 | $(btn).click(function (e)
910 | {
911 | e.preventDefault();
912 | e.stopPropagation();
913 | let tweetData = tweets.get(id);
914 |
915 | if(tweetData == null) { return;}
916 | if(tweetData.bookmarked)
917 | {
918 | unbookmarkPost(id, (resp) =>
919 | {
920 | if(resp.status < 300)
921 | {
922 | tweetData.unbookmark();
923 | btnCopy.updateCol();
924 | }
925 | });
926 | }
927 | else
928 | {
929 | bookmarkPost(id, (resp) =>
930 | {
931 | if(resp.status < 300)
932 | {
933 | tweetData.bookmark();
934 | btnCopy.updateCol();
935 | }
936 | });
937 | }
938 | });
939 |
940 | btnCopy.btn.style += " -webkit-flex: 0.6 1.0; flex: 0.6 1.0;";;
941 | }
942 |
943 | async function addDownloadButton(tweetElem, tweetData, mediaInfo)
944 | {
945 | for(let i = mediaInfo.data.mediaNum - 1; i > 0; i--)
946 | {
947 | if(tweetData.media[i].isVideo) { return; }
948 | }
949 |
950 | const btnCopy = getPostButtonCopy(tweetElem, "Download", dlSVG, "-80 -80 160 160", "#f3d607FF", "#f3d60720");
951 | if(btnCopy == null) { return; }
952 |
953 | const dlBtn = btnCopy.btn;
954 |
955 | if(dlBtn == null || btnCopy == null) { return; }
956 |
957 | let isIframe = btnCopy.inIframe;
958 | const filename = filenameFromMediaData(mediaInfo.data);
959 |
960 | const linkElem = dlBtn
961 |
962 | if (isIframe)
963 | {
964 | let classy = dlBtn.className;
965 | dlBtn.className = "";
966 | linkElem = $(dlBtn).wrapAll(``)[0].parentElement;
967 | linkElem.setAttribute('download', filename);
968 | dlBtn.querySelector('div[dir="auto"] > span').innerText = "Download";
969 | btnCopy.btn = $(linkElem).wrapAll(``)[0].parentElement;
970 | }
971 | else
972 | {
973 | dlBtn.style.marginLeft = "";
974 | linkElem.style.cssText = dlBtn.style.cssText;
975 | dlBtn.style.marginRight = "";
976 | }
977 |
978 |
979 | $(linkElem).click(async function (e) {
980 | linkElem.setAttribute('downloading','');
981 | e.preventDefault();
982 | e.stopPropagation();
983 | let dlurl = mediaInfo.data.getContentURL();
984 | await download(dlurl, filename);
985 | linkElem.removeAttribute('downloading');
986 | });
987 |
988 | btnCopy.btn.className = btnCopy.origBtn.className;
989 | btnCopy.btn.style += " -webkit-flex: 0.6 1.0; flex: 0.6 1.0; justify-content: center !important;";;
990 | }
991 |
992 | function waitForImgLoad(img)
993 | {
994 | return new Promise((resolve, reject) =>
995 | {
996 | img.onload = () => resolve(img);
997 | img.onerror = reject;
998 | });
999 | }
1000 |
1001 | function updateImgSrc(imgElem, bgElem, src)
1002 | {
1003 | if (imgElem.src != src && toggleHQImg.enabled)
1004 | {
1005 | imgElem.src = src;
1006 | bgElem.style.backgroundImage = `url("${src}")`;
1007 | }
1008 | };
1009 |
1010 | function updateElemPadding(panelCnt, background, imgContainerElem)
1011 | {
1012 | if (panelCnt != 3)
1013 | {
1014 | if(background)
1015 | {
1016 | background.style.backgroundSize = "cover";
1017 | } else {}
1018 | //imgContainerElem.style.marginBottom = "0%";
1019 | }
1020 | if (panelCnt < 2)
1021 | {
1022 | // imgContainerElem.removeAttribute('style');
1023 | }
1024 | else
1025 | {
1026 | imgContainerElem.style.marginLeft = "0%";
1027 | imgContainerElem.style.marginRight = "0%";
1028 | imgContainerElem.style.marginTop = "0%";
1029 | }
1030 | };
1031 |
1032 | function updateContentElement(tweetElem, tweetData, mediaInfo, elemIndex, elemCnt)
1033 | {
1034 | let mediaElem = mediaInfo.mediaElem;
1035 | let tweetPhoto = mediaElem.closest('div[data-testid="tweetPhoto"],div[data-testid^="card.wrapper"]');
1036 | const flexDir = $(tweetPhoto).css('flex-direction');
1037 | const isVideo = mediaInfo.data.isVideo;
1038 | let bg = isVideo ? mediaElem.parentElement : tweetPhoto.querySelector('div[style^="background-image"]');
1039 |
1040 | let linkElem = tweetPhoto.querySelector('div[data-testid="videoPlayer"]') ?? mediaElem?.closest('a');
1041 |
1042 | mediaInfo.tweetPhotoElem = tweetPhoto;
1043 | mediaInfo.linkElem = linkElem;
1044 | mediaInfo.bgElem = bg;
1045 | mediaInfo.flex = flexDir;
1046 |
1047 | if(isVideo)
1048 | {
1049 | addDownloadButton(tweetElem, tweetData, mediaInfo);
1050 | //Consistent video controls for GIF videos too
1051 | if(mediaInfo.data.media_url.includes('/tweet_video_thumb'))
1052 | {
1053 | mediaElem.setAttribute('controls',"true");
1054 | mediaElem.onplaying = (e) => { if(!mediaElem.paused && !mediaElem.getAttribute("isHovering")) { mediaElem.removeAttribute('controls'); } };
1055 | mediaElem.onmouseover = (e) => { mediaElem.controls = true; mediaElem.setAttribute("isHovering", true); };
1056 | mediaElem.onmouseout = (e) => { if(!mediaElem.paused ) { mediaElem.removeAttribute('controls'); mediaElem.removeAttribute("isHovering"); } };
1057 |
1058 | let vidComp = mediaElem.closest('div[data-testid="videoComponent"]');
1059 | if(vidComp)
1060 | {
1061 | if(vidComp.childElementCount > 1)
1062 | {
1063 | let tab = vidComp.lastElementChild;
1064 | if(tab)
1065 | {
1066 | tab.remove();
1067 | }
1068 | }
1069 | }
1070 | }
1071 | }
1072 | else if(toggleHQImg.enabled === true)
1073 | {
1074 | let hqImg = mediaInfo.data.getContentURL();
1075 | updateImgSrc(mediaElem, bg, hqImg);
1076 | }
1077 |
1078 | addCustomCtxMenu(tweetData, mediaInfo, mediaInfo.linkElem);
1079 | // mediaElem.setAttribute(modifiedAttr, "");
1080 |
1081 | updateElemPadding(elemCnt, bg, tweetPhoto);
1082 | doOnAttributeChange(tweetPhoto, (container) => updateElemPadding(elemCnt, bg, container), true);
1083 | }
1084 |
1085 | function updateContentElements(tweetElem, tweetData, mediaInfos)
1086 | {
1087 | if(tweetData == null || mediaInfos == null) { return; }
1088 |
1089 | let elemCnt = mediaInfos.length;
1090 | for(let i = 0; i < elemCnt; i++)
1091 | {
1092 | updateContentElement(tweetElem, tweetData, mediaInfos[i], i, elemCnt);
1093 | }
1094 |
1095 | updateContentLayout(tweetElem, tweetData, mediaInfos);
1096 | }
1097 |
1098 | async function updatePadder(tweetElem, ratio)
1099 | {
1100 | let padder = await awaitElem(tweetElem, 'div[id^="id_"] [thd_customctx]');
1101 | if(padder == null) { return; }
1102 |
1103 | padder = padder.closest('div[id^="id_"]').querySelector('div[style^="padding-bottom"]');
1104 |
1105 | const modPaddingAttr = "modifiedPadding";
1106 |
1107 | if(padder != null && !addHasAttribute(padder, modPaddingAttr))
1108 | {
1109 | const padderParent = padder.parentElement;
1110 | const flexer = padder.closest('div[id^="id_"] > div');
1111 | const bg = flexer.querySelector('div[style^="background"] > div');
1112 |
1113 | padder.style = `padding-bottom: ${ratio}%;`;
1114 | flexer.style = "align-self:normal; !important"; //Counteract Twitter's new variable width display of content that is rather wasteful of screenspace
1115 | if(bg) { bg.style.width = "100%"; }
1116 |
1117 | padderParent.removeAttribute('style');
1118 |
1119 | doOnAttributeChange(padder, (padderElem) => { if(padderElem.getAttribute("modifiedPadding") == null) { padderElem.style = "padding-bottom: " + (ratio) + "%;";} })
1120 | if(!addHasAttribute(padderParent, modPaddingAttr))
1121 | {
1122 | doOnAttributeChange(padderParent, (padderParentElem) => { if(padderParentElem.getAttribute("modifiedPadding") == null) { padderParentElem.removeAttribute('style');} })
1123 | }
1124 | }
1125 | }
1126 |
1127 | function updateContentLayout(tweetElem, tweetData, mediaElems)
1128 | {
1129 | processBlurButton(tweetElem);
1130 |
1131 | let elemCnt = mediaElems.length;
1132 |
1133 | let ratio = (mediaElems[0].data.height / mediaElems[0].data.width);
1134 |
1135 | if(elemCnt < 2 && mediaElems[0].data.isVideo)
1136 | {
1137 | let innerHeight = window.innerHeight;
1138 | let curRatio = (innerHeight / curLayoutWidth) * 0.8;
1139 | if(curRatio < ratio) ratio = curRatio;
1140 | }
1141 | ratio *= 100;
1142 | if (elemCnt == 2)
1143 | {
1144 | let img1 = mediaElems[0];
1145 | let img2 = mediaElems[1];
1146 | let img1Ratio = img1.data.height / img1.data.width;
1147 | let img2Ratio = img2.data.height / img2.data.width;
1148 | let imgToRatio = img1Ratio > img2Ratio ? img1 : img2;
1149 | ratio = (imgToRatio.data.height / imgToRatio.data.width);
1150 |
1151 | img1.bgElem.style.backgroundSize = "cover";
1152 | img2.bgElem.style.backgroundSize = "cover";
1153 | img1.tweetPhotoElem.parentElement.removeAttribute("style");
1154 | img2.tweetPhotoElem.parentElement.removeAttribute("style");
1155 |
1156 | if (img1.flex == "row")
1157 | {
1158 | if (imgToRatio.data.height > imgToRatio.data.width)
1159 | {
1160 | ratio *= 0.5;
1161 | }
1162 | }
1163 | else
1164 | {
1165 | //if(ratio > 1.0) { ratio = ((ratio - 1.0) * 0.5) + 1.0;}
1166 | ratio *= 0.5;
1167 | }
1168 |
1169 | if (imgToRatio.data.isVideo && imgToRatio.data.width > imgToRatio.data.height)
1170 | {
1171 | ratio = (img1.data.height + img2.data.height) / img1.data.width;
1172 | const padderly = tweetElem.querySelector('div[id^="id_"] div[style^="padding-bottom"]');
1173 | padderly.parentElement.lastElementChild.firstElementChild.style.flexDirection = "column";
1174 | }
1175 |
1176 | ratio = Math.min(ratio, 3.0);
1177 | ratio = ratio * 100;
1178 | }
1179 | else if (elemCnt == 3 && mediaElems[0].flex == "row")
1180 | {
1181 | let img1 = mediaElems[0];
1182 | let img1Ratio = img1.data.height / img1.data.width;
1183 | if (img1Ratio < 1.10 && img1Ratio > 0.9) { img1.bgElem.style.backgroundSize = "contain"; }
1184 | }
1185 | else if (elemCnt == 4)
1186 | {
1187 | if (mediaElems[0].data.width > mediaElems[0].data.height &&
1188 | mediaElems[1].data.width > mediaElems[1].data.height &&
1189 | mediaElems[2].data.width > mediaElems[2].data.height &&
1190 | mediaElems[3].data.width > mediaElems[3].data.height)
1191 | {} //All-wide 4-panel already has an optimal layout by default.
1192 | else if (mediaElems[0].data.width > mediaElems[0].data.height)
1193 | {
1194 | // ratio = 100;
1195 | let img1Ratio = mediaElems[0].data.height / mediaElems[0].data.width;
1196 | let img2Ratio = mediaElems[1].data.height / mediaElems[1].data.width;
1197 | let img3Ratio = mediaElems[2].data.height / mediaElems[2].data.width;
1198 | let img4Ratio = mediaElems[3].data.height / mediaElems[3].data.width;
1199 | let minImg = img1Ratio > img2Ratio ? mediaElems[1] : mediaElems[0];
1200 |
1201 | ratio = (mediaElems[0].data.height + mediaElems[1].data.height) / minImg.data.width;
1202 | ratio *= 100;
1203 | }
1204 | }
1205 |
1206 |
1207 | updatePadder(tweetElem, ratio);
1208 | watchForChange(tweetElem, argsChildAndSub, (tweet, mutes) => { updatePadder(tweetElem, ratio); });
1209 |
1210 |
1211 | /* for (let i = 0; i < elemCnt; i++)
1212 | {
1213 | let curImg = mediaElems[i];
1214 | updateImgSrc(curImg, curImg.bgElem, curImg.hqSrc);
1215 | doOnAttributeChange(curImg.layoutContainer, () => { updateImgSrc(curImg, curImg.bgElem, curImg.hqSrc) });
1216 | }*/
1217 |
1218 | //Annoying Edge....edge-case. Have to find this random class name generated element and remove its align so that elements will expand fully into the feed column
1219 | let edgeCase = getCSSRuleContainingStyle('align-self', ['.r-'], 0, 'flex-start');
1220 | if (edgeCase != null)
1221 | {
1222 | edgeCase.style.setProperty('align-self', "inherit");
1223 | }
1224 |
1225 | // if(padder != null)
1226 | // {
1227 | // doOnAttributeChange(padder, (padderElem) => { if(padderElem.getAttribute("modifiedPadding") == null) { padderElem.style = "padding-bottom: " + (ratio) + "%;";} })
1228 | // doOnAttributeChange(padder.parentElement, (padderParentElem) => { if(padderParentElem.getAttribute("modifiedPadding") == null) { padderParentElem.style = "";} })
1229 | // }
1230 | }
1231 |
1232 |
1233 | function isAdvert(tweet)
1234 | {
1235 | let impression = tweet.querySelector('div[data-testid="placementTracking"] div[data-testid$="impression-pixel"]');
1236 | if(impression)
1237 | {
1238 | tweet.style.display = "none";
1239 | return true;
1240 | }
1241 | return false;
1242 | }
1243 |
1244 | const mediaInfoBuffer = [8];
1245 | const elemsQueryBuffer = [8];
1246 |
1247 | async function processTweetContent(tweet, tweetData)
1248 | {
1249 | let medias = tweetData.media;
1250 | let elemsQuery = new Array(medias.length);
1251 | let mediaInfos = new Array(medias.length);
1252 |
1253 | for(let i = 0; i < medias.length; i++)
1254 | {
1255 | let mediaData = tweetData.getMediaData(i);
1256 |
1257 | mediaInfos[i] = { data: mediaData, mediaElem: null, linkElem: null, tweetPhotoElem: null, bgElem: null, flex: null };
1258 |
1259 | if(mediaData.isPhoto === true)
1260 | {
1261 | let srcID = mediaData.media_url.substring(mediaData.media_url.lastIndexOf('/') + 1).split('?')[0].split('.')[0];
1262 | elemsQuery[i] = awaitElem(tweet, `[src*="${srcID}"]`, argsChildAndSub);
1263 | }
1264 | else
1265 | {
1266 | elemsQuery[i] = awaitElem(tweet, `video[poster^="${mediaData.media_url.split('?')[0]}"]`, argsChildAndSub);
1267 | }
1268 |
1269 | }
1270 |
1271 | Promise.allSettled(elemsQuery).then((values) =>
1272 | {
1273 | for(let i = 0; i < values.length; i++)
1274 | {
1275 | mediaInfos[i].mediaElem = values[i].value;
1276 | }
1277 | updateContentElements(tweet, tweetData, mediaInfos);
1278 | });
1279 | }
1280 |
1281 | function processTweet(tweet, tweetObserver)
1282 | {
1283 | tweetObserver.disconnect();
1284 | if (tweet == null /*|| (!isOStatusPage() && tweet.querySelector('div[data-testid="placementTracking"]') == null)*/ ) { return false; } //If video, should have placementTracking after first mutation
1285 | if (tweet.getAttribute(modifiedAttr) != null || tweet.querySelector(`[${modifiedAttr}]`) ) { return true; }
1286 |
1287 | addHasAttribute(tweet, modifiedAttr);
1288 |
1289 | if(toggleMakeLinksVX.enabled)
1290 | {
1291 | awaitElem(tweet, 'a:has(> time)', { childList: true, subtree: true, attributes: false}).then((linky) => {
1292 | linky.href = replaceWithVX(linky.href);
1293 | });
1294 | }
1295 | if(isAdvert(tweet)) { return; }
1296 |
1297 | let tweetData = getTweetData(tweet);
1298 | if(tweetData == null) { return; }
1299 |
1300 |
1301 | if(tweetData.hasMedia) { processTweetContent(tweet, tweetData); }
1302 | if(tweetData.isQuote && tweetData.quote.hasMedia) {
1303 | tweetData.quote.tweetElem = tweet;
1304 | processTweetContent(tweet, tweetData.quote);
1305 | }
1306 | }
1307 |
1308 | async function listenForMediaType(tweet)
1309 | {
1310 | if (addHasAttribute(tweet, "thd_observing")) { return; }
1311 |
1312 | // if(!setupFilters(tweet)) { return; }
1313 | const tweetObserver = new MutationObserver((muteList, observer) => {
1314 |
1315 | processTweet(tweet, observer);
1316 | tweetObserver.observe(tweet, { attributes: true, childList: true, subtree: true });
1317 | });
1318 |
1319 | processTweet(tweet, tweetObserver);
1320 | tweetObserver.observe(tweet, { attributes: true, childList: true, subtree: true });
1321 | }
1322 |
1323 | const topicsFilter = 'a[href^="/i/topics/"]';
1324 | const likedFilter = 'a[href^="/i/user/"]';
1325 | const followsFilter = 'a[href="/i/timeline"]';
1326 |
1327 | const setupToggle = function(elem, toggle)
1328 | {
1329 | elem.style.display = toggle.enabled ? "block" : "none";
1330 | toggle.listen((e)=>{
1331 | elem.style.display = e.detail.toggle.enabled ? "block" : "none";
1332 | });
1333 | }
1334 |
1335 | /*
1336 | function setupFilters(tweet)
1337 | {
1338 | return true;
1339 | let socialCtx = tweet.querySelector('span[data-testid="socialContext"]');
1340 | if(socialCtx != null)
1341 | {
1342 | let root = tweet.closest('[data-testid="cellInnerDiv"]');
1343 |
1344 | let topics = tweet.querySelector(topicsFilter);
1345 | if(topics != null)
1346 | {
1347 | setupToggle(root, toggleTopics);
1348 | if(!toggleTopics.enabled)
1349 | {
1350 | root.removeChild(root.firstElementChild);
1351 | }
1352 | return toggleTopics.enabled;
1353 | }
1354 |
1355 | let followed = tweet.querySelector(followsFilter);
1356 | if(followed != null)
1357 | {
1358 | if(!toggleFollowed.enabled)
1359 | {
1360 | root.removeChild(root.firstElementChild);
1361 | }
1362 | setupToggle(root, toggleFollowed);
1363 | return toggleFollowed.enabled;
1364 | }
1365 |
1366 | let liked = tweet.querySelector(`likedFilter`);
1367 | if(liked != null)
1368 | {
1369 | if(liked.href.includes('/user/') && root.firstElementChild.className.split(' ').length < 4)
1370 | {
1371 | //reply
1372 | return true;
1373 | }
1374 | if(!toggleLiked.enabled)
1375 | {
1376 | root.removeChild(root.firstElementChild);
1377 | }
1378 | setupToggle(root, toggleLiked);
1379 |
1380 | return toggleLiked.enabled;
1381 | }
1382 | Bugs to iron out
1383 | // let retweet = tweet.querySelector('a[href^="/"][dir="auto"][role="link"]');
1384 | // if(retweet != null)
1385 | // {
1386 | // setupToggle(root, toggleRetweet);
1387 | // }
1388 |
1389 |
1390 | }
1391 | return true;
1392 | }
1393 | */
1394 |
1395 | //<--> COLUMN RESIZING START<-->//
1396 | var primaryColumnCursorDistToEdge = 900;
1397 | var primaryColumnMouseDownPos = 0;
1398 | var primaryColumnResizing = false;
1399 | var primaryColumnPreWidth = 600;
1400 | var maxWidthClass = null;
1401 | var preCursor;
1402 | var headerColumn = null;
1403 |
1404 | function primaryColumnResizer(primaryColumn, mouseEvent, mouseDown, mouseUp)
1405 | {
1406 | if(mouseDown && mouseEvent.button != 0) { return; }
1407 | let primaryRect = primaryColumn.getBoundingClientRect();
1408 | let localPosX = mouseEvent.clientX - primaryRect.left;
1409 | primaryColumnCursorDistToEdge = Math.abs(primaryRect.width - localPosX);
1410 |
1411 | if (mouseUp || primaryColumnCursorDistToEdge > 180)
1412 | {
1413 | primaryColumnResizing = false;
1414 | if (mouseUp)
1415 | {
1416 | let primarySize = parseInt(maxWidthClass.style.getPropertyValue('max-width'));
1417 | updateLayoutWidth(primarySize, true);
1418 | }
1419 | };
1420 | if (primaryColumnCursorDistToEdge < 6 || primaryColumnResizing)
1421 | {
1422 | preCursor = document.body.style.cursor;
1423 | document.body.style.cursor = "ew-resize";
1424 | if (mouseDown)
1425 | {
1426 | primaryColumnMouseDownPos = mouseEvent.pageX;
1427 | primaryColumnResizing = true;
1428 | primaryColumnPreWidth = parseInt(maxWidthClass.style.getPropertyValue('max-width'));
1429 | }
1430 | }
1431 | else
1432 | {
1433 | document.body.style.cursor = (preCursor == "ew-resize") ? "auto" : preCursor;
1434 | }
1435 | if (primaryColumnResizing)
1436 | {
1437 | mouseEvent.preventDefault();
1438 | let columnOffset = mouseEvent.pageX - primaryColumnMouseDownPos;
1439 | let newColumnSize = primaryColumnPreWidth + columnOffset;
1440 | newColumnSize = Math.max(250, newColumnSize);
1441 | updateLayoutWidth(newColumnSize);
1442 | }
1443 | }
1444 |
1445 | var curLayoutWidth = 600;
1446 |
1447 | function updateLayoutWidth(width, finalize)
1448 | {
1449 | curLayoutWidth = width;
1450 | if(!toggleTimelineScaling.enabled) { return; }
1451 |
1452 | maxWidthClass.style.setProperty('max-width', width + "px");
1453 | if (finalize)
1454 | {
1455 | headerColumn = document.body.querySelector('HEADER');
1456 | headerColumn.style.flexGrow = 0.2;
1457 | headerColumn.style.webkitBoxFlex = 0.2;
1458 | setUserPref(usePref_MainWidthKey, width);
1459 | }
1460 | }
1461 |
1462 |
1463 | function refreshLayoutWidth()
1464 | {
1465 | let width = getUserPref(usePref_MainWidthKey, 600);
1466 | updateLayoutWidth(width, true);
1467 | }
1468 | //<--> COLUMN RESIZING END <-->//
1469 |
1470 | //<--> TIMELINE PROCESSING <-->//
1471 | async function onTimelineContainerChange(container, mutations)
1472 | {
1473 | replaceMuskratText(container);
1474 | LogMessage("on timeline container change");
1475 | let tl = await awaitElem(container, 'DIV[style*="position:"]', { childList: true, subtree: true, attributes: true });
1476 | observeTimeline(tl);
1477 | }
1478 |
1479 | function onTimelineChange(addedNodes)
1480 | {
1481 | // replaceMuskratText(document.body);
1482 | if (addedNodes.length == 0) { LogMessage("no added nodes"); return; }
1483 | addedNodes.forEach((child) =>
1484 | {
1485 | // if(addHasAttribute(child, modifiedAttr)) { return; }
1486 | awaitElem(child, 'ARTICLE', argsChildAndSub).then(listenForMediaType);
1487 | });
1488 | }
1489 |
1490 | function observeTimeline(tl)
1491 | {
1492 | if (!addHasAttribute(tl, "thd_observing_timeline"))
1493 | {
1494 | LogMessage("starting timeline observation");
1495 | const childNodes = Array.from(tl.childNodes);
1496 | onTimelineChange(childNodes);
1497 |
1498 | watchForAddedNodes(tl, false, { attributes: false, childList: true, subtree: false }, onTimelineChange);
1499 | }
1500 | }
1501 |
1502 | async function watchForTimeline(primaryColumn)
1503 | {
1504 | let section = await awaitElem(primaryColumn, 'section[role="region"]', argsChildAndSub);
1505 |
1506 | if (addHasAttribute(section, modifiedAttr)) { return; }
1507 |
1508 | if(!addHasAttribute(section.parentElement, modifiedAttr)) {
1509 | watchForAddedNodes(section.parentElement, false,{ attributes: false, childList: true, subtree: false }, (addedNodes, mutes) => {
1510 | watchForTimeline(primaryColumn);
1511 |
1512 | });
1513 | }
1514 |
1515 | const checkTimeline = async function ()
1516 | {
1517 | let tl = await awaitElem(section, 'DIV[style*="position:"]', { childList: true, subtree: true, attributes: true });
1518 | let progBar = tl.querySelector('[role="progressbar"]');
1519 | if (progBar)
1520 | {
1521 | // Wait for an Article to show up before proceeding
1522 | // LogMessage("Has Prog Bar, Awaiting Article");
1523 | let art = await awaitElem(section, "article", { childList: true, subtree: true, attributes: true });
1524 | // LogMessage("Found Article");
1525 | }
1526 |
1527 | let tlContainer = tl.parentElement;
1528 | if (!addHasAttribute(tlContainer, "thd_observing_timeline"))
1529 | {
1530 | observeTimeline(tl);
1531 | watchForChange(tlContainer, { attributes: false, childList: true }, (tlc, mutes) => { onTimelineContainerChange(tlc, mutes); });
1532 | }
1533 |
1534 | };
1535 |
1536 | checkTimeline();
1537 |
1538 | let progBarObserver = new MutationObserver((mutations) => {
1539 | progBarObserver.disconnect();
1540 | checkTimeline();
1541 | progBarObserver.observe(section, { attributes: false, childList: true });
1542 | });
1543 | progBarObserver.observe(section, { attributes: false, childList: true });
1544 | }
1545 |
1546 | var pageWidthLayoutRule;
1547 |
1548 |
1549 | async function watchPrimaryColumn(main, primaryColumn)
1550 | {
1551 | if(primaryColumn == null) { return; }
1552 | //
1553 |
1554 | awaitElem(primaryColumn, 'nav', argsChildAndSub).then((nav) =>
1555 | {
1556 | let navCont = nav.parentElement;
1557 | if (addHasAttribute(navCont, modifiedAttr)) { return; }
1558 | watchForChange(navCont, argsChildOnly, () => { watchForTimeline(primaryColumn, navCont); });
1559 | });
1560 |
1561 | if (!addHasAttribute(primaryColumn.firstElementChild, modifiedAttr)) {
1562 | //Watch to handle case where timelines are partially lost when clicking on the quoted post name.
1563 | watchForChange(primaryColumn.firstElementChild, argsChildOnly, () => {
1564 | watchForTimeline(primaryColumn);
1565 | });
1566 | }
1567 |
1568 | if (addHasAttribute(primaryColumn, modifiedAttr)) { return; }
1569 | hideForYou(primaryColumn);
1570 |
1571 | if(pageWidthLayoutRule == null) { pageWidthLayoutRule = getCSSRuleContainingStyle('width', (("." + main.className).replace(' ', ' .')).split(' ')); }
1572 |
1573 | if(toggleTimelineScaling.enabled)
1574 | {
1575 | pageWidthLayoutRule.style.setProperty('width', "100%");
1576 |
1577 | let primaryColumnGrp = primaryColumn.parentElement.parentElement;
1578 | let columnClassNames = ("." + primaryColumn.className.replace(" ", " .")).split(' ');
1579 |
1580 | maxWidthClass = getCSSRuleContainingStyle("max-width", columnClassNames);
1581 | getUserPref(usePref_MainWidthKey, 600).then((userWidth) => updateLayoutWidth(userWidth, true));
1582 |
1583 | primaryColumnGrp.addEventListener('mousemove', (e) => { primaryColumnResizer(primaryColumn, e, false, false) });
1584 | primaryColumnGrp.addEventListener('mousedown', (e) => { primaryColumnResizer(primaryColumn, e, true, false) });
1585 | window.addEventListener('mouseup', (e) => { primaryColumnResizer(primaryColumn, e, false, true) });
1586 | document.addEventListener('mouseup', (e) => { primaryColumnResizer(primaryColumn, e, false, true) });
1587 | }
1588 |
1589 | watchForTimeline(primaryColumn);
1590 | }
1591 |
1592 | async function onMainChange(main, mutations)
1593 | {
1594 |
1595 | replaceMuskratText(document.body);
1596 | awaitElem(main, 'div[data-testid="primaryColumn"]', argsChildAndSub).then((primaryColumn) =>{
1597 | watchPrimaryColumn(main, primaryColumn);
1598 | replaceMuskratText(document.body);
1599 | });
1600 |
1601 | watchSideBar(main);
1602 | }
1603 |
1604 |
1605 | function replaceMuskratText(root)
1606 | {
1607 | let labels = root.querySelectorAll('span, span > span');
1608 |
1609 | for(let i = 0; i < labels.length; i++)
1610 | {
1611 | let label = labels[i];
1612 | if(label.innerText == "Post")
1613 | {
1614 | label.innerText = "Tweet";
1615 | }
1616 | else if(label.innerText == "Posts") { label.innerText == "Tweets"; }
1617 | }
1618 | }
1619 |
1620 |
1621 | function hideForYou(primaryColumn)
1622 | {
1623 | if(!toggleDisableForYou.enabled) { return; }
1624 |
1625 | let mainTabs = primaryColumn.querySelector('div[role="tablist"]');
1626 | if(mainTabs)
1627 | {
1628 | let tabs = mainTabs.querySelectorAll('div[role="presentation"] > a[href="/home"]');
1629 | if(tabs.length > 1)
1630 | {
1631 | tabs[0].parentElement.style.display = 'none';
1632 | tabs[1].parentElement.click();
1633 | }
1634 | }
1635 | }
1636 |
1637 | //<--> RIGHT SIDEBAR CONTENT <-->//
1638 |
1639 | //<--> Save/Load User Cutom Prefs <-->//
1640 | const usePref_MainWidthKey = "thd_primaryWidth";
1641 | const usePref_hideTrendingKey = "thd_hideTrending";
1642 | const usePref_lastTopicsClearTime = "thd_lastTopicsClearTime";
1643 |
1644 | var toggleNSFW;
1645 | var toggleHQImg;
1646 | var toggleHQVideo;
1647 | var toggleLiked;
1648 | var toggleFollowed;
1649 | var toggleRetweet;
1650 | var toggleTopics;
1651 | var toggleClearTopics;
1652 | var toggleTimelineScaling;
1653 | var toggleAnalyticsDisplay;
1654 | var toggleDisableForYou;
1655 | var toggleMakeLinksVX;
1656 |
1657 | var prefsLoaded = false;
1658 |
1659 | async function watchSideBar(main)
1660 | {
1661 | awaitElem(main, 'div[data-testid="sidebarColumn"]', argsChildAndSub).then((sideBar) =>
1662 | {
1663 | awaitElem(sideBar, 'section[role="region"] > div:has([data-testid="trend"])', argsChildAndSub).then((sideBarTrending) =>
1664 | {
1665 | setupTrendingControls(sideBarTrending);
1666 | setupToggles(sideBar);
1667 | clearTopicsAndInterests();
1668 | });
1669 | });
1670 | }
1671 |
1672 | async function getToggleObj(name, defaultVal)
1673 | {
1674 | let enable = await getUserPref(name, defaultVal);
1675 | return {enabled: enable, elem: null, name: name, onChanged: new EventTarget(), listen: function(func) { this.onChanged.addEventListener(this.name, func); }};
1676 | }
1677 |
1678 | const nsfwBlurBtnSelector = 'div[role="button"][style*="backdrop-filter: blur(4px);"][style*="background-color:"]';
1679 | const nsfwBlurBtnSubSelector = 'div[dir="ltr"] > span > span';
1680 |
1681 | const nsfwBlurBtnElemQuery = `${nsfwBlurBtnSelector}:has(> ${nsfwBlurBtnSubSelector})`;
1682 | const nsfwBlurBtnElemSelector = `${nsfwBlurBtnSelector} > ${nsfwBlurBtnSubSelector}`;
1683 |
1684 | const nsfwBlurRootQuery = `div:has(> div > div > ${nsfwBlurBtnElemSelector}):has(div[data-testid="tweetPhoto"])`;
1685 | const nsfwBlurUIQuery = `${nsfwBlurRootQuery} > div:has(${nsfwBlurBtnSelector}):has(${nsfwBlurBtnSubSelector})`;
1686 | const nsfwBlurBtnQuery = `${nsfwBlurRootQuery} ${nsfwBlurBtnElemQuery}`;
1687 | const nsfwBlurFilterQuery = `${nsfwBlurRootQuery} > div:has(div[data-testid="tweetPhoto"])`;
1688 |
1689 | function toggle_nsfwBlurStyle(enabled)
1690 | {
1691 | if(!enabled)
1692 | {
1693 | addGlobalStyle(//Media thumb view overlay css target
1694 | `div[aria-label^="Timeline:"] div[data-testid="cellInnerDiv"] li[role="listitem"] div:has(> div > svg > g > path) > div:has(img) {
1695 | -webkit-filter: blur(0px) !important;
1696 | filter: blur(0px) !important;
1697 | }
1698 |
1699 | div[aria-label^="Timeline:"] div[data-testid="cellInnerDiv"] li[role="listitem"] div:has(> svg > g > path) {
1700 | display: none !important;
1701 | }
1702 |
1703 | article[data-testid="tweet"] div[aria-labelledby^="id_"] :not([tabindex]) > div:has(> div > div > div[role="button"][style*="blur"])
1704 | {
1705 | > div:not(:has(div[data-testid^="tweet"]), :has([style^="transition-d"] > [aria-label])) { display: none !important; }
1706 | > div > div > div[role="button"] { display: none !important; }
1707 | > div:has(div[data-testid^="tweet"]) { -webkit-filter: none !important; filter: none !important; }
1708 | }
1709 | `, "nsfwblur");
1710 |
1711 | }
1712 | if(enabled)
1713 | {
1714 | removeGlobalStyle("nsfwblur");
1715 | }
1716 | }
1717 | /*
1718 | article[data-testid="tweet"] div[aria-labelledby^="id_"] [data-testid="videoComponent"] > div[tabindex]:has([style^="transition-d"] > [aria-label])
1719 | {
1720 | display: none !important;
1721 | }*/
1722 |
1723 | async function loadToggleValues()
1724 | {
1725 | if(prefsLoaded === true) { return; }
1726 |
1727 | await Promise.allSettled([
1728 | (async() => { toggleNSFW = await getToggleObj("thd_blurNSFW", false) })(),
1729 | (async() => { toggleHQImg = await getToggleObj("thd_toggleHQImg", true) })(),
1730 | (async() => { toggleHQVideo = await getToggleObj("thd_toggleHQVideo", true) })(),
1731 | (async() => { toggleLiked = await getToggleObj("thd_toggleLiked", true) })(),
1732 | (async() => { toggleFollowed = await getToggleObj("thd_toggleFollowed", false) })(),
1733 | (async() => { toggleRetweet = await getToggleObj("thd_toggleRetweet", false) })(),
1734 | (async() => { toggleTopics = await getToggleObj("thd_toggleTopics", false) })(),
1735 | (async() => { toggleClearTopics = await getToggleObj("thd_toggleClearTopics", false) })(),
1736 | (async() => { toggleTimelineScaling = await getToggleObj("thd_toggleTimelineScaling", true) })(),
1737 | (async() => { toggleAnalyticsDisplay = await getToggleObj("thd_toggleAnalyticsDisplay", false) })(),
1738 | (async() => { toggleDisableForYou = await getToggleObj("thd_toggleDisableForYou", false) })(),
1739 | (async() => { toggleMakeLinksVX = await getToggleObj("thd_makeLinksVX", true) })()
1740 | ]);
1741 |
1742 | prefsLoaded = true;
1743 |
1744 | if(!toggleAnalyticsDisplay.enabled)
1745 | {
1746 | addGlobalStyle('div[role="group"] > div:has(> a[href$="/analytics"]) { display: none !important; }', "analyticsStyle");
1747 | }
1748 | toggleAnalyticsDisplay.onChanged.addEventListener(toggleAnalyticsDisplay.name, (e) =>
1749 | {
1750 | if(!toggleAnalyticsDisplay.enabled) { addGlobalStyle('div[role="group"] > div:has(> a[href$="/analytics"]) { display: none !important; }', "analyticsStyle"); }
1751 | else { removeGlobalStyle("analyticsStyle"); }
1752 | });
1753 | toggleNSFW.onChanged.addEventListener(toggleNSFW.name,(e) =>
1754 | {
1755 | toggle_nsfwBlurStyle(toggleNSFW.enabled);
1756 | });
1757 | toggle_nsfwBlurStyle(toggleNSFW.enabled);
1758 | }
1759 |
1760 | async function setupToggles(sidePanel)
1761 | {
1762 | if(sidePanel.querySelector('.thd_settings_collapsible')){ return; }
1763 | sidePanel = sidePanel.querySelector('div:has(> div > nav)');
1764 |
1765 | let settingsFold = document.createElement('button');
1766 | settingsFold.className = 'thd_settings_collapsible';
1767 | settingsFold.innerText = "Twitter AutoHD Settings:";
1768 | let settingsContainer = document.createElement('div');
1769 | settingsContainer.className = 'thd_settings_content thd_settings_content_closed';
1770 | let footer_elem = sidePanel.querySelector('div:has(> nav)');
1771 | sidePanel.insertBefore(settingsFold, footer_elem);
1772 | sidePanel.insertBefore(settingsContainer, footer_elem);
1773 | sidePanel = settingsContainer;
1774 | settingsFold.addEventListener("click", function() {
1775 | this.classList.toggle("thd_settings_active");
1776 | var content = this.nextElementSibling;
1777 | content.classList.toggle('thd_settings_content_closed');
1778 | });
1779 | createToggleOption(sidePanel, toggleNSFW, "NSFW Blur: ", "ON", "OFF");
1780 | createToggleOption(sidePanel, toggleHQImg, "HQ Image Loading: ", "ON", "OFF");
1781 | createToggleOption(sidePanel, toggleHQVideo, "HQ Video Loading: ", "ON", "OFF");
1782 | createToggleOption(sidePanel, toggleTimelineScaling, "Timeline Width Scaling: ", "ON", "OFF");
1783 | createToggleOption(sidePanel, toggleMakeLinksVX, "Replace Links with VX Link: ", "ON", "OFF");
1784 | createToggleOption(sidePanel, toggleLiked, "Liked Tweets: ", "ON", "OFF");
1785 | createToggleOption(sidePanel, toggleFollowed, "Followed By Tweets: ", "ON", "OFF");
1786 | createToggleOption(sidePanel, toggleRetweet, "Retweets: ", "ON", "OFF");
1787 | createToggleOption(sidePanel, toggleTopics, "Topic Tweets: ", "ON", "OFF");
1788 | createToggleOption(sidePanel, toggleClearTopics, "Interests/Topics Prefs AutoClear: ", "ON", "OFF");
1789 | createToggleOption(sidePanel, toggleAnalyticsDisplay, "Show Post Views: ", "ON", "OFF");
1790 | createToggleOption(sidePanel, toggleDisableForYou, 'Disable "For You" page: ', "ON", "OFF");
1791 | }
1792 |
1793 | function createToggleOption(sidePanel, toggleState, toggleText, toggleOnText, toggleOffText)
1794 | {
1795 | toggleState.elem = sidePanel.querySelector('#' + toggleState.name);
1796 | toggleOnText = toggleText + toggleOnText;
1797 | toggleOffText = toggleText + toggleOffText;
1798 | if (toggleState.elem == null)
1799 | {
1800 | toggleState.elem = createToggleButton(toggleState.enabled ? toggleOnText : toggleOffText, toggleState.name);
1801 | toggleState.elem.classList.toggle("thd_settings_toggle_enabled", toggleState.enabled);
1802 |
1803 | toggleState.elem.addEventListener('click', (e) =>
1804 | {
1805 | toggleState.enabled = toggleState.enabled ? false : true;
1806 | toggleState.elem.classList.toggle("thd_settings_toggle_enabled", toggleState.enabled);
1807 | setUserPref(toggleState.name, toggleState.enabled);
1808 | toggleState.onChanged.dispatchEvent(new CustomEvent(toggleState.name, {'detail':{'toggle':toggleState}}));
1809 | toggleState.elem.innerHTML = toggleState.enabled ? toggleOnText : toggleOffText;
1810 | }, false);
1811 |
1812 | sidePanel.appendChild(toggleState.elem);
1813 | }
1814 | }
1815 |
1816 | var blurShowText = "";
1817 |
1818 | async function processBlurButton(tweet)
1819 | {
1820 | const getBlurText = function(blur)
1821 | {
1822 | return blur.querySelector('span > span').innerText;
1823 | }
1824 |
1825 | const blurBtn = tweet.querySelector(nsfwBlurBtnQuery);
1826 | if(blurBtn != null)
1827 | {
1828 | if(blurShowText == "")
1829 | {
1830 | blurShowText = getBlurText(blurBtn);
1831 | }
1832 | if(!toggleNSFW.enabled)
1833 | {
1834 | blurBtn.click();
1835 | }
1836 | blurBtn.style.display = toggleNSFW.enabled ? "block" : "none";
1837 |
1838 | watchForChange(tweet, {attributes: false, childList: true, subtree: true}, (blurParent, mutes) => {
1839 |
1840 | let curBlur = blurParent.querySelector(nsfwBlurBtnQuery);
1841 | if(curBlur == null) { return; }
1842 |
1843 | if(!toggleNSFW.enabled && getBlurText(curBlur) == blurShowText)
1844 | {
1845 | curBlur?.click();
1846 | }
1847 |
1848 | curBlur.style.display = toggleNSFW.enabled ? "block" : "none";
1849 | let span = curBlur.querySelector('span > span');
1850 |
1851 | if(!addHasAttribute(curBlur, modifiedAttr))
1852 | {
1853 | watchForChange(curBlur, {attributes:true, characterData: true, childList: true, subtree: true}, (blur, mutes) => {
1854 | curBlur = tweet.querySelector(nsfwBlurBtnQuery);
1855 | if(curBlur)
1856 | {
1857 | curBlur.style.display = toggleNSFW.enabled ? "block" : "none";
1858 | }
1859 | });
1860 | toggleNSFW.onChanged.addEventListener("nsfwToggleChanged", function(enabled) {
1861 | curBlur = tweet.querySelector(nsfwBlurBtnQuery);
1862 | if(curBlur)
1863 | {
1864 | curBlur.click();
1865 | curBlur.style.display = toggleNSFW.enabled ? "block" : "none";
1866 | }
1867 | });
1868 | }
1869 |
1870 | });
1871 | }
1872 | }
1873 |
1874 | async function setupTrendingControls(trendingBox)
1875 | {
1876 | const showStr = "Show Trending";
1877 | const hideStr = "Hide Trending";
1878 |
1879 | const setTrendingVisible = function (container, button, hidden)
1880 | {
1881 | container.style.maxHeight = hidden ? "20px" : "none";
1882 | button.innerText = hidden ? showStr : hideStr;
1883 | setUserPref(usePref_hideTrendingKey, hidden);
1884 | };
1885 |
1886 | if (!addHasAttribute(trendingBox, modifiedAttr))
1887 | {
1888 | let toggle = trendingBox.querySelector('#thd_toggleTrending');
1889 |
1890 | if (toggle == null)
1891 | {
1892 | toggle = createToggleButton(hideStr, "thd_toggleTrending");
1893 | toggle.addEventListener('click', (e) =>
1894 | {
1895 | let isHidden = toggle.innerText == hideStr;
1896 | setTrendingVisible(trendingBox, toggle, isHidden);
1897 | });
1898 | trendingBox.prepend(toggle);
1899 | }
1900 | getUserPref(usePref_hideTrendingKey, true).then((visible) =>
1901 | {
1902 | setTrendingVisible(trendingBox, toggle, visible);
1903 | watchForChange(trendingBox, argsChildAndSub, setupTrendingControls);
1904 | });
1905 |
1906 | }
1907 | }
1908 |
1909 | function createToggleButton(text, id)
1910 | {
1911 | const btn = document.createElement('button');
1912 | btn.innerText = text;
1913 | btn.id = id;
1914 | btn.className = 'thd_settings_toggle';
1915 | return btn;
1916 | }
1917 |
1918 | async function watchForComments(dialog)
1919 | {
1920 | let commentList = await awaitElem(dialog, 'div[style^="position: relative"]', argsChildAndSub);
1921 | observeTimeline(commentList);
1922 | }
1923 |
1924 | //<--> FULL-SCREEN IMAGE VIEW RELATED <-->//
1925 | async function onLayersChange(layers, mutation)
1926 | {
1927 |
1928 | if (mutation.addedNodes != null && mutation.addedNodes.length > 0)
1929 | {
1930 | const contentContainer = Array.from(mutation.addedNodes)[0];
1931 | if(addHasAttribute(contentContainer, 'thd_modified')) { return; }
1932 | const dialog = await awaitElem(contentContainer, 'div[role="dialog"]', argsChildAndSub);
1933 |
1934 | watchForComments(dialog);
1935 |
1936 | let ctxTarget = await awaitElem(dialog, 'img[alt="Image"],div[data-testid="videoPlayer"]', argsChildAndSub);
1937 |
1938 | const list = dialog.querySelector('ul[role="list"]');
1939 | let id = getIDFromURL(window.location.href);
1940 | let tweetData = tweets.get(id);
1941 | if(tweetData == null) { return; }
1942 |
1943 | if (list != null /* && !addHasAttribute(list, 'thd_modified')*/ )
1944 | {
1945 | const listItems = list.querySelectorAll('li');
1946 | const itemCnt = listItems.length;
1947 |
1948 | for (let i = 0; i < itemCnt; i++)
1949 | {
1950 | ctxTarget = await awaitElem(listItems[i], 'img[alt="Image"],div[data-testid="videoPlayer"]', argsChildAndSub);
1951 |
1952 | let mediaData = tweetData.getMediaData(i);
1953 | if(mediaData)
1954 | {
1955 | updateFullViewImage(ctxTarget, tweetData, mediaData);
1956 | }
1957 | }
1958 | }
1959 | else
1960 | {
1961 | let mediaData = tweetData.getMediaData(0);
1962 | updateFullViewImage(ctxTarget, tweetData, mediaData);
1963 | }
1964 |
1965 | }
1966 | }
1967 |
1968 | async function updateFullViewImage(ctxTarget, tweetData, mediaData)
1969 | {
1970 | // if (addHasAttribute(img, "thd_modified")) { return; }
1971 | let bg = ctxTarget.parentElement.querySelector('div') ?? ctxTarget.parentElement;
1972 |
1973 | let mediaInfo = { data: mediaData, linkElem: ctxTarget, mediaElem: ctxTarget };
1974 |
1975 | addCustomCtxMenu(tweetData, mediaInfo, ctxTarget);
1976 | if(toggleHQImg.enabled === true && !mediaData.isVideo === true)
1977 | {
1978 | let hqSrc = mediaData.getContentURL();
1979 | updateImgSrc(ctxTarget, bg, hqSrc);
1980 | doOnAttributeChange(ctxTarget, (ctxTarg) => { updateImgSrc(ctxTarg, bg, hqSrc); }, false);
1981 | }
1982 | }
1983 |
1984 | //<--> RIGHT-CLICK CONTEXT MENU STUFF START <-->//
1985 | var ctxMenu;
1986 | var ctxMenuList;
1987 | var ctxMenuOpenInNewTab;
1988 | var ctxMenuOpenVidInNewTab;
1989 | var ctxMenuSaveAs;
1990 | var ctxMenuSaveAsVid;
1991 | var ctxMenuCopyImg;
1992 | var ctxMenuCopyAddress;
1993 | var ctxMenuCopyVidAddress;
1994 | var ctxMenuGRIS;
1995 | var ctxMenuShowDefault;
1996 |
1997 | function initializeCtxMenu()
1998 | {
1999 | ctxMenu = document.createElement('div');
2000 | ctxMenu.style.zIndex = "500";
2001 | ctxMenu.id = "contextMenu";
2002 | ctxMenu.className = "context-menu";
2003 | ctxMenuList = document.createElement('ul');
2004 | //ctxMenuList.style.zIndex = 500;
2005 | ctxMenu.appendChild(ctxMenuList);
2006 |
2007 | ctxMenuOpenInNewTab = createCtxMenuItem(ctxMenuList, "Open Image in New Tab");
2008 | ctxMenuOpenVidInNewTab = createCtxMenuItem(ctxMenuList, "Open Video in New Tab");
2009 | ctxMenuSaveAs = createCtxMenuItem(ctxMenuList, "Save Image As");
2010 | ctxMenuSaveAsVid = createCtxMenuItem(ctxMenuList, "Save Video As");
2011 | ctxMenuCopyImg = createCtxMenuItem(ctxMenuList, "Copy Image");
2012 | ctxMenuCopyAddress = createCtxMenuItem(ctxMenuList, "Copy Image Link");
2013 | ctxMenuCopyVidAddress = createCtxMenuItem(ctxMenuList, "Copy Video Link");
2014 | ctxMenuGRIS = createCtxMenuItem(ctxMenuList, "Search Google for Image");
2015 | ctxMenuShowDefault = createCtxMenuItem(ctxMenuList, "Show Default Context Menu");
2016 |
2017 | document.body.appendChild(ctxMenu);
2018 | document.body.addEventListener('click', function (e) { setContextMenuVisible(false); });
2019 |
2020 | setContextMenuVisible(false);
2021 |
2022 | window.addEventListener('locationchange', function () {
2023 | setContextMenuVisible(false);
2024 | });
2025 | window.addEventListener('popstate',() => {
2026 | setContextMenuVisible(false);
2027 | });
2028 |
2029 | }
2030 |
2031 | function createCtxMenuItem(menuList, text)
2032 | {
2033 | let menuItem = document.createElement('LI');
2034 | menuItem.innerText = text;
2035 | menuList.appendChild(menuItem);
2036 | return menuItem;
2037 | }
2038 |
2039 | function mouseX(evt)
2040 | {
2041 | if (evt.pageX)
2042 | {
2043 | return evt.pageX;
2044 | }
2045 | else if (evt.clientX)
2046 | {
2047 | return evt.clientX + (document.documentElement.scrollLeft ?
2048 | document.documentElement.scrollLeft :
2049 | document.body.scrollLeft);
2050 | }
2051 | else
2052 | {
2053 | return null;
2054 | }
2055 | }
2056 |
2057 | function mouseY(evt)
2058 | {
2059 | if (evt.pageY)
2060 | {
2061 | return evt.pageY;
2062 | }
2063 | else if (evt.clientY)
2064 | {
2065 | return evt.clientY + (document.documentElement.scrollTop ?
2066 | document.documentElement.scrollTop :
2067 | document.body.scrollTop);
2068 | }
2069 | else
2070 | {
2071 | return null;
2072 | }
2073 | }
2074 |
2075 | function setContextMenuVisible(visible)
2076 | {
2077 | ctxMenu.style.display = visible ? "block" : "none";
2078 | }
2079 |
2080 | var selectedShowDefaultContext = false;
2081 | //To avoid the value being captured when setting up the event listeners.
2082 | function wasShowDefaultContextClicked()
2083 | {
2084 | return selectedShowDefaultContext;
2085 | }
2086 |
2087 | async function updateContextMenuLink(tweetData, mediaInfo)
2088 | {
2089 | if(mediaInfo == null) { return; }
2090 |
2091 | ctxMenu.setAttribute('selection', mediaInfo.data.media_id);
2092 |
2093 | let isImage = mediaInfo.mediaElem.tagName.toLowerCase() == "img";
2094 |
2095 | let imgVisibility = isImage ? "block" : "none";
2096 | let vidVisibility = isImage ? "none" : "block";
2097 |
2098 | ctxMenuOpenInNewTab.style.display = imgVisibility;
2099 | ctxMenuSaveAs.style.display = imgVisibility;
2100 | ctxMenuCopyImg.style.display = imgVisibility;
2101 | ctxMenuCopyAddress.style.display = imgVisibility;
2102 | ctxMenuGRIS.style.display = imgVisibility;
2103 |
2104 | ctxMenuOpenVidInNewTab.style.display = vidVisibility;
2105 | ctxMenuSaveAsVid.style.display = vidVisibility;
2106 | ctxMenuCopyVidAddress.style.display = vidVisibility;
2107 |
2108 | const copyAddress = function(url){ setContextMenuVisible(false); navigator.clipboard.writeText(url); };
2109 | const saveMedia = async function(url)
2110 | {
2111 | setContextMenuVisible(false);
2112 | await download(url, filenameFromMediaData(mediaInfo.data));
2113 | };
2114 | const openInNewTab = function(url)
2115 | {
2116 | setContextMenuVisible(false);
2117 | if (GM_OpenInTabMissing)
2118 | {
2119 | let lastWin = window;
2120 | window.open(url, '_blank');
2121 | lastWin.focus();
2122 | }
2123 | else { GM_openInTab(url, { active: false, insert: true, setParent: true, incognito: false }); }
2124 | };
2125 |
2126 |
2127 | //Image Context
2128 | if(isImage == true)
2129 | {
2130 | mediaInfo.mediaElem.crossOrigin = 'Anonymous'; //Needed to avoid browser preventing the Canvas from being copied when doing "Copy Image"
2131 |
2132 | ctxMenuOpenInNewTab.onmouseup = () => { openInNewTab(mediaInfo.data.getContentURL()) };
2133 | ctxMenuSaveAs.onmouseup = () => { saveMedia(mediaInfo.data.getContentURL()) };
2134 |
2135 | ctxMenuCopyImg.onmouseup = () =>
2136 | {
2137 | setContextMenuVisible(false);
2138 | try
2139 | {
2140 | let c = document.createElement('canvas');
2141 | c.width = mediaInfo.data.width;
2142 | c.height = mediaInfo.data.height;
2143 | c.getContext('2d').drawImage(mediaInfo.mediaElem, 0, 0, c.width, c.height);
2144 | c.toBlob((png) =>
2145 | {
2146 | navigator.clipboard.write([new ClipboardItem({
2147 | [png.type]: png })]);
2148 | }, "image/png", 1);
2149 | }
2150 | catch (err) { console.log(err); };
2151 | };
2152 | ctxMenuCopyAddress.onmouseup = (e) => { copyAddress(mediaInfo.data.getContentURL()); };
2153 | ctxMenuGRIS.onmouseup = () => { setContextMenuVisible(false);
2154 | window.open("https://www.google.com/searchbyimage?sbisrc=cr_1_5_2&image_url=" + mediaInfo.data.getContentURL()); };
2155 | }
2156 | else //Video
2157 | {
2158 | ctxMenuOpenVidInNewTab.onmouseup = () => { openInNewTab(mediaInfo.data.getContentURL()) };
2159 | if(!mediaInfo.mediaElem.hasAttribute("downloading"))
2160 | {
2161 | ctxMenuSaveAsVid.onmouseup = async() =>
2162 | {
2163 | mediaInfo.mediaElem.setAttribute("downloading","");
2164 | await saveMedia(mediaInfo.data.getContentURL());
2165 | mediaInfo.mediaElem.removeAttribute("downloading");
2166 | };
2167 | } else { ctxMenuSaveAsVid.style.display = "none"; }
2168 |
2169 | ctxMenuCopyVidAddress.onmouseup = () => { copyAddress(mediaInfo.data.getContentURL()) };
2170 | }
2171 |
2172 | //Generic Stuff
2173 | ctxMenuShowDefault.onclick = () => { selectedShowDefaultContext = true;
2174 | setContextMenuVisible(false); };
2175 | }
2176 |
2177 | function addCustomCtxMenu(tweetData, mediaInfo, ctxTarget)
2178 | {
2179 | if (addHasAttribute(ctxTarget, "thd_customctx")) { return; }
2180 |
2181 | ctxTarget.addEventListener('contextmenu', function (e)
2182 | {
2183 | e.stopPropagation();
2184 |
2185 | let curSel = ctxMenu.getAttribute('selection');
2186 |
2187 | if (wasShowDefaultContextClicked())
2188 | { //Skip everything here and show default context menu
2189 | selectedShowDefaultContext = false;
2190 | return;
2191 | }
2192 |
2193 | e.preventDefault();
2194 |
2195 | if(ctxMenu.style.display != "block" || (curSel == null || (curSel != null && curSel != mediaInfo.data.media_id)))
2196 | {
2197 | updateContextMenuLink(tweetData, mediaInfo);
2198 | setContextMenuVisible(true);
2199 | ctxMenu.style.left = -12.0 + mouseX(e) + "px";
2200 | ctxMenu.style.top = -10.0 + mouseY(e) + "px";
2201 | }
2202 | else { setContextMenuVisible(false); }
2203 |
2204 | }, {capture: true}, true);
2205 | }
2206 |
2207 | //<--> TWITTER UTILITY FUNCTIONS <-->//
2208 |
2209 | //Because Firefox doesn't assume the format unlike Chrome...
2210 | function getMediaFormat(url)
2211 | {
2212 | let end = url.split('/').pop();
2213 | let periodSplit = end.split('.');
2214 | if (periodSplit.length > 1)
2215 | {
2216 | return '.' + periodSplit.pop().split('?')[0];
2217 | }
2218 | if (url.includes('format='))
2219 | {
2220 | let params = url.split('?').pop().split('&');
2221 | for (let p = 0; p < params.length; p++)
2222 | {
2223 | if (params[p].includes('format'))
2224 | {
2225 | return '.' + params[p].split('=').pop().split('?')[0];
2226 | }
2227 | }
2228 | }
2229 |
2230 | return '';
2231 | }
2232 |
2233 | function isDirectImagePage(url) //Checks if webpage we're on is a direct image view
2234 | {
2235 | if (url.includes('pbs.twimg.com/media/'))
2236 | {
2237 | if(!url.includes('name=orig'))
2238 | {
2239 | window.location.href = getHighQualityImage(url);
2240 | }
2241 | return true;
2242 | }
2243 | return false;
2244 | }
2245 |
2246 | function download(url, filename)
2247 | {
2248 | return new Promise((resolve, reject) =>
2249 | {
2250 | GM_download(
2251 | {
2252 | name: filename + getMediaFormat(url),
2253 | url: url,
2254 | onload: resolve,
2255 | onerror: resolve,
2256 | ontimeout: resolve
2257 | });
2258 | });
2259 |
2260 | }
2261 |
2262 | function getUrlFromTweet(tweet)
2263 | {
2264 | let article = tweet.tagName.toUpperCase() == 'ARTICLE' ? tweet : tweet.querySelector('article');
2265 |
2266 | if (article == null) { return null; }
2267 | let timeElem = article.querySelector('time');
2268 | if(timeElem)
2269 | {
2270 | let parentLink = timeElem.parentElement;
2271 | if(parentLink.tagName.toUpperCase() == 'A')
2272 | {
2273 | return parentLink.href;
2274 | }
2275 | }
2276 |
2277 | let postLink = article.querySelector('a:not([href*="/retweets"],[href$="/likes"])[href*="/status/"][role="link"][dir="auto"]');
2278 | let imgLink = article.querySelector('a:not([href*="/retweets"],[href$="/likes"],[dir="auto"])[href*="/status/"][role="link"]');
2279 |
2280 | if (imgLink)
2281 | {
2282 | let statusLink = imgLink.href.split('/photo/')[0];
2283 | let imgUser = statusLink.split('/status/')[0];
2284 | if (postLink == null || !postLink.href.includes(imgUser)) { return statusLink; }
2285 | }
2286 |
2287 | if (postLink) { return postLink.href; }
2288 |
2289 |
2290 | return null;
2291 | }
2292 |
2293 | function getIDFromTweet(tweet)
2294 | {
2295 | let url = getUrlFromTweet(tweet);
2296 | return getIDFromURL(url);
2297 | }
2298 |
2299 | function getIDFromURL(url)
2300 | {
2301 | if(url == null) return null;
2302 |
2303 | let split = url.split('/');
2304 | for(let i = 0; i < split.length; i++)
2305 | {
2306 | if(split[i] == 'status') { return split[i + 1]; }
2307 | }
2308 |
2309 | return null;
2310 | }
2311 |
2312 | function getTweetData(tweet)
2313 | {
2314 | let id = getIDFromTweet(tweet);
2315 | if (id == null) { return null; }
2316 |
2317 | let tweetData = tweets.get(id);
2318 |
2319 | if(tweetData == null) { return null; }
2320 |
2321 | return tweetData;
2322 | }
2323 |
2324 | function filenameFromMediaData(mediaData)
2325 | {
2326 | let filename = mediaData.username + ' - ' + mediaData.id;
2327 | if (mediaData.mediaNum >= 0) { filename += '_' + mediaData.mediaNum.toString(); }
2328 | return filename;
2329 | }
2330 |
2331 | function getHighQualityImage(url)
2332 | {
2333 | if(!url.includes("name=")) { return url + (url.includes('?') ? '&' : '?') + 'name=orig'; }
2334 | return url.replace(/(?<=[\&\?]name=)([A-Za-z0-9])+(?=\&)?/, 'orig');
2335 | }
2336 |
2337 |
2338 | //--> PREFERENCE UPDATING <--//
2339 | var clearedTopics = false;
2340 |
2341 | async function clearTopicsAndInterests(force = false)
2342 | {
2343 | if(!force && clearedTopics) { return; }
2344 | clearedTopics = true;
2345 |
2346 | let autoClear = await getUserPref(toggleClearTopics.name, false);
2347 | if(autoClear == false && force == false) { return; }
2348 |
2349 | let lastClearTimeText = await getUserPref(usePref_lastTopicsClearTime, "16");
2350 | let lastClearTime = parseInt(lastClearTimeText);
2351 | let curTime = Date.now();
2352 |
2353 | if(curTime - lastClearTime < 86400000 || curTime == lastClearTime)
2354 | {
2355 | return;
2356 | }
2357 |
2358 | await setUserPref(usePref_lastTopicsClearTime, curTime.toString());
2359 |
2360 | fetch("https://twitter.com/i/api/1.1/account/personalization/twitter_interests.json", {
2361 | "headers": {
2362 | "accept": "*/*",
2363 | "accept-language": "en-US,en;q=0.9",
2364 | "authorization": authy,
2365 | "sec-fetch-dest": "empty",
2366 | "sec-fetch-mode": "cors",
2367 | "sec-fetch-site": "same-origin",
2368 | "x-csrf-token": cooky,
2369 | "x-twitter-active-user": "yes",
2370 | "x-twitter-auth-type": "OAuth2Session",
2371 | "x-twitter-client-language": "en"
2372 | },
2373 | "referrer": "https://twitter.com/settings/your_twitter_data/twitter_interests",
2374 | "referrerPolicy": "strict-origin-when-cross-origin",
2375 | "body": null,
2376 | "method": "GET",
2377 | "mode": "cors",
2378 | "credentials": "include"
2379 | }).then(function(response) {
2380 | if(response.status == 200)
2381 | {
2382 | response.json().then((json) => {
2383 |
2384 | fetch("https://twitter.com/i/api/1.1/account/personalization/p13n_preferences.json",
2385 | {
2386 | "headers": {
2387 | "accept": "*/*",
2388 | "accept-language": "en-US,en;q=0.9",
2389 | "authorization": authy,
2390 | "sec-fetch-dest": "empty",
2391 | "sec-fetch-mode": "cors",
2392 | "sec-fetch-site": "same-origin",
2393 | "x-csrf-token": cooky,
2394 | "x-twitter-active-user": "yes",
2395 | "x-twitter-auth-type": "OAuth2Session",
2396 | "x-twitter-client-language": "en"
2397 | },
2398 | "referrer": "https://twitter.com/settings/your_twitter_data/twitter_interests",
2399 | "referrerPolicy": "strict-origin-when-cross-origin",
2400 | "body": null,
2401 | "method": "GET",
2402 | "mode": "cors",
2403 | "credentials": "include"
2404 | }).then((response) =>
2405 | {
2406 | if(response.status == 200)
2407 | {
2408 | response.json().then((prefs) =>
2409 | {
2410 | const interests = json.interested_in;
2411 | if(interests.length == 0) { return; }
2412 | const disinterests = prefs.interest_preferences.disabled_interests;
2413 | prefs.allow_ads_personalization = false;
2414 | prefs.use_cookie_personalization = false;
2415 | prefs.is_eu_country = true;
2416 | prefs.age_preferences.use_age_for_personalization = false;
2417 | prefs.gender_preferences.use_gender_for_personalization = false;
2418 |
2419 | for(let i = 0; i < interests.length; i++)
2420 | {
2421 | disinterests.push(interests[i].id);
2422 | }
2423 |
2424 | prefs.interest_preferences.disabled_interests = disinterests;
2425 |
2426 | fetch("https://twitter.com/i/api/1.1/account/personalization/p13n_preferences.json", {
2427 | "headers": {
2428 | "authorization": authy,
2429 | "content-type": "application/json",
2430 | "x-csrf-token": cooky,
2431 | "x-twitter-active-user": "yes",
2432 | "x-twitter-auth-type": "OAuth2Session",
2433 | "x-twitter-client-language": "en"
2434 | },
2435 | "referrer": "https://twitter.com/settings/your_twitter_data/twitter_interests",
2436 | "referrerPolicy": "strict-origin-when-cross-origin",
2437 | "body": `{"preferences":${JSON.stringify(prefs)}}`,
2438 | "method": "POST",
2439 | "mode": "cors",
2440 | "credentials": "include"
2441 | });
2442 | });
2443 | }
2444 | });
2445 |
2446 | });
2447 | }
2448 | });
2449 |
2450 | fetch("https://twitter.com/i/api/graphql/Lt9WPkNBUP-LtG_OPW9FkA/TopicsManagementPage?variables=%7B%22withSuperFollowsUserFields%22%3Afalse%2C%22withDownvotePerspective%22%3Afalse%2C%22withReactionsMetadata%22%3Afalse%2C%22withReactionsPerspective%22%3Afalse%2C%22withSuperFollowsTweetFields%22%3Atrue%7D&features=%7B%22responsive_web_twitter_blue_verified_badge_is_enabled%22%3Afalse%2C%22verified_phone_label_enabled%22%3Afalse%2C%22responsive_web_graphql_timeline_navigation_enabled%22%3Atrue%2C%22unified_cards_ad_metadata_container_dynamic_card_content_query_enabled%22%3Atrue%2C%22tweetypie_unmention_optimization_enabled%22%3Atrue%2C%22responsive_web_uc_gql_enabled%22%3Atrue%2C%22vibe_api_enabled%22%3Atrue%2C%22responsive_web_edit_tweet_api_enabled%22%3Atrue%2C%22graphql_is_translatable_rweb_tweet_is_translatable_enabled%22%3Atrue%2C%22standardized_nudges_misinfo%22%3Atrue%2C%22tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled%22%3Afalse%2C%22interactive_text_enabled%22%3Atrue%2C%22responsive_web_text_conversations_enabled%22%3Afalse%2C%22responsive_web_enhance_cards_enabled%22%3Atrue%7D", {
2451 | "headers": {
2452 | "accept": "*/*",
2453 | "accept-language": "en-US,en;q=0.9",
2454 | "authorization": authy,
2455 | "content-type": "application/json",
2456 | "sec-fetch-dest": "empty",
2457 | "sec-fetch-mode": "cors",
2458 | "sec-fetch-site": "same-origin",
2459 | "x-csrf-token": cooky,
2460 | "x-twitter-active-user": "yes",
2461 | "x-twitter-auth-type": "OAuth2Session",
2462 | "x-twitter-client-language": "en"
2463 | },
2464 | "referrer": window.location.href, //test if this is fine... had hardcoded url here before
2465 | "referrerPolicy": "strict-origin-when-cross-origin",
2466 | "body": null,
2467 | "method": "GET",
2468 | "mode": "cors",
2469 | "credentials": "include"
2470 | }).then((resp) => {
2471 | if(resp.status == 200)
2472 | {
2473 | resp.json().then((topics) => {
2474 | let items = topics.data.viewer.topics_management_page.body.initialTimeline.timeline.timeline.instructions[2].entries;
2475 |
2476 | for(let t = 0; t < items.length; t++)
2477 | {
2478 | let item = items[t];
2479 | if(item.content.clientEventInfo.component == "suggest_followed_topic" && item.content.itemContent.topic.following == true)
2480 | {
2481 | fetch("https://twitter.com/i/api/graphql/srwjU6JM_ZKTj_QMfUGNcw/TopicUnfollow", {
2482 | "headers": {
2483 | "accept": "*/*",
2484 | "accept-language": "en-US,en;q=0.9",
2485 | "authorization": authy,
2486 | "content-type": "application/json",
2487 | "sec-fetch-dest": "empty",
2488 | "sec-fetch-mode": "cors",
2489 | "sec-fetch-site": "same-origin",
2490 | "x-csrf-token": cooky,
2491 | "x-twitter-active-user": "yes",
2492 | "x-twitter-auth-type": "OAuth2Session",
2493 | "x-twitter-client-language": "en"
2494 | },
2495 | "body": `{"variables":{"topicId":"${item.content.itemContent.topic.topic_id}"},"queryId":""}`,
2496 | "method": "POST",
2497 | "mode": "cors",
2498 | "credentials": "include"
2499 | });
2500 | }
2501 | }
2502 | });
2503 | }
2504 | });
2505 | }
2506 |
2507 | async function bookmarkPost(postId, onResponse)
2508 | {
2509 | fetch("https://api.twitter.com/graphql/aoDbu3RHznuiSkQ9aNM67Q/CreateBookmark", {
2510 | "headers": {
2511 | "accept": "*/*",
2512 | "authorization": authy,
2513 | "content-type": "application/json",
2514 | "sec-fetch-dest": "empty",
2515 | "sec-fetch-mode": "cors",
2516 | "sec-fetch-site": "same-site",
2517 | "x-csrf-token": cooky,
2518 | "x-twitter-active-user": "yes",
2519 | "x-twitter-auth-type": "OAuth2Session",
2520 | "x-twitter-client-language": "en"
2521 | },
2522 | "referrer": window.location.href,
2523 | // "referrerPolicy": "strict-origin-when-cross-origin",
2524 | "body": `{"variables":{"tweet_id":"${postId}"},"queryId":"aoDbu3RHznuiSkQ9aNM67Q"}`,
2525 | "method": "POST",
2526 | "mode": "cors",
2527 | "credentials": "include"
2528 | }).then((resp) => { onResponse(resp); }).catch((err) => {});
2529 | }
2530 |
2531 | async function unbookmarkPost(postId, onResponse)
2532 | {
2533 | fetch("https://twitter.com/i/api/graphql/Wlmlj2-xzyS1GN3a6cj-mQ/DeleteBookmark", {
2534 | "headers": {
2535 | "accept": "*/*",
2536 | "authorization": authy,
2537 | "content-type": "application/json",
2538 | "sec-fetch-dest": "empty",
2539 | "sec-fetch-mode": "cors",
2540 | "sec-fetch-site": "same-site",
2541 | "x-csrf-token": cooky,
2542 | "x-twitter-active-user": "yes",
2543 | "x-twitter-auth-type": "OAuth2Session",
2544 | "x-twitter-client-language": "en"
2545 | },
2546 | "referrer": window.location.href,
2547 | // "referrerPolicy": "strict-origin-when-cross-origin",
2548 | "body": `{"variables":{"tweet_id":"${postId}"},"queryId":"Wlmlj2-xzyS1GN3a6cj-mQ"}`,
2549 | "method": "POST",
2550 | "mode": "cors",
2551 | "credentials": "include"
2552 | }).then((resp) => { onResponse(resp); }).catch((err) => {});
2553 | }
2554 |
2555 | //<--> GENERIC UTILITY FUNCTIONS <-->//
2556 | function watchForChange(root, obsArguments, onChange)
2557 | {
2558 | const rootObserver = new MutationObserver(function (mutations)
2559 | {
2560 | rootObserver?.disconnect();
2561 | mutations.forEach((mutation) => onChange(root, mutation));
2562 | rootObserver?.observe(root, obsArguments);
2563 | });
2564 | rootObserver.observe(root, obsArguments);
2565 | return rootObserver;
2566 | }
2567 |
2568 | function watchForChangeFull(root, obsArguments, onChange)
2569 | {
2570 | const rootObserver = new MutationObserver(function (mutations)
2571 | {
2572 | rootObserver.disconnect();
2573 | onChange(root, mutations);
2574 | rootObserver.observe(root, obsArguments);
2575 | });
2576 | rootObserver.observe(root, obsArguments);
2577 | return rootObserver;
2578 | }
2579 |
2580 | async function watchForAddedNodes(root, stopAfterFirstMutation, obsArguments, executeAfter)
2581 | {
2582 | const rootObserver = new MutationObserver(
2583 | function (mutations)
2584 | {
2585 | rootObserver.disconnect();
2586 | // LogMessage("timeline mutated");
2587 | mutations.forEach(function (mutation)
2588 | {
2589 | if (mutation.addedNodes == null || mutation.addedNodes.length == 0) { return; }
2590 | executeAfter(mutation.addedNodes);
2591 | });
2592 | if (!stopAfterFirstMutation) { rootObserver.observe(root, obsArguments); }
2593 | });
2594 |
2595 | rootObserver.observe(root, obsArguments);
2596 | }
2597 |
2598 | function findElem(rootElem, query, observer, resolve)
2599 | {
2600 | const elem = rootElem.querySelector(query);
2601 | if (elem != null && elem != undefined)
2602 | {
2603 | observer?.disconnect();
2604 | resolve(elem);
2605 | }
2606 | return elem;
2607 | }
2608 |
2609 | async function awaitElem(root, query, obsArguments)
2610 | {
2611 | return new Promise((resolve, reject) =>
2612 | {
2613 | if (findElem(root, query, null, resolve)) { return; }
2614 | const rootObserver = new MutationObserver((mutes, obs) => {
2615 | findElem(root, query, obs, resolve);
2616 | });
2617 | rootObserver.observe(root, obsArguments);
2618 | });
2619 | }
2620 |
2621 | function doOnAttributeChange(elem, onChange, repeatOnce = false)
2622 | {
2623 | let rootObserver = new MutationObserver((mutes, obvs) => async function()
2624 | {
2625 | obvs.disconnect();
2626 | await onChange(elem);
2627 | if (repeatOnce == true) { return; }
2628 | obvs.observe(elem, { childList: false, subtree: false, attributes: true })
2629 | });
2630 | rootObserver.observe(elem, { childList: false, subtree: false, attributes: true });
2631 | }
2632 |
2633 | function addHasAttribute(elem, attr)
2634 | {
2635 | if (elem.hasAttribute(attr)) { return true; }
2636 | elem.setAttribute(attr, "");
2637 | return false;
2638 | }
2639 |
2640 | function getCookie(name)
2641 | {
2642 | let match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
2643 | if (match) { return match[2].toString(); }
2644 | return null;
2645 | }
2646 |
2647 | function getCSSRuleContainingStyle(styleName, selectors, styleCnt = 0, matchingValue = "")
2648 | {
2649 | let sheets = document.styleSheets;
2650 | for (let i = 0, l = sheets.length; i < l; i++)
2651 | {
2652 | let curSheet = sheets[i];
2653 |
2654 | if (!curSheet.cssRules) { continue; }
2655 |
2656 | for (let j = 0, k = curSheet.cssRules.length; j < k; j++)
2657 | {
2658 | let rule = curSheet.cssRules[j];
2659 | if (styleCnt != 0 && styleCnt != rule.style.length) { return null; }
2660 | if (rule.selectorText && rule.style.length > 0 /* && rule.selectorText.split(',').indexOf(selector) !== -1*/ )
2661 | {
2662 | for (let s = 0; s < selectors.length; s++)
2663 | {
2664 | if (rule.selectorText.includes(selectors[s]) && rule.style[0] == styleName)
2665 | {
2666 | if (matchingValue === "" || matchingValue == rule.style[styleName])
2667 | {
2668 | return rule;
2669 | }
2670 | }
2671 | }
2672 | }
2673 | }
2674 | }
2675 | return null;
2676 | }
2677 |
2678 | async function getUserPref(key, defaultVal)
2679 | {
2680 | if (isGM) { return await GM.getValue(key, defaultVal); }
2681 | return await GM_getValue(key, defaultVal);
2682 | }
2683 | async function setUserPref(key, value)
2684 | {
2685 | if (isGM) { return await GM.setValue(key, value); }
2686 | return await GM_setValue(key, value);
2687 | }
2688 |
2689 | function LogMessage(text) { /*console.log(text);*/ }
2690 |
2691 | function addGlobalStyle(css, id)
2692 | {
2693 | if(id && document.querySelector('#' + id)) { return; }
2694 | let head, style;
2695 | head = document.getElementsByTagName('head')[0];
2696 | if (!head) { return; }
2697 | style = document.createElement('style');
2698 | style.type = 'text/css';
2699 | if(id) { style.id = id; }
2700 | if (style.styleSheet) {
2701 | style.styleSheet.cssText = css;
2702 | } else
2703 | {
2704 | style.appendChild(document.createTextNode(css));
2705 | }
2706 | head.appendChild(style);
2707 | return style;
2708 | }
2709 |
2710 | function removeGlobalStyle(id)
2711 | {
2712 | let head, style;
2713 | head = document.getElementsByTagName('head')[0];
2714 | if (!head) { return; }
2715 | /*
2716 | if(styleElem == null){ return; }
2717 | let head, style;
2718 | head = document.getElementsByTagName('head')[0];
2719 | if (!head) { console.warn("Couldn't find HEAD element, style not removed, and likely doesn't exist anyways."); return; }
2720 | head.removeChild(styleElem);*/
2721 | let styleElem = document.querySelector('#' + id);
2722 | if(styleElem)
2723 | {
2724 | head.removeChild(styleElem);
2725 | }
2726 | }
2727 |
2728 |
2729 | //<--> BEGIN PROCESSING <-->//
2730 |
2731 | /*async function LoadPrefs()
2732 | {
2733 | getUserPref(usePref_blurNSFW, false).then((res) => { toggleNSFW.enabled = res; });
2734 | }*/
2735 | // 2.0
2736 |
2737 | function ObserveObj(observerConstraints, observeBehaviour, asyncGetElemBehaviour)
2738 | {
2739 | this.elem = null;
2740 | this.observer = null;
2741 |
2742 | this.updateObserver = async function()
2743 | {
2744 | if(this.elem == null)
2745 | {
2746 | this.elem = await asyncGetElemBehaviour();
2747 |
2748 | observeBehaviour(this.elem, null);
2749 |
2750 | if(this.observer == null)
2751 | {
2752 | this.observer = watchForChangeFull(this.elem, observerConstraints, (elem, mutes) => {
2753 | observeBehaviour(elem, mutes)
2754 | });
2755 | }
2756 | else { this.observer?.observe(this.elem, observerConstraints); }
2757 | }
2758 | };
2759 |
2760 | this.updateObserver();
2761 | }
2762 |
2763 | function HeaderData(header)
2764 | {
2765 | this.xLabel = '/ X';
2766 | this.title = new ObserveObj(
2767 | argsChildOnly,
2768 | (title, mutes) => { if(title && title.innerText.endsWith(this.xLabel)) { title.innerText = title.innerText.replace(this.xLabel, ''); }},
2769 | function() { return awaitElem(document.head, 'title', argsChildOnly); });
2770 |
2771 | this.meta = new ObserveObj(argsAll, (meta, mutes) => {
2772 | if(meta && meta.content.endsWith(this.xLabel)) { meta.content = meta.content.replace(this.xLabel, ''); }
2773 | }, function() { return awaitElem(document.head, 'meta[content$="/ X"]', argsChildOnly); });
2774 |
2775 | this.shortcutIco = new ObserveObj(argsAttrOnly, (shortcutIco, mutes) => {
2776 | if(shortcutIco) { shortcutIco.href = shortcutIco.href.replace('twitter.3', 'twitter.2'); }
2777 | }, function() { return awaitElem(document.head, 'link[rel="shortcut icon"]', argsChildOnly); });
2778 |
2779 | this.checkObservers = function()
2780 | {
2781 | this.title.updateObserver();
2782 | this.meta.updateObserver();
2783 | this.shortcutIco.updateObserver();
2784 | };
2785 | }
2786 |
2787 | var headerData = new HeaderData(document.head);
2788 | watchForChangeFull(document.head, argsChildOnly, () => headerData.checkObservers());
2789 |
2790 | async function swapTwitterSplashLogo(reactRoot)
2791 | {
2792 | let placeholder = reactRoot.querySelector('div#placeholder svg');
2793 | if(placeholder != null)
2794 | {
2795 | placeholder.innerHTML = twitSVG;
2796 | }
2797 |
2798 | let logo = await awaitElem(reactRoot, 'header h1 > a svg', argsChildAndSub);
2799 | logo.innerHTML = twitSVG;
2800 | }
2801 |
2802 | function replaceWithVX(txt)
2803 | {
2804 | if(!toggleMakeLinksVX.enabled) { return txt; }
2805 | if(txt.includes('/status/') && !txt.includes('//fixupx.com/'))
2806 | {
2807 | if(!txt.includes('.com'))
2808 | {
2809 | return 'https://fixupx.com' + txt;
2810 | }
2811 | return txt.split('?')[0].replace('//twitter.com/','//fixupx.com/').replace('//x.com/', '//fixupx.com/');
2812 | }
2813 | return txt;
2814 | }
2815 |
2816 | document.addEventListener('copy', function(e)
2817 | {
2818 | if(toggleMakeLinksVX.enabled)
2819 | {
2820 | let txt = e?.srcElement?.innerText;
2821 | if(txt && (txt.startsWith('http') || txt.startsWith("x.com") || txt.startsWith("twitter.com")))
2822 | {
2823 | txt = replaceWithVX(txt);
2824 |
2825 | e.clipboardData.setData('text/plain', txt);
2826 | e.preventDefault();
2827 | }
2828 | }
2829 | });
2830 |
2831 |
2832 | (async function ()
2833 | {
2834 | 'use strict';
2835 |
2836 | if (isDirectImagePage(window.location.href)) { return; }
2837 | // if(window.location.href.includes('.x.com/') || window.location.href.includes('/x.com/')) { window.location.href = changeToTwitter(window.location.href); return; }
2838 |
2839 | let prefsLoading = loadToggleValues();
2840 |
2841 | await awaitElem(document, 'BODY', argsChildAndSub);
2842 | NodeList.prototype.forEach = Array.prototype.forEach;
2843 | preCursor = document.body.style.cursor;
2844 | initializeCtxMenu();
2845 |
2846 | let isIframe = document.body.querySelector('div#app');
2847 |
2848 | if (isIframe != null)
2849 | {
2850 | awaitElem(isIframe, 'article[role="article"]', argsChildAndSub).then(listenForMediaType);
2851 | return;
2852 | }
2853 |
2854 | const reactRoot = await awaitElem(document.body, 'div#react-root', argsChildAndSub);
2855 | swapTwitterSplashLogo(reactRoot);
2856 | const main = await awaitElem(reactRoot, 'main[role="main"] div', argsChildAndSub);
2857 | await prefsLoading;
2858 |
2859 | let layers = reactRoot.querySelector('div#layers');
2860 |
2861 | awaitElem(reactRoot, 'div#layers', argsChildAndSub).then((layers) =>
2862 | {
2863 | if (!addHasAttribute(layers, "watchingLayers")) { watchForChange(layers, { childList: true, subtree: true }, onLayersChange); }
2864 | });
2865 |
2866 | addHasAttribute(main, modifiedAttr);
2867 |
2868 | onMainChange(main);
2869 | watchForChange(main, argsChildOnly, onMainChange);
2870 | })();
--------------------------------------------------------------------------------