From bd4f3797e5c94605c4e7bdf99620cb7dd94a7bc0 Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 12 May 2025 22:26:50 +0800 Subject: [PATCH 01/29] Wrap console.log within developerMode & sendMessage for MediaTweets: - Console log can slow down updating of timelines when scrolling home page - MediaTweets: For added parity (i.e. For my own userscript that listens to webrequests and captures data there) --- scripts/apis.js | 2 ++ scripts/helpers.js | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/apis.js b/scripts/apis.js index dba5fd3b..1dc6f71f 100644 --- a/scripts/apis.js +++ b/scripts/apis.js @@ -1897,6 +1897,8 @@ const API = { }).filter(i => i); } + sendRequestToEventListeners("MediaTweets", data) + let newCursor = entries.find(e => e.entryId.startsWith('cursor-bottom-')); if(newCursor) { newCursor = newCursor.content.value; diff --git a/scripts/helpers.js b/scripts/helpers.js index 7a73309b..461652d0 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1452,7 +1452,9 @@ function renderMedia(t) { src="${m.media_url_https + (vars.showOriginalImages && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' && !vars.disableDataSaver ? '?name=small' : '')}" class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}" >`; - console.log(html` + if (typeof vars === "object" && vars.developerMode) + { + console.log(html` `); + } } else if(m.type === 'animated_gif') { let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height); let rid = m.id_str + m.media_key; From b540ec932457422f050f9a84b5b372c3b860836c Mon Sep 17 00:00:00 2001 From: Shinon Date: Tue, 13 May 2025 04:20:26 +0800 Subject: [PATCH 02/29] Extract appendTweet massive blob into seperate script & update purify js --- libraries/purify.min.js | 4 +- manifest.json | 2 +- scripts/helpers.js | 233 +------- scripts/tweetConstructor.js | 1047 +++++++++++++++++++++++++++++++++++ 4 files changed, 1065 insertions(+), 221 deletions(-) create mode 100644 scripts/tweetConstructor.js diff --git a/libraries/purify.min.js b/libraries/purify.min.js index 7b8a6bc1..fd9338b4 100644 --- a/libraries/purify.min.js +++ b/libraries/purify.min.js @@ -1,3 +1,3 @@ -/*! @license DOMPurify 3.1.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.3/LICENSE */ -!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=S(Array.prototype.forEach),m=S(Array.prototype.pop),p=S(Array.prototype.push),f=S(String.prototype.toLowerCase),d=S(String.prototype.toString),h=S(String.prototype.match),g=S(String.prototype.replace),_=S(String.prototype.indexOf),T=S(String.prototype.trim),y=S(Object.prototype.hasOwnProperty),E=S(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function w(e){for(let t=0;t/gm),W=a(/\${[\w\W]*}/gm),G=a(/^data-[\-\w.\u00B7-\uFFFF]/),Y=a(/^aria-[\-\w]+$/),j=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),X=a(/^(?:\w+script|data):/i),q=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),$=a(/^html$/i),K=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var V=Object.freeze({__proto__:null,MUSTACHE_EXPR:z,ERB_EXPR:B,TMPLIT_EXPR:W,DATA_ATTR:G,ARIA_ATTR:Y,IS_ALLOWED_URI:j,IS_SCRIPT_OR_DATA:X,ATTR_WHITESPACE:q,DOCTYPE_NAME:$,CUSTOM_ELEMENT:K});const Z=1,J=3,Q=7,ee=8,te=9,ne=function(){return"undefined"==typeof window?null:window},oe=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ne();const o=e=>t(e);if(o.version="3.1.3",o.removed=[],!n||!n.document||n.document.nodeType!==te)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:S,Element:w,NodeFilter:z,NamedNodeMap:B=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:W,DOMParser:G,trustedTypes:Y}=n,X=w.prototype,q=v(X,"cloneNode"),K=v(X,"nextSibling"),re=v(X,"childNodes"),ie=v(X,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ae,le="";const{implementation:ce,createNodeIterator:se,createDocumentFragment:ue,getElementsByTagName:me}=r,{importNode:pe}=a;let fe={};o.isSupported="function"==typeof e&&"function"==typeof ie&&ce&&void 0!==ce.createHTMLDocument;const{MUSTACHE_EXPR:de,ERB_EXPR:he,TMPLIT_EXPR:ge,DATA_ATTR:_e,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:Ae}=V;let{IS_ALLOWED_URI:Ne}=V,be=null;const Se=R({},[...L,...D,...O,...k,...I]);let Re=null;const we=R({},[...U,...P,...F,...H]);let Ce=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,De=!0,Oe=!0,xe=!1,ke=!0,Me=!1,Ie=!0,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!1,We=!0,Ge=!1;const Ye="user-content-";let je=!0,Xe=!1,qe={},$e=null;const Ke=R({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=R({},["audio","video","img","source","image","track"]);let Je=null;const Qe=R({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=R({},[et,tt,nt],d);let lt=null;const ct=["application/xhtml+xml","text/html"],st="text/html";let ut=null,mt=null;const pt=255,ft=r.createElement("form"),dt=function(e){return e instanceof RegExp||e instanceof Function},ht=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mt||mt!==e){if(e&&"object"==typeof e||(e={}),e=C(e),lt=-1===ct.indexOf(e.PARSER_MEDIA_TYPE)?st:e.PARSER_MEDIA_TYPE,ut="application/xhtml+xml"===lt?d:f,be=y(e,"ALLOWED_TAGS")?R({},e.ALLOWED_TAGS,ut):Se,Re=y(e,"ALLOWED_ATTR")?R({},e.ALLOWED_ATTR,ut):we,it=y(e,"ALLOWED_NAMESPACES")?R({},e.ALLOWED_NAMESPACES,d):at,Je=y(e,"ADD_URI_SAFE_ATTR")?R(C(Qe),e.ADD_URI_SAFE_ATTR,ut):Qe,Ve=y(e,"ADD_DATA_URI_TAGS")?R(C(Ze),e.ADD_DATA_URI_TAGS,ut):Ze,$e=y(e,"FORBID_CONTENTS")?R({},e.FORBID_CONTENTS,ut):Ke,ve=y(e,"FORBID_TAGS")?R({},e.FORBID_TAGS,ut):{},Le=y(e,"FORBID_ATTR")?R({},e.FORBID_ATTR,ut):{},qe=!!y(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,Oe=!1!==e.ALLOW_DATA_ATTR,xe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ke=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Me=e.SAFE_FOR_TEMPLATES||!1,Ie=!1!==e.SAFE_FOR_XML,Ue=e.WHOLE_DOCUMENT||!1,He=e.RETURN_DOM||!1,ze=e.RETURN_DOM_FRAGMENT||!1,Be=e.RETURN_TRUSTED_TYPE||!1,Fe=e.FORCE_BODY||!1,We=!1!==e.SANITIZE_DOM,Ge=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,Ne=e.ALLOWED_URI_REGEXP||j,ot=e.NAMESPACE||nt,Ce=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Me&&(Oe=!1),ze&&(He=!0),qe&&(be=R({},I),Re=[],!0===qe.html&&(R(be,L),R(Re,U)),!0===qe.svg&&(R(be,D),R(Re,P),R(Re,H)),!0===qe.svgFilters&&(R(be,O),R(Re,P),R(Re,H)),!0===qe.mathMl&&(R(be,k),R(Re,F),R(Re,H))),e.ADD_TAGS&&(be===Se&&(be=C(be)),R(be,e.ADD_TAGS,ut)),e.ADD_ATTR&&(Re===we&&(Re=C(Re)),R(Re,e.ADD_ATTR,ut)),e.ADD_URI_SAFE_ATTR&&R(Je,e.ADD_URI_SAFE_ATTR,ut),e.FORBID_CONTENTS&&($e===Ke&&($e=C($e)),R($e,e.FORBID_CONTENTS,ut)),je&&(be["#text"]=!0),Ue&&R(be,["html","head","body"]),be.table&&(R(be,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ae=e.TRUSTED_TYPES_POLICY,le=ae.createHTML("")}else void 0===ae&&(ae=oe(Y,c)),null!==ae&&"string"==typeof le&&(le=ae.createHTML(""));i&&i(e),mt=e}},gt=R({},["mi","mo","mn","ms","mtext"]),_t=R({},["foreignobject","annotation-xml"]),Tt=R({},["title","style","font","a","script"]),yt=R({},[...D,...O,...x]),Et=R({},[...k,...M]),At=function(e){let t=ie(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||gt[o]):Boolean(yt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&_t[o]:Boolean(Et[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!_t[o])&&(!(t.namespaceURI===et&&!gt[o])&&(!Et[n]&&(Tt[n]||!yt[n]))):!("application/xhtml+xml"!==lt||!it[e.namespaceURI]))},Nt=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},bt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Re[e])if(He||ze)try{Nt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},St=function(e){let t=null,n=null;if(Fe)e=""+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===lt&&ot===nt&&(e=''+e+"");const o=ae?ae.createHTML(e):e;if(ot===nt)try{t=(new G).parseFromString(o,lt)}catch(e){}if(!t||!t.documentElement){t=ce.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?le:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?me.call(t,Ue?"html":"body")[0]:Ue?t.documentElement:i},Rt=function(e){return se.call(e.ownerDocument||e,e,z.SHOW_ELEMENT|z.SHOW_COMMENT|z.SHOW_TEXT|z.SHOW_PROCESSING_INSTRUCTION|z.SHOW_CDATA_SECTION,null)},wt=function(e){return e instanceof W&&(void 0!==e.__depth&&"number"!=typeof e.__depth||void 0!==e.__removalCount&&"number"!=typeof e.__removalCount||"string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof B)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Ct=function(e){return"function"==typeof S&&e instanceof S},vt=function(e,t,n){fe[e]&&u(fe[e],(e=>{e.call(o,t,n,mt)}))},Lt=function(e){let t=null;if(vt("beforeSanitizeElements",e,null),wt(e))return Nt(e),!0;const n=ut(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:n,allowedTags:be}),e.hasChildNodes()&&!Ct(e.firstElementChild)&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return Nt(e),!0;if(e.nodeType===Q)return Nt(e),!0;if(Ie&&e.nodeType===ee&&E(/<[/\w]/g,e.data))return Nt(e),!0;if(!be[n]||ve[n]){if(!ve[n]&&Ot(n)){if(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n))return!1;if(Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ie(e)||e.parentNode,n=re(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=q(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,K(e))}}}return Nt(e),!0}return e instanceof w&&!At(e)?(Nt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!E(/<\/no(script|embed|frames)/i,e.innerHTML)?(Me&&e.nodeType===J&&(t=e.textContent,u([de,he,ge],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),vt("afterSanitizeElements",e,null),!1):(Nt(e),!0)},Dt=function(e,t,n){if(We&&("id"===t||"name"===t)&&(n in r||n in ft||"__depth"===n||"__removalCount"===n))return!1;if(Oe&&!Le[t]&&E(_e,t));else if(De&&E(Te,t));else if(!Re[t]||Le[t]){if(!(Ot(e)&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&E(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(Je[t]);else if(E(Ne,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==_(n,"data:")||!Ve[e]){if(xe&&!E(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},Ot=function(e){return"annotation-xml"!==e&&h(e,Ae)},xt=function(e){vt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Re};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ut(a);let p="value"===a?c:T(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,vt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(bt(a,e),!n.keepAttr)continue;if(!ke&&E(/\/>/i,p)){bt(a,e);continue}if(Ie&&E(/((--!?|])>)|<\/(style|title)/i,p)){bt(a,e);continue}Me&&u([de,he,ge],(e=>{p=g(p,e," ")}));const f=ut(e.nodeName);if(Dt(f,s,p)){if(!Ge||"id"!==s&&"name"!==s||(bt(a,e),p=Ye+p),ae&&"object"==typeof Y&&"function"==typeof Y.getAttributeType)if(l);else switch(Y.getAttributeType(f,s)){case"TrustedHTML":p=ae.createHTML(p);break;case"TrustedScriptURL":p=ae.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),wt(e)?Nt(e):m(o.removed)}catch(e){}}}vt("afterSanitizeAttributes",e,null)},kt=function e(t){let n=null;const o=Rt(t);for(vt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();){if(vt("uponSanitizeShadowNode",n,null),Lt(n))continue;const t=ie(n);n.nodeType===Z&&(t&&t.__depth?n.__depth=(n.__removalCount||0)+t.__depth+1:n.__depth=1),(n.__depth>=pt||n.__depth<0||b(n.__depth))&&Nt(n),n.content instanceof s&&(n.content.__depth=n.__depth,e(n.content)),xt(n)}vt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Ct(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||ht(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=ut(e.nodeName);if(!be[t]||ve[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof S)n=St("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===Z&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!He&&!Me&&!Ue&&-1===e.indexOf("<"))return ae&&Be?ae.createHTML(e):e;if(n=St(e),!n)return He?null:Be?le:""}n&&Fe&&Nt(n.firstChild);const c=Rt(Xe?e:n);for(;i=c.nextNode();){if(Lt(i))continue;const e=ie(i);i.nodeType===Z&&(e&&e.__depth?i.__depth=(i.__removalCount||0)+e.__depth+1:i.__depth=1),(i.__depth>=pt||i.__depth<0||b(i.__depth))&&Nt(i),i.content instanceof s&&(i.content.__depth=i.__depth,kt(i.content)),xt(i)}if(Xe)return e;if(He){if(ze)for(l=ue.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Re.shadowroot||Re.shadowrootmode)&&(l=pe.call(a,l,!0)),l}let m=Ue?n.outerHTML:n.innerHTML;return Ue&&be["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&E($,n.ownerDocument.doctype.name)&&(m="\n"+m),Me&&u([de,he,ge],(e=>{m=g(m,e," ")})),ae&&Be?ae.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ht(e),Pe=!0},o.clearConfig=function(){mt=null,Pe=!1},o.isValidAttribute=function(e,t,n){mt||ht({});const o=ut(e),r=ut(t);return Dt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(fe[e]=fe[e]||[],p(fe[e],t))},o.removeHook=function(e){if(fe[e])return m(fe[e])},o.removeHooks=function(e){fe[e]&&(fe[e]=[])},o.removeAllHooks=function(){fe={}},o}();return re})); +/*! @license DOMPurify 3.2.5 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.2.5/LICENSE */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=R(Array.prototype.forEach),m=R(Array.prototype.lastIndexOf),p=R(Array.prototype.pop),f=R(Array.prototype.push),d=R(Array.prototype.splice),h=R(String.prototype.toLowerCase),g=R(String.prototype.toString),T=R(String.prototype.match),y=R(String.prototype.replace),E=R(String.prototype.indexOf),A=R(String.prototype.trim),_=R(Object.prototype.hasOwnProperty),S=R(RegExp.prototype.test),b=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n1?n-1:0),r=1;r2&&void 0!==arguments[2]?arguments[2]:h;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function O(e){for(let t=0;t/gm),G=a(/\$\{[\w\W]*/gm),Y=a(/^data-[\-\w.\u00B7-\uFFFF]+$/),j=a(/^aria-[\-\w]+$/),X=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),q=a(/^(?:\w+script|data):/i),$=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),K=a(/^html$/i),V=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var Z=Object.freeze({__proto__:null,ARIA_ATTR:j,ATTR_WHITESPACE:$,CUSTOM_ELEMENT:V,DATA_ATTR:Y,DOCTYPE_NAME:K,ERB_EXPR:W,IS_ALLOWED_URI:X,IS_SCRIPT_OR_DATA:q,MUSTACHE_EXPR:B,TMPLIT_EXPR:G});const J=1,Q=3,ee=7,te=8,ne=9,oe=function(){return"undefined"==typeof window?null:window};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe();const o=e=>t(e);if(o.version="3.2.5",o.removed=[],!n||!n.document||n.document.nodeType!==ne||!n.Element)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:R,Element:O,NodeFilter:B,NamedNodeMap:W=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:G,DOMParser:Y,trustedTypes:j}=n,q=O.prototype,$=v(q,"cloneNode"),V=v(q,"remove"),re=v(q,"nextSibling"),ie=v(q,"childNodes"),ae=v(q,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let le,ce="";const{implementation:se,createNodeIterator:ue,createDocumentFragment:me,getElementsByTagName:pe}=r,{importNode:fe}=a;let de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]};o.isSupported="function"==typeof e&&"function"==typeof ae&&se&&void 0!==se.createHTMLDocument;const{MUSTACHE_EXPR:he,ERB_EXPR:ge,TMPLIT_EXPR:Te,DATA_ATTR:ye,ARIA_ATTR:Ee,IS_SCRIPT_OR_DATA:Ae,ATTR_WHITESPACE:_e,CUSTOM_ELEMENT:Se}=Z;let{IS_ALLOWED_URI:be}=Z,Ne=null;const Re=w({},[...L,...C,...x,...M,...U]);let we=null;const Oe=w({},[...z,...P,...H,...F]);let De=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,Ce=!0,xe=!0,Ie=!1,Me=!0,ke=!1,Ue=!0,ze=!1,Pe=!1,He=!1,Fe=!1,Be=!1,We=!1,Ge=!0,Ye=!1,je=!0,Xe=!1,qe={},$e=null;const Ke=w({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=w({},["audio","video","img","source","image","track"]);let Je=null;const Qe=w({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=w({},[et,tt,nt],g);let lt=w({},["mi","mo","mn","ms","mtext"]),ct=w({},["annotation-xml"]);const st=w({},["title","style","font","a","script"]);let ut=null;const mt=["application/xhtml+xml","text/html"];let pt=null,ft=null;const dt=r.createElement("form"),ht=function(e){return e instanceof RegExp||e instanceof Function},gt=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!ft||ft!==e){if(e&&"object"==typeof e||(e={}),e=D(e),ut=-1===mt.indexOf(e.PARSER_MEDIA_TYPE)?"text/html":e.PARSER_MEDIA_TYPE,pt="application/xhtml+xml"===ut?g:h,Ne=_(e,"ALLOWED_TAGS")?w({},e.ALLOWED_TAGS,pt):Re,we=_(e,"ALLOWED_ATTR")?w({},e.ALLOWED_ATTR,pt):Oe,it=_(e,"ALLOWED_NAMESPACES")?w({},e.ALLOWED_NAMESPACES,g):at,Je=_(e,"ADD_URI_SAFE_ATTR")?w(D(Qe),e.ADD_URI_SAFE_ATTR,pt):Qe,Ve=_(e,"ADD_DATA_URI_TAGS")?w(D(Ze),e.ADD_DATA_URI_TAGS,pt):Ze,$e=_(e,"FORBID_CONTENTS")?w({},e.FORBID_CONTENTS,pt):Ke,ve=_(e,"FORBID_TAGS")?w({},e.FORBID_TAGS,pt):{},Le=_(e,"FORBID_ATTR")?w({},e.FORBID_ATTR,pt):{},qe=!!_(e,"USE_PROFILES")&&e.USE_PROFILES,Ce=!1!==e.ALLOW_ARIA_ATTR,xe=!1!==e.ALLOW_DATA_ATTR,Ie=e.ALLOW_UNKNOWN_PROTOCOLS||!1,Me=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,ke=e.SAFE_FOR_TEMPLATES||!1,Ue=!1!==e.SAFE_FOR_XML,ze=e.WHOLE_DOCUMENT||!1,Fe=e.RETURN_DOM||!1,Be=e.RETURN_DOM_FRAGMENT||!1,We=e.RETURN_TRUSTED_TYPE||!1,He=e.FORCE_BODY||!1,Ge=!1!==e.SANITIZE_DOM,Ye=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,be=e.ALLOWED_URI_REGEXP||X,ot=e.NAMESPACE||nt,lt=e.MATHML_TEXT_INTEGRATION_POINTS||lt,ct=e.HTML_INTEGRATION_POINTS||ct,De=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(De.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&ht(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(De.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(De.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),ke&&(xe=!1),Be&&(Fe=!0),qe&&(Ne=w({},U),we=[],!0===qe.html&&(w(Ne,L),w(we,z)),!0===qe.svg&&(w(Ne,C),w(we,P),w(we,F)),!0===qe.svgFilters&&(w(Ne,x),w(we,P),w(we,F)),!0===qe.mathMl&&(w(Ne,M),w(we,H),w(we,F))),e.ADD_TAGS&&(Ne===Re&&(Ne=D(Ne)),w(Ne,e.ADD_TAGS,pt)),e.ADD_ATTR&&(we===Oe&&(we=D(we)),w(we,e.ADD_ATTR,pt)),e.ADD_URI_SAFE_ATTR&&w(Je,e.ADD_URI_SAFE_ATTR,pt),e.FORBID_CONTENTS&&($e===Ke&&($e=D($e)),w($e,e.FORBID_CONTENTS,pt)),je&&(Ne["#text"]=!0),ze&&w(Ne,["html","head","body"]),Ne.table&&(w(Ne,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw b('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');le=e.TRUSTED_TYPES_POLICY,ce=le.createHTML("")}else void 0===le&&(le=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}}(j,c)),null!==le&&"string"==typeof ce&&(ce=le.createHTML(""));i&&i(e),ft=e}},Tt=w({},[...C,...x,...I]),yt=w({},[...M,...k]),Et=function(e){f(o.removed,{element:e});try{ae(e).removeChild(e)}catch(t){V(e)}},At=function(e,t){try{f(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){f(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e)if(Fe||Be)try{Et(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},_t=function(e){let t=null,n=null;if(He)e=""+e;else{const t=T(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===ut&&ot===nt&&(e=''+e+"");const o=le?le.createHTML(e):e;if(ot===nt)try{t=(new Y).parseFromString(o,ut)}catch(e){}if(!t||!t.documentElement){t=se.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?ce:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?pe.call(t,ze?"html":"body")[0]:ze?t.documentElement:i},St=function(e){return ue.call(e.ownerDocument||e,e,B.SHOW_ELEMENT|B.SHOW_COMMENT|B.SHOW_TEXT|B.SHOW_PROCESSING_INSTRUCTION|B.SHOW_CDATA_SECTION,null)},bt=function(e){return e instanceof G&&("string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof W)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Nt=function(e){return"function"==typeof R&&e instanceof R};function Rt(e,t,n){u(e,(e=>{e.call(o,t,n,ft)}))}const wt=function(e){let t=null;if(Rt(de.beforeSanitizeElements,e,null),bt(e))return Et(e),!0;const n=pt(e.nodeName);if(Rt(de.uponSanitizeElement,e,{tagName:n,allowedTags:Ne}),e.hasChildNodes()&&!Nt(e.firstElementChild)&&S(/<[/\w!]/g,e.innerHTML)&&S(/<[/\w!]/g,e.textContent))return Et(e),!0;if(e.nodeType===ee)return Et(e),!0;if(Ue&&e.nodeType===te&&S(/<[/\w]/g,e.data))return Et(e),!0;if(!Ne[n]||ve[n]){if(!ve[n]&&Dt(n)){if(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n))return!1;if(De.tagNameCheck instanceof Function&&De.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ae(e)||e.parentNode,n=ie(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=$(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,re(e))}}}return Et(e),!0}return e instanceof O&&!function(e){let t=ae(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=h(e.tagName),o=h(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||lt[o]):Boolean(Tt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&ct[o]:Boolean(yt[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!ct[o])&&!(t.namespaceURI===et&&!lt[o])&&!yt[n]&&(st[n]||!Tt[n]):!("application/xhtml+xml"!==ut||!it[e.namespaceURI]))}(e)?(Et(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!S(/<\/no(script|embed|frames)/i,e.innerHTML)?(ke&&e.nodeType===Q&&(t=e.textContent,u([he,ge,Te],(e=>{t=y(t,e," ")})),e.textContent!==t&&(f(o.removed,{element:e.cloneNode()}),e.textContent=t)),Rt(de.afterSanitizeElements,e,null),!1):(Et(e),!0)},Ot=function(e,t,n){if(Ge&&("id"===t||"name"===t)&&(n in r||n in dt))return!1;if(xe&&!Le[t]&&S(ye,t));else if(Ce&&S(Ee,t));else if(!we[t]||Le[t]){if(!(Dt(e)&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,e)||De.tagNameCheck instanceof Function&&De.tagNameCheck(e))&&(De.attributeNameCheck instanceof RegExp&&S(De.attributeNameCheck,t)||De.attributeNameCheck instanceof Function&&De.attributeNameCheck(t))||"is"===t&&De.allowCustomizedBuiltInElements&&(De.tagNameCheck instanceof RegExp&&S(De.tagNameCheck,n)||De.tagNameCheck instanceof Function&&De.tagNameCheck(n))))return!1}else if(Je[t]);else if(S(be,y(n,_e,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==E(n,"data:")||!Ve[e]){if(Ie&&!S(Ae,y(n,_e,"")));else if(n)return!1}else;return!0},Dt=function(e){return"annotation-xml"!==e&&T(e,Se)},vt=function(e){Rt(de.beforeSanitizeAttributes,e,null);const{attributes:t}=e;if(!t||bt(e))return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:we,forceKeepAttr:void 0};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=pt(a),m=c;let f="value"===a?m:A(m);if(n.attrName=s,n.attrValue=f,n.keepAttr=!0,n.forceKeepAttr=void 0,Rt(de.uponSanitizeAttribute,e,n),f=n.attrValue,!Ye||"id"!==s&&"name"!==s||(At(a,e),f="user-content-"+f),Ue&&S(/((--!?|])>)|<\/(style|title)/i,f)){At(a,e);continue}if(n.forceKeepAttr)continue;if(!n.keepAttr){At(a,e);continue}if(!Me&&S(/\/>/i,f)){At(a,e);continue}ke&&u([he,ge,Te],(e=>{f=y(f,e," ")}));const d=pt(e.nodeName);if(Ot(d,s,f)){if(le&&"object"==typeof j&&"function"==typeof j.getAttributeType)if(l);else switch(j.getAttributeType(d,s)){case"TrustedHTML":f=le.createHTML(f);break;case"TrustedScriptURL":f=le.createScriptURL(f)}if(f!==m)try{l?e.setAttributeNS(l,a,f):e.setAttribute(a,f),bt(e)?Et(e):p(o.removed)}catch(t){At(a,e)}}else At(a,e)}Rt(de.afterSanitizeAttributes,e,null)},Lt=function e(t){let n=null;const o=St(t);for(Rt(de.beforeSanitizeShadowDOM,t,null);n=o.nextNode();)Rt(de.uponSanitizeShadowNode,n,null),wt(n),vt(n),n.content instanceof s&&e(n.content);Rt(de.afterSanitizeShadowDOM,t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Nt(e)){if("function"!=typeof e.toString)throw b("toString is not a function");if("string"!=typeof(e=e.toString()))throw b("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||gt(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=pt(e.nodeName);if(!Ne[t]||ve[t])throw b("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof R)n=_t("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===J&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!Fe&&!ke&&!ze&&-1===e.indexOf("<"))return le&&We?le.createHTML(e):e;if(n=_t(e),!n)return Fe?null:We?ce:""}n&&He&&Et(n.firstChild);const c=St(Xe?e:n);for(;i=c.nextNode();)wt(i),vt(i),i.content instanceof s&&Lt(i.content);if(Xe)return e;if(Fe){if(Be)for(l=me.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(we.shadowroot||we.shadowrootmode)&&(l=fe.call(a,l,!0)),l}let m=ze?n.outerHTML:n.innerHTML;return ze&&Ne["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&S(K,n.ownerDocument.doctype.name)&&(m="\n"+m),ke&&u([he,ge,Te],(e=>{m=y(m,e," ")})),le&&We?le.createHTML(m):m},o.setConfig=function(){gt(arguments.length>0&&void 0!==arguments[0]?arguments[0]:{}),Pe=!0},o.clearConfig=function(){ft=null,Pe=!1},o.isValidAttribute=function(e,t,n){ft||gt({});const o=pt(e),r=pt(t);return Ot(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&f(de[e],t)},o.removeHook=function(e,t){if(void 0!==t){const n=m(de[e],t);return-1===n?void 0:d(de[e],n,1)[0]}return p(de[e])},o.removeHooks=function(e){de[e]=[]},o.removeAllHooks=function(){de={afterSanitizeAttributes:[],afterSanitizeElements:[],afterSanitizeShadowDOM:[],beforeSanitizeAttributes:[],beforeSanitizeElements:[],beforeSanitizeShadowDOM:[],uponSanitizeAttribute:[],uponSanitizeElement:[],uponSanitizeShadowNode:[]}},o}();return re})); //# sourceMappingURL=purify.min.js.map \ No newline at end of file diff --git a/manifest.json b/manifest.json index 6be6e4e9..26428fa4 100644 --- a/manifest.json +++ b/manifest.json @@ -89,7 +89,7 @@ "https://x.com/i/tweetdeck", "https://x.com/i/communitynotes", "https://x.com/i/broadcasts/*", "https://x.com/search-advanced", "https://x.com/x/migrate", "https://x.com/i/grok" ], - "js": ["scripts/blockBeforeInject.js", "scripts/config.js", "scripts/helpers.js", "scripts/apis.js", "scripts/twchallenge.js", "scripts/injection.js"], + "js": ["scripts/blockBeforeInject.js", "scripts/config.js", "scripts/tweetConstructor.js", "scripts/helpers.js", "scripts/apis.js", "scripts/twchallenge.js", "scripts/injection.js"], "css": ["libraries/viewer.min.css"], "all_frames": true, "run_at": "document_start" diff --git a/scripts/helpers.js b/scripts/helpers.js index 461652d0..1545c289 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1930,226 +1930,23 @@ async function appendTweet(t, timelineContainer, options = {}) { //else this is not reply but mention }); } - // i fucking hate this thing + const [topContent, actualContent] = await constructTweet(t, { + videos:videos, + isMatchingLanguage:isMatchingLanguage, + mentionedUserText:mentionedUserText, + full_text:full_text, + isQuoteMatchingLanguage:isQuoteMatchingLanguage, + quoteMentionedUserText:quoteMentionedUserText, + followUserText:followUserText, + unfollowUserText:unfollowUserText, + blockUserText:blockUserText, + unblockUserText:unblockUserText + }, options) + tweet.innerHTML = html` - - - ${t.user.name} - -
- - ${escapeHTML(t.user.name)} - @${t.user.screen_name} - - ${timeElapsed(new Date(t.created_at).getTime())} - ${location.pathname.split("?")[0].split("#")[0] === '/i/bookmarks' ? `×` : ''} - ${options.mainTweet && t.user.id_str !== user.id_str ? `` : ''} - ${!options.mainTweet && !isMatchingLanguage ? `${`${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && innerWidth > 650 ? LOC.view_translation.message : ''}` : ''} -
+ ${topContent}
- ${mentionedUserText !== `` && - !options.threadContinuation && - !options.noTop && - !location.pathname.includes('/status/') && - !vars.useOldStyleReply ? html` -
${LOC.replying_to_user.message.replace('$SCREEN_NAME$', mentionedUserText.trim().replaceAll(`> <`, `>${LOC.replying_to_comma.message}<`).replace(`>${LOC.replying_to_comma.message}<`, `>${LOC.replying_to_and.message}<`))}
- `: ''} -
- ${vars.useOldStyleReply ? /*html*/mentionedUserText: ''}${full_text ? await renderTweetBodyHTML(t) : ''} -
- ${!isMatchingLanguage && options.mainTweet ? html` -
- ${LOC.view_translation.message} - ` : ``} - ${t.extended_entities && t.extended_entities.media ? html` -
- ${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? html` -
- - - - - - -
- ` : ''} - ${renderMedia(t)} -
- ${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? html`
GIF
` : ''} - ${videos ? html` -
- ${videos[0].ext && videos[0].ext.mediaStats && videos[0].ext.mediaStats.r && videos[0].ext.mediaStats.r.ok ? `${Number(videos[0].ext.mediaStats.r.ok.viewCount).toLocaleString().replace(/\s/g, ',')} ${LOC.views.message} • ` : ''}${LOC.reload.message} • - ${videos[0].video_info.variants.filter(v => v.bitrate).map(v => `${v.url.match(/\/(\d+)x/)[1] + 'p'} `).join(" / ")} -
- ` : ``} - - ` : ``} - ${t.card ? `
` : ''} - ${t.quoted_status ? html` - - ${escapeHTML(t.quoted_status.user.name)} -
- - ${escapeHTML(t.quoted_status.user.name)} - @${t.quoted_status.user.screen_name} - -
- ${timeElapsed(new Date(t.quoted_status.created_at).getTime())} - ${quoteMentionedUserText !== `` && !vars.useOldStyleReply ? html` - ${LOC.replying_to_user.message.replace('$SCREEN_NAME$', quoteMentionedUserText.trim().replaceAll(` `,LOC.replying_to_comma.message).replace(LOC.replying_to_comma.message,LOC.replying_to_and.message))} - ` : ''} - ${vars.useOldStyleReply? quoteMentionedUserText: ''}${t.quoted_status.full_text ? await renderTweetBodyHTML(t, true) : ''} - ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? html` -
- ${t.quoted_status.extended_entities.media.map(m => `<${m.type === 'photo' ? 'img' : 'video'} ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text, true)}" title="${escapeHTML(m.ext_alt_text, true)}"` : ''} crossorigin="anonymous" width="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}" height="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}" loading="lazy" ${m.type === 'video' ? 'disableRemotePlayback controls' : ''} ${m.type === 'animated_gif' ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' : ''}${m.type === 'animated_gif' && !vars.disableGifAutoplay ? ' autoplay' : ''} src="${m.type === 'photo' ? m.media_url_https + (vars.showOriginalImages && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' && !vars.disableDataSaver ? '?name=small' : '') : m.video_info.variants.find(v => v.content_type === 'video/mp4').url}" class="tweet-media-element tweet-media-element-quote ${m.type === 'animated_gif' ? 'tweet-media-element-quote-gif' : ''} ${mediaClasses[t.quoted_status.extended_entities.media.length]} ${!vars.displaySensitiveContent && t.quoted_status.possibly_sensitive ? 'tweet-media-element-censor' : ''}">${m.type === 'photo' ? '' : ''}`).join('\n')} -
- ` : ''} - ${!isQuoteMatchingLanguage ? html` - ${LOC.view_translation.message} - ` : ``} -
- ` : ``} - ${t.limited_actions === 'limit_trusted_friends_tweet' && (options.mainTweet || !location.pathname.includes('/status/')) ? html` -
- ${LOC.circle_limited_tweet.message} - ${LOC.learn_more.message} -
- `.replace('$SCREEN_NAME$', tweet.trusted_circle_owner ? tweet.trusted_circle_owner : tweetStorage[t.conversation_id_str] ? tweetStorage[t.conversation_id_str].user.screen_name : t.in_reply_to_screen_name ? t.in_reply_to_screen_name : t.user.screen_name) : ''} - ${t.tombstone ? `
${t.tombstone}
` : ''} - ${((t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) || t.withheld_scope) ? `
This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` : ''} - ${t.conversation_control ? `
${t.limited_actions_text ? t.limited_actions_text : LOC.limited_tweet.message}${t.conversation_control.policy && (t.user.id_str === user.id_str || (t.conversation_control.policy.toLowerCase() === 'community' && (t.user.followed_by || (full_text && full_text.includes(`@${user.screen_name}`)))) || (t.conversation_control.policy.toLowerCase() === 'by_invitation' && full_text && full_text.includes(`@${user.screen_name}`))) ? ' ' + LOC.you_can_reply.message : ''}
` : ''} - ${options.mainTweet ? html` - - ` : ''} -
${new Date(t.created_at).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }).toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}  ・ ${t.source ? t.source.split('>')[1].split('<')[0] : 'Unknown'}
-
- ${options.mainTweet ? '' : formatLargeNumber(t.reply_count).replace(/\s/g, ',')} - ${options.mainTweet ? '' : formatLargeNumber(t.retweet_count).replace(/\s/g, ',')} - - ${options.mainTweet ? '' : formatLargeNumber(t.favorite_count).replace(/\s/g, ',')} - ${(vars.showBookmarkCount || options.mainTweet) && typeof t.bookmark_count !== 'undefined' ? - html`${formatLargeNumber(t.bookmark_count).replace(/\s/g, ',')}` : - ''} - ${vars.seeTweetViews && t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? html`${formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ',')}` : ''} - - - ${options.selfThreadButton && t.self_thread && t.self_thread.id_str && !options.threadContinuation && !location.pathname.includes('/status/') ? html`${LOC.show_this_thread.message}` : ``} - ${!options.noTop && !options.selfThreadButton && t.in_reply_to_status_id_str && !(options.threadContinuation || (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str)) && !location.pathname.includes('/status/') ? `${LOC.show_this_thread.message}` : ``} -
- - -
- ${options.selfThreadContinuation && t.self_thread && t.self_thread.id_str && !location.pathname.includes('/status/') ? html`
- - ${LOC.show_this_thread.message} - - -
- ` : html` - ${location.pathname.includes('/status/') ? `

` : ''} - -
- `} -
+ ${actualContent}
`; // gifs diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js new file mode 100644 index 00000000..86fd4b31 --- /dev/null +++ b/scripts/tweetConstructor.js @@ -0,0 +1,1047 @@ +async function constructQuotedTweet( + t, + isQuoteMatchingLanguage, + quoteMentionedUserText +) { + return `${ + t.quoted_status + ? html` + + ${escapeHTML(t.quoted_status.user.name)} +
+ + ${escapeHTML(t.quoted_status.user.name)} + @${t.quoted_status.user.screen_name} + +
+ ${timeElapsed( + new Date(t.quoted_status.created_at).getTime() + )} + ${quoteMentionedUserText !== `` && !vars.useOldStyleReply + ? html` + ${LOC.replying_to_user.message.replace( + "$SCREEN_NAME$", + quoteMentionedUserText + .trim() + .replaceAll( + ` `, + LOC.replying_to_comma.message + ) + .replace( + LOC.replying_to_comma.message, + LOC.replying_to_and.message + ) + )} + ` + : ""} + ${vars.useOldStyleReply + ? quoteMentionedUserText + : ""}${t.quoted_status.full_text + ? await renderTweetBodyHTML(t, true) + : ""} + ${t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ? html` +
+ ${t.quoted_status.extended_entities.media + .map( + (m) => + `<${ + m.type === "photo" + ? "img" + : "video" + } ${ + m.ext_alt_text + ? `alt="${escapeHTML( + m.ext_alt_text, + true + )}" title="${escapeHTML( + m.ext_alt_text, + true + )}"` + : "" + } crossorigin="anonymous" width="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities + .media.length + ]( + m.original_info.width, + m.original_info.height + )[0] + }" height="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities + .media.length + ]( + m.original_info.width, + m.original_info.height + )[1] + }" loading="lazy" ${ + m.type === "video" + ? "disableRemotePlayback controls" + : "" + } ${ + m.type === "animated_gif" + ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' + : "" + }${ + m.type === "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } src="${ + m.type === "photo" + ? m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith( + ".jpg" + ) || + m.media_url_https.endsWith( + ".png" + )) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator + .connection + .type === + "cellular" && + !vars.disableDataSaver + ? "?name=small" + : "") + : m.video_info.variants.find( + (v) => + v.content_type === + "video/mp4" + ).url + }" class="tweet-media-element tweet-media-element-quote ${ + m.type === "animated_gif" + ? "tweet-media-element-quote-gif" + : "" + } ${ + mediaClasses[ + t.quoted_status + .extended_entities + .media.length + ] + } ${ + !vars.displaySensitiveContent && + t.quoted_status + .possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${ + m.type === "photo" + ? "" + : "" + }` + ) + .join("\n")} +
+ ` + : ""} + ${!isQuoteMatchingLanguage + ? html` + ${LOC.view_translation.message} + ` + : ``} +
+ ` + : `` + }`; +} + +async function constructTweet(t, c_args, options = {}) { + const tweet_top = `${
+        t.user.name
+    }`; + + const tweet_header = `
${escapeHTML(t.user.name)}@${ + t.user.screen_name + }${timeElapsed(new Date(t.created_at).getTime())}${ + location.pathname.split("?")[0].split("#")[0] === "/i/bookmarks" + ? `×` + : "" + } ${ + options.mainTweet && t.user.id_str !== user.id_str + ? `` + : "" + } ${ + !options.mainTweet && !c_args.isMatchingLanguage + ? `${ + `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && + innerWidth > 650 + ? LOC.view_translation.message + : "" + }` + : "" + }
`; + + // body + var mentioned_text = ""; + if ( + c_args.mentionedUserText !== `` && + !options.threadContinuation && + !options.noTop && + !location.pathname.includes("/status/") && + !vars.useOldStyleReply + ) { + mentioned_text = ` +
+ + ${LOC.replying_to_user.message.replace( + "$SCREEN_NAME$", + c_args.mentionedUserText + .trim() + .replaceAll( + `> <`, + `>${LOC.replying_to_comma.message}<` + ) + .replace( + `>${LOC.replying_to_comma.message}<`, + `>${LOC.replying_to_and.message}<` + ) + )} +
+ `; + } + + const body_text = `
+ ${ + vars.useOldStyleReply + ? /*html*/ c_args.mentionedUserText + : "" + }${ + c_args.full_text ? await renderTweetBodyHTML(t) : "" + } +
`; + var translate_text = ""; + if (!c_args.isMatchingLanguage && options.mainTweet) { + translate_text = ` +
+ ${LOC.view_translation.message} + `; + } + + var extended_media = ""; + if (t.extended_entities && t.extended_entities.media) { + extended_media = ` +
+ ${t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? ` +
+ + + + + + +
+ ` + : ""} + ${renderMedia(t)} +
+ ${t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ? `
GIF
` + : ""} + ${c_args.videos + ? ` +
+ ${c_args.videos[0].ext && + c_args.videos[0].ext.mediaStats && + c_args.videos[0].ext.mediaStats.r && + c_args.videos[0].ext.mediaStats.r.ok + ? `${Number( + c_args.videos[0].ext.mediaStats.r.ok + .viewCount + ) + .toLocaleString() + .replace(/\s/g, ",")} ${ + LOC.views.message + } • ` + : ""}${LOC.reload.message} + • + ${c_args.videos[0].video_info.variants + .filter((v) => v.bitrate) + .map( + (v) => + `${ + v.url.match(/\/(\d+)x/)[1] + "p" + } ` + ) + .join(" / ")} +
+ ` + : ``} + + `; + } + + const card = t.card ? `
` : ""; + const quoted_tweet = await constructQuotedTweet( + t, + c_args.isQuoteMatchingLanguage, + c_args.quoteMentionedUserText + ); + var limited = ""; + if ( + t.limited_actions === "limit_trusted_friends_tweet" && + (options.mainTweet || !location.pathname.includes("/status/")) + ) { + limited = ` +
+ ${LOC.circle_limited_tweet.message} + ${LOC.learn_more.message} +
+ `.replace( + "$SCREEN_NAME$", + tweet.trusted_circle_owner + ? tweet.trusted_circle_owner + : tweetStorage[t.conversation_id_str] + ? tweetStorage[t.conversation_id_str].user.screen_name + : t.in_reply_to_screen_name + ? t.in_reply_to_screen_name + : t.user.screen_name + ); + } + var tomb_stone = t.tombstone + ? `
${t.tombstone}
` + : ""; + var country_restrictions = + (t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY"))) || + t.withheld_scope + ? `
+ This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` + : ""; + + const conversation_control = t.conversation_control + ? `
${ + t.limited_actions_text + ? t.limited_actions_text + : LOC.limited_tweet.message + }${ + t.conversation_control.policy && + (t.user.id_str === user.id_str || + (t.conversation_control.policy.toLowerCase() === + "community" && + (t.user.followed_by || + (c_args.full_text && + c_args.full_text.includes( + `@${user.screen_name}` + )))) || + (t.conversation_control.policy.toLowerCase() === + "by_invitation" && + c_args.full_text && + c_args.full_text.includes(`@${user.screen_name}`))) + ? " " + LOC.you_can_reply.message + : "" + }
` + : ""; + + const tweet_footer = options.mainTweet + ? ` + + ` + : ""; + + const tweet_date = `
${new Date(t.created_at) + .toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" }) + .toLowerCase()} - ${new Date(t.created_at).toLocaleDateString( + undefined, + { day: "numeric", month: "short", year: "numeric" } + )}  ・ ${t.source ? t.source.split(">")[1].split("<")[0] : "Unknown"}
`; + + const tweet_interact = `
+ ${ + options.mainTweet + ? "" + : formatLargeNumber(t.reply_count).replace(/\s/g, ",") + } + ${ + options.mainTweet + ? "" + : formatLargeNumber(t.retweet_count).replace(/\s/g, ",") + } + + ${ + options.mainTweet + ? "" + : formatLargeNumber(t.favorite_count).replace(/\s/g, ",") + } + ${ + (vars.showBookmarkCount || options.mainTweet) && + typeof t.bookmark_count !== "undefined" + ? html`${formatLargeNumber( + t.bookmark_count + ).replace(/\s/g, ",")}` + : "" + } + ${ + vars.seeTweetViews && + t.ext && + t.ext.views && + t.ext.views.r && + t.ext.views.r.ok && + t.ext.views.r.ok.count + ? html`${formatLargeNumber( + t.ext.views.r.ok.count + ).replace(/\s/g, ",")}` + : "" + } + + + ${ + options.selfThreadButton && + t.self_thread && + t.self_thread.id_str && + !options.threadContinuation && + !location.pathname.includes("/status/") + ? `${LOC.show_this_thread.message}` + : `` + } + ${ + !options.noTop && + !options.selfThreadButton && + t.in_reply_to_status_id_str && + !( + options.threadContinuation || + (options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str) + ) && + !location.pathname.includes("/status/") + ? `${LOC.show_this_thread.message}` + : `` + } +
`; + + const tweet_edit = ` + `; + + const replies = `
+ ${ + options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str && + !location.pathname.includes("/status/") + ? `
+ + ${LOC.show_this_thread.message} + + +
` + : ` + ${location.pathname.includes("/status/") + ? `

` + : ""} + +
+ ` + } +
`; + + return [ + tweet_top + tweet_header, + mentioned_text + + body_text + + translate_text + + extended_media + + card + + quoted_tweet + + limited + + tomb_stone + + country_restrictions + + conversation_control + + tweet_footer + + tweet_date + + tweet_interact + + tweet_edit + + replies, + ]; +} From 47e2d7b35ddbf7d65e4e7502cd7ae28e885adcce Mon Sep 17 00:00:00 2001 From: Shinon Date: Wed, 14 May 2025 02:18:10 +0800 Subject: [PATCH 03/29] Begin Templatify-html elements - Media elements are done --- scripts/helpers.js | 163 ++++++---- scripts/tweetConstructor.js | 629 +++++++++++++++++++----------------- 2 files changed, 434 insertions(+), 358 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 1545c289..490e733c 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -405,6 +405,8 @@ function html(strings, ...values) { return str; } + + async function renderTweetBodyHTML(t, is_quoted) { let result = "", last_pos = 0, @@ -1417,6 +1419,30 @@ async function renderDiscovery(cache = true) { console.warn(e); } } + +const elNew = (tag, prop, children=[]) => { + const element = Object.assign(document.createElement(tag), prop); + if (children) children.forEach(child=>{element.appendChild(child)}); +}; + +const img_template = elNew("img", { + crossorigin:"anonymous", + loading:"lazy", + className:"tweet-media-element", +}) + +const animated_gif_template = elNew("video", { + crossorigin:"anonymous", + loading:"lazy", + className:"tweet-media-element tweet-media-gif", +}) + +const video_template = elNew("video", { + crossorigin:"anonymous", + preload:"none", + className:"tweet-media-element", +}) + function renderMedia(t) { let _html = ''; if(!t.extended_entities || !t.extended_entities.media) return ''; @@ -1442,48 +1468,54 @@ function renderMedia(t) { } if(m.type === 'photo') { let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height); - _html += html` - `; - if (typeof vars === "object" && vars.developerMode) - { - console.log(html` - `); - } + const newClone = img_template.cloneNode(true) + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : "" + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.src = m.media_url_https + (vars.showOriginalImages && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' && !vars.disableDataSaver ? '?name=small' : '') + var mediaClass = mediaClasses[t.extended_entities.media.length] + if (mediaClass) + newClone.classList.add(mediaClass) + if (toCensor) + newClone.classList.add('tweet-media-element-censor') + _html += newClone.outerHTML; + } else if(m.type === 'animated_gif') { let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height); let rid = m.id_str + m.media_key; - _html += html` - - `; + + const newClone = animated_gif_template.cloneNode(true) + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : "" + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + + newClone.defaultMuted = true; + newClone.muted = true; + newClone.disableRemotePlayback = true; + if (!vars.disableGifAutoplay) + newClone.autoplay = true + else + newClone.autoplay = false + var mediaClass = mediaClasses[t.extended_entities.media.length] + if (mediaClass) + newClone.classList.add(mediaClass) + if (toCensor) + newClone.classList.add('tweet-media-element-censor') + m.video_info.variants.forEach(variant =>{ + var source = document.createElement("source") + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }) + newClone.appendChild(document.createTextNode(LOC.unsupported_video.message)) + _html += newClone.outerHTML; } else if(m.type === 'video') { if(m.mediaStats && m.mediaStats.viewCount) { m.ext = { @@ -1491,23 +1523,40 @@ function renderMedia(t) { } } let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height); - _html += html` - - `; + const newClone = video_template.cloneNode(true) + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : "" + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + + + newClone.disableRemotePlayback = true; + if (vars.muteVideos) + { + newClone.defaultMuted = true; + newClone.muted = true; + } + else { + newClone.defaultMuted = false; + newClone.muted = false; + } + var mediaClass = mediaClasses[t.extended_entities.media.length] + if (mediaClass) + newClone.classList.add(mediaClass) + if (toCensor) + newClone.classList.add('tweet-media-element-censor') + newClone.poster = m.media_url_https; + m.video_info.variants.forEach(variant =>{ + var source = document.createElement("source") + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }) + newClone.appendChild(document.createTextNode(LOC.unsupported_video.message)) + _html += newClone.outerHTML; } if(i === 1 && t.extended_entities.media.length > 3) { _html += '
'; @@ -1943,7 +1992,7 @@ async function appendTweet(t, timelineContainer, options = {}) { unblockUserText:unblockUserText }, options) - tweet.innerHTML = html` + tweet.innerHTML = ` ${topContent}
${actualContent} diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 86fd4b31..97399bfa 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -3,213 +3,213 @@ async function constructQuotedTweet( isQuoteMatchingLanguage, quoteMentionedUserText ) { - return `${ - t.quoted_status - ? html` - - ${escapeHTML(t.quoted_status.user.name)} -
- - ${escapeHTML(t.quoted_status.user.name)} - @${t.quoted_status.user.screen_name} - -
- ${timeElapsed( - new Date(t.quoted_status.created_at).getTime() - )} - ${quoteMentionedUserText !== `` && !vars.useOldStyleReply - ? html` - ${LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - quoteMentionedUserText - .trim() - .replaceAll( - ` `, - LOC.replying_to_comma.message - ) - .replace( - LOC.replying_to_comma.message, - LOC.replying_to_and.message - ) - )} - ` - : ""} - ${vars.useOldStyleReply - ? quoteMentionedUserText - : ""}${t.quoted_status.full_text - ? await renderTweetBodyHTML(t, true) - : ""} - ${t.quoted_status.extended_entities && - t.quoted_status.extended_entities.media - ? html` -
- ${t.quoted_status.extended_entities.media - .map( - (m) => - `<${ - m.type === "photo" - ? "img" - : "video" - } ${ - m.ext_alt_text - ? `alt="${escapeHTML( - m.ext_alt_text, - true - )}" title="${escapeHTML( - m.ext_alt_text, - true - )}"` - : "" - } crossorigin="anonymous" width="${ - quoteSizeFunctions[ - t.quoted_status - .extended_entities - .media.length - ]( - m.original_info.width, - m.original_info.height - )[0] - }" height="${ - quoteSizeFunctions[ - t.quoted_status - .extended_entities - .media.length - ]( - m.original_info.width, - m.original_info.height - )[1] - }" loading="lazy" ${ - m.type === "video" - ? "disableRemotePlayback controls" - : "" - } ${ - m.type === "animated_gif" - ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' - : "" - }${ - m.type === "animated_gif" && - !vars.disableGifAutoplay - ? " autoplay" - : "" - } src="${ - m.type === "photo" - ? m.media_url_https + - (vars.showOriginalImages && - (m.media_url_https.endsWith( - ".jpg" - ) || - m.media_url_https.endsWith( - ".png" - )) - ? "?name=orig" - : window.navigator && - navigator.connection && - navigator - .connection - .type === - "cellular" && - !vars.disableDataSaver - ? "?name=small" - : "") - : m.video_info.variants.find( - (v) => - v.content_type === - "video/mp4" - ).url - }" class="tweet-media-element tweet-media-element-quote ${ - m.type === "animated_gif" - ? "tweet-media-element-quote-gif" - : "" - } ${ - mediaClasses[ - t.quoted_status - .extended_entities - .media.length - ] - } ${ - !vars.displaySensitiveContent && - t.quoted_status - .possibly_sensitive - ? "tweet-media-element-censor" - : "" - }">${ - m.type === "photo" - ? "" - : "" - }` - ) - .join("\n")} -
- ` - : ""} - ${!isQuoteMatchingLanguage - ? html` - ${LOC.view_translation.message} - ` - : ``} -
- ` - : `` - }`; + const root = elNew("template"); + + const rootAHref = elNew("a", { + className: "tweet-body-quote", + target: "_blank", + href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, + },[elNew("img", { + src: + t.quoted_status.user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(t.quoted_status.user.id_str) % 7 + }_normal.png` + ) + : t.quoted_status.user.profile_image_url_https, + alt: escapeHTML(t.quoted_status.user.name), + className: "tweet-avatar-quote", + width: "24", + height: "24", + }), +]); + + root.appendChild(rootAHref); + return ` + +
+ + ${escapeHTML(t.quoted_status.user.name)} + @${t.quoted_status.user.screen_name} + +
+ ${timeElapsed( + new Date(t.quoted_status.created_at).getTime() + )} + ${ + quoteMentionedUserText !== `` && !vars.useOldStyleReply + ? html` + ${LOC.replying_to_user.message.replace( + "$SCREEN_NAME$", + quoteMentionedUserText + .trim() + .replaceAll( + ` `, + LOC.replying_to_comma.message + ) + .replace( + LOC.replying_to_comma.message, + LOC.replying_to_and.message + ) + )} + ` + : "" + } + ${vars.useOldStyleReply ? quoteMentionedUserText : ""}${ + t.quoted_status.full_text ? await renderTweetBodyHTML(t, true) : "" + } + ${ + t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ? html` +
+ ${t.quoted_status.extended_entities.media + .map( + (m) => + `<${ + m.type === "photo" + ? "img" + : "video" + } ${ + m.ext_alt_text + ? `alt="${escapeHTML( + m.ext_alt_text, + true + )}" title="${escapeHTML( + m.ext_alt_text, + true + )}"` + : "" + } crossorigin="anonymous" width="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities.media + .length + ]( + m.original_info.width, + m.original_info.height + )[0] + }" height="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities.media + .length + ]( + m.original_info.width, + m.original_info.height + )[1] + }" loading="lazy" ${ + m.type === "video" + ? "disableRemotePlayback controls" + : "" + } ${ + m.type === "animated_gif" + ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' + : "" + }${ + m.type === "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } src="${ + m.type === "photo" + ? m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith( + ".jpg" + ) || + m.media_url_https.endsWith( + ".png" + )) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator.connection + .type === + "cellular" && + !vars.disableDataSaver + ? "?name=small" + : "") + : m.video_info.variants.find( + (v) => + v.content_type === + "video/mp4" + ).url + }" class="tweet-media-element tweet-media-element-quote ${ + m.type === "animated_gif" + ? "tweet-media-element-quote-gif" + : "" + } ${ + mediaClasses[ + t.quoted_status + .extended_entities.media + .length + ] + } ${ + !vars.displaySensitiveContent && + t.quoted_status.possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${ + m.type === "photo" + ? "" + : "" + }` + ) + .join("\n")} +
+ ` + : "" + } + ${ + !isQuoteMatchingLanguage + ? html` + ${LOC.view_translation.message} + ` + : `` + } + + `; } async function constructTweet(t, c_args, options = {}) { @@ -340,23 +340,24 @@ async function constructTweet(t, c_args, options = {}) { c_args.full_text ? await renderTweetBodyHTML(t) : "" } `; - var translate_text = ""; - if (!c_args.isMatchingLanguage && options.mainTweet) { - translate_text = ` + const translate_text = + !c_args.isMatchingLanguage && options.mainTweet + ? `
${LOC.view_translation.message} - `; - } + ` + : ""; - var extended_media = ""; - if (t.extended_entities && t.extended_entities.media) { - extended_media = ` + var extended_media = + t.extended_entities && t.extended_entities.media + ? `
- ${t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "video" - ? ` + ${ + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? `
` - : ""} + : "" + } ${renderMedia(t)}
- ${t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ? `
GIF
` - : ""} - ${c_args.videos - ? ` + ${ + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ? `
GIF
` + : "" + } + ${ + c_args.videos + ? `
- ${c_args.videos[0].ext && - c_args.videos[0].ext.mediaStats && - c_args.videos[0].ext.mediaStats.r && - c_args.videos[0].ext.mediaStats.r.ok - ? `${Number( - c_args.videos[0].ext.mediaStats.r.ok - .viewCount - ) - .toLocaleString() - .replace(/\s/g, ",")} ${ - LOC.views.message - } • ` - : ""}${Number( + c_args.videos[0].ext.mediaStats.r.ok + .viewCount + ) + .toLocaleString() + .replace(/\s/g, ",")} ${ + LOC.views.message + } • ` + : "" + }${LOC.reload.message} • @@ -415,17 +422,20 @@ async function constructTweet(t, c_args, options = {}) { .join(" / ")}
` - : ``} + : `` + } - `; - } + ` + : ""; const card = t.card ? `
` : ""; - const quoted_tweet = await constructQuotedTweet( - t, - c_args.isQuoteMatchingLanguage, - c_args.quoteMentionedUserText - ); + const quoted_tweet = t.quoted_status + ? await constructQuotedTweet( + t, + c_args.isQuoteMatchingLanguage, + c_args.quoteMentionedUserText + ) + : ""; var limited = ""; if ( t.limited_actions === "limit_trusted_friends_tweet" && @@ -451,10 +461,10 @@ async function constructTweet(t, c_args, options = {}) { : t.user.screen_name ); } - var tomb_stone = t.tombstone + const tomb_stone = t.tombstone ? `
${t.tombstone}
` : ""; - var country_restrictions = + const country_restrictions = (t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) || @@ -507,8 +517,9 @@ async function constructTweet(t, c_args, options = {}) { > - ${vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 - ? html` - ${LOC.quotes.message} - ${formatLargeNumber(t.quote_count).replace( - /\s/g, - "," - )} 0 + ? html` - ` - : ""} + ${LOC.quotes.message} + ${formatLargeNumber( + t.quote_count + ).replace(/\s/g, ",")} + ` + : "" + } ${vars.heartsNotStars - ? LOC.likes.message - : LOC.favorites.message}${ + vars.heartsNotStars + ? LOC.likes.message + : LOC.favorites.message + } ${LOC.delete_tweet.message} - ${typeof pageUser !== "undefined" && - pageUser.id_str === user.id_str - ? `${pinnedTweet && - pinnedTweet.id_str === t.id_str - ? LOC.unpin_tweet.message - : LOC.pin_tweet - .message}${ + pinnedTweet && + pinnedTweet.id_str === + t.id_str + ? LOC.unpin_tweet + .message + : LOC.pin_tweet.message + }` - : ""} + : "" + } ` : "" } @@ -739,9 +758,11 @@ async function constructTweet(t, c_args, options = {}) { ? ` ${t.moderated - ? LOC.unhide_tweet.message - : LOC.hide_tweet.message}${ + t.moderated + ? LOC.unhide_tweet.message + : LOC.hide_tweet.message + } ` : "" @@ -769,9 +790,11 @@ async function constructTweet(t, c_args, options = {}) { ${t.user.following - ? c_args.unfollowUserText - : c_args.followUserText}${ + t.user.following + ? c_args.unfollowUserText + : c_args.followUserText + } ` : "" @@ -905,8 +928,7 @@ async function constructTweet(t, c_args, options = {}) { ? `${LOC.show_this_thread.message}` : `` @@ -995,8 +1017,7 @@ async function constructTweet(t, c_args, options = {}) { ${LOC.show_this_thread.message} @@ -1009,18 +1030,24 @@ async function constructTweet(t, c_args, options = {}) { style="margin-left: -120px;margin-top: -3px;" > ` : ` - ${location.pathname.includes("/status/") - ? `

` - : ""} + ${ + location.pathname.includes("/status/") + ? `

` + : "" + }
` } From b500ad2957c2e8b16528b0b1c2f561ecbf773578 Mon Sep 17 00:00:00 2001 From: Shinon Date: Wed, 14 May 2025 04:27:48 +0800 Subject: [PATCH 04/29] More elementSafeties & work on tweetConstructor --- scripts/helpers.js | 16 +++- scripts/tweetConstructor.js | 177 +++++++++++++++++++++--------------- 2 files changed, 118 insertions(+), 75 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 490e733c..2d0c7510 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1420,11 +1420,21 @@ async function renderDiscovery(cache = true) { } } -const elNew = (tag, prop, children=[]) => { +const elNew = (tag, prop, children = []) => { const element = Object.assign(document.createElement(tag), prop); - if (children) children.forEach(child=>{element.appendChild(child)}); + if (children.length > 0) + children.forEach((child) => { + if (child !== null) { + if (typeof child === "string") { + element.appendChild(document.createTextNode(child)); + } else { + element.appendChild(child); + } + } + }); }; + const img_template = elNew("img", { crossorigin:"anonymous", loading:"lazy", @@ -1566,7 +1576,7 @@ function renderMedia(t) { if(cws.length > 0) { cws = [...new Set(cws)]; cws = LOC.content_warning.message.replace('$WARNINGS$', cws.join(', ')); - _html += html`
${cws}
`; + _html += `
` + elNew("div",{className:"tweet-media-cws"},[cws]).outerHTML; } return _html; } diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 97399bfa..f04b8c93 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -5,73 +5,100 @@ async function constructQuotedTweet( ) { const root = elNew("template"); - const rootAHref = elNew("a", { - className: "tweet-body-quote", - target: "_blank", - href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, - },[elNew("img", { - src: - t.quoted_status.user.default_profile_image && - vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(t.quoted_status.user.id_str) % 7 - }_normal.png` - ) - : t.quoted_status.user.profile_image_url_https, - alt: escapeHTML(t.quoted_status.user.name), - className: "tweet-avatar-quote", - width: "24", - height: "24", - }), -]); - - root.appendChild(rootAHref); - return ` - -
- - ${escapeHTML(t.quoted_status.user.name)} - @${t.quoted_status.user.screen_name} - -
- ${timeElapsed( - new Date(t.quoted_status.created_at).getTime() - )} - ${ - quoteMentionedUserText !== `` && !vars.useOldStyleReply - ? html` - ${LOC.replying_to_user.message.replace( + const rootAHref = elNew( + "a", + { + className: "tweet-body-quote", + target: "_blank", + href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, + }, + [ + elNew("img", { + src: + t.quoted_status.user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(t.quoted_status.user.id_str) % 7 + }_normal.png` + ) + : t.quoted_status.user.profile_image_url_https, + alt: escapeHTML(t.quoted_status.user.name), + className: "tweet-avatar-quote", + width: "24", + height: "24", + }), + // Header + elNew("div", { className: "tweet-header-quote" }, [ + elNew("span", { className: "tweet-header-info-quote" }, [ + // User name + elNew( + "b", + { + className: + "tweet-header-name-quote" + + t.quoted_status.user.verified + ? " user-verified" + : t.quoted_status.user.id_str === + "1708130407663759360" + ? " user-verified user-verified-dimden" + : "" + t.quoted_status.user.protected + ? " user-protected" + : "" + + t.quoted_status.user.verified_type === + "Government" + ? " user-verified-gray" + : t.quoted_status.user.verified_type === + "Business" + ? " user-verified-yellow" + : t.quoted_status.user.verified_type === + "Blue" + ? " user-verified-blue" + : "", + }, + [ + document.createTextNode( + escapeHTML(t.quoted_status.user.name) + ), + ] + ), + // At handle + elNew("span", { className: "tweet-header-handle-quote" }, [ + document.createTextNode( + "@" + escapeHTML(t.quoted_status.user.screen_name) + ), + ]), + ]), + ]), + // Tweet Time Quote + elNew( + "span", + { + className: "tweet-time-quote", + dataset: { + timestamp: new Date( + t.quoted_status.created_at + ).getTime(), + }, + title: new Date( + t.quoted_status.created_at + ).toLocaleString(), + }, + [ + document.createTextNode( + timeElapsed( + new Date(t.quoted_status.created_at).getTime() + ) + ), + ] + ), + quoteMentionedUserText !== `` && !vars.useOldStyleReply + ? elNew( + "span", + { className: "tweet-reply-to tweet-quote-reply-to" }, + [ + document.createTextNode( + LOC.replying_to_user.message.replace( "$SCREEN_NAME$", quoteMentionedUserText .trim() @@ -83,11 +110,17 @@ async function constructQuotedTweet( LOC.replying_to_comma.message, LOC.replying_to_and.message ) - )} - ` - : "" - } + ) + ), + ] + ) + : null, + ] + ); + + root.appendChild(rootAHref); + return root.innerHTML; + return ` Date: Wed, 14 May 2025 23:38:08 +0800 Subject: [PATCH 05/29] Convert more quote code to templated versions --- scripts/helpers.js | 4 +- scripts/tweetConstructor.js | 1536 +++++++++++++++++------------------ 2 files changed, 749 insertions(+), 791 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 2d0c7510..e1323b8a 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1427,11 +1427,13 @@ const elNew = (tag, prop, children = []) => { if (child !== null) { if (typeof child === "string") { element.appendChild(document.createTextNode(child)); - } else { + } + else { element.appendChild(child); } } }); + return element }; diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index f04b8c93..c145495a 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1,396 +1,368 @@ +function htmlToNodes(string) { + const tmp = document.createElement("template"); + tmp.innerHTML = string; + return tmp; +} + async function constructQuotedTweet( - t, - isQuoteMatchingLanguage, - quoteMentionedUserText + t, + isQuoteMatchingLanguage, + quoteMentionedUserText ) { - const root = elNew("template"); + const root = elNew("template"); - const rootAHref = elNew( - "a", + const rootAHref = elNew( + "a", + { + className: "tweet-body-quote", + target: "_blank", + href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, + }, + [ + elNew("img", { + src: + t.quoted_status.user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(t.quoted_status.user.id_str) % 7 + }_normal.png` + ) + : t.quoted_status.user.profile_image_url_https, + alt: escapeHTML(t.quoted_status.user.name), + className: "tweet-avatar-quote", + width: "24", + height: "24", + }), + // Header + elNew("div", { className: "tweet-header-quote" }, [ + elNew("span", { className: "tweet-header-info-quote" }, [ + // User name + elNew( + "b", + { + className: + "tweet-header-name-quote" + t.quoted_status.user.verified + ? " user-verified" + : t.quoted_status.user.id_str === "1708130407663759360" + ? " user-verified user-verified-dimden" + : "" + t.quoted_status.user.protected + ? " user-protected" + : "" + t.quoted_status.user.verified_type === "Government" + ? " user-verified-gray" + : t.quoted_status.user.verified_type === "Business" + ? " user-verified-yellow" + : t.quoted_status.user.verified_type === "Blue" + ? " user-verified-blue" + : "", + }, + [document.createTextNode(escapeHTML(t.quoted_status.user.name))] + ), + // At handle + elNew("span", { className: "tweet-header-handle-quote" }, [ + document.createTextNode( + "@" + escapeHTML(t.quoted_status.user.screen_name) + ), + ]), + ]), + ]), + // Tweet Time Quote + elNew( + "span", { - className: "tweet-body-quote", - target: "_blank", - href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, + className: "tweet-time-quote", + dataset: { + timestamp: new Date(t.quoted_status.created_at).getTime(), + }, + title: new Date(t.quoted_status.created_at).toLocaleString(), }, [ - elNew("img", { - src: - t.quoted_status.user.default_profile_image && - vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(t.quoted_status.user.id_str) % 7 - }_normal.png` - ) - : t.quoted_status.user.profile_image_url_https, - alt: escapeHTML(t.quoted_status.user.name), - className: "tweet-avatar-quote", - width: "24", - height: "24", - }), - // Header - elNew("div", { className: "tweet-header-quote" }, [ - elNew("span", { className: "tweet-header-info-quote" }, [ - // User name - elNew( - "b", - { - className: - "tweet-header-name-quote" + - t.quoted_status.user.verified - ? " user-verified" - : t.quoted_status.user.id_str === - "1708130407663759360" - ? " user-verified user-verified-dimden" - : "" + t.quoted_status.user.protected - ? " user-protected" - : "" + - t.quoted_status.user.verified_type === - "Government" - ? " user-verified-gray" - : t.quoted_status.user.verified_type === - "Business" - ? " user-verified-yellow" - : t.quoted_status.user.verified_type === - "Blue" - ? " user-verified-blue" - : "", - }, - [ - document.createTextNode( - escapeHTML(t.quoted_status.user.name) - ), - ] - ), - // At handle - elNew("span", { className: "tweet-header-handle-quote" }, [ - document.createTextNode( - "@" + escapeHTML(t.quoted_status.user.screen_name) - ), - ]), - ]), - ]), - // Tweet Time Quote - elNew( - "span", - { - className: "tweet-time-quote", - dataset: { - timestamp: new Date( - t.quoted_status.created_at - ).getTime(), - }, - title: new Date( - t.quoted_status.created_at - ).toLocaleString(), - }, - [ - document.createTextNode( - timeElapsed( - new Date(t.quoted_status.created_at).getTime() - ) - ), - ] - ), - quoteMentionedUserText !== `` && !vars.useOldStyleReply - ? elNew( - "span", - { className: "tweet-reply-to tweet-quote-reply-to" }, - [ - document.createTextNode( - LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - quoteMentionedUserText - .trim() - .replaceAll( - ` `, - LOC.replying_to_comma.message - ) - .replace( - LOC.replying_to_comma.message, - LOC.replying_to_and.message - ) - ) - ), - ] - ) - : null, + document.createTextNode( + timeElapsed(new Date(t.quoted_status.created_at).getTime()) + ), ] - ); + ), + quoteMentionedUserText !== `` && !vars.useOldStyleReply + ? elNew("span", { className: "tweet-reply-to tweet-quote-reply-to" }, [ + document.createTextNode( + LOC.replying_to_user.message.replace( + "$SCREEN_NAME$", + quoteMentionedUserText + .trim() + .replaceAll(` `, LOC.replying_to_comma.message) + .replace( + LOC.replying_to_comma.message, + LOC.replying_to_and.message + ) + ) + ), + ]) + : null, + elNew( + "span", + { + className: + "tweet-body-text tweet-body-text-quote tweet-body-text-long", + style: "color:var(--default-text-color)!important", + }, + [vars.useOldStyleReply ? quoteMentionedUserText : ""] + + t.quoted_status.full_text + ? htmlToNodes(await renderTweetBodyHTML(t, true)).children + : [] + ), + t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ? elNew( + "div", + { className: "tweet-media-quote" }, + t.quoted_status.extended_entities.media.map((m) => { + const [w, h] = quoteSizeFunctions[ + t.quoted_status.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + const mediaElement = elNew(m.type === "photo" ? "img" : "video", { + alt: escapeHTML(m.ext_alt_text, true), + title: escapeHTML(m.ext_alt_text, true), + crossorigin: "anonymous", + width: w, + height: h, + loading: "lazy", + }); + if (m.type == "video") { + mediaElement.disableRemotePlayback = true; + mediaElement.controls = true; + } + if (m.type == "animated_gif") { + mediaElement.disableRemotePlayback = true; + mediaElement.loop = true; + mediaElement.defaultMuted = true; + mediaElement.muted = true; + // XXX: To check if this works. + mediaElement.onclick = + "if(this.paused) this.play(); else this.pause()"; + } + if (m.type === "animated_gif" && !vars.disableGifAutoplay) + mediaElement.autoplay = true; + return mediaElement; + }) + ) + : ``, + ] + ); - root.appendChild(rootAHref); - return root.innerHTML; - return ` - ${vars.useOldStyleReply ? quoteMentionedUserText : ""}${ - t.quoted_status.full_text ? await renderTweetBodyHTML(t, true) : "" - } + // root.appendChild(rootAHref); + return root.innerHTML; + return ` ${ - t.quoted_status.extended_entities && - t.quoted_status.extended_entities.media - ? html` -
- ${t.quoted_status.extended_entities.media - .map( - (m) => - `<${ - m.type === "photo" - ? "img" - : "video" - } ${ - m.ext_alt_text - ? `alt="${escapeHTML( - m.ext_alt_text, - true - )}" title="${escapeHTML( - m.ext_alt_text, - true - )}"` - : "" - } crossorigin="anonymous" width="${ - quoteSizeFunctions[ - t.quoted_status - .extended_entities.media - .length - ]( - m.original_info.width, - m.original_info.height - )[0] - }" height="${ - quoteSizeFunctions[ - t.quoted_status - .extended_entities.media - .length - ]( - m.original_info.width, - m.original_info.height - )[1] - }" loading="lazy" ${ - m.type === "video" - ? "disableRemotePlayback controls" - : "" - } ${ - m.type === "animated_gif" - ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' - : "" - }${ - m.type === "animated_gif" && - !vars.disableGifAutoplay - ? " autoplay" - : "" - } src="${ - m.type === "photo" - ? m.media_url_https + - (vars.showOriginalImages && - (m.media_url_https.endsWith( - ".jpg" - ) || - m.media_url_https.endsWith( - ".png" - )) - ? "?name=orig" - : window.navigator && - navigator.connection && - navigator.connection - .type === - "cellular" && - !vars.disableDataSaver - ? "?name=small" - : "") - : m.video_info.variants.find( - (v) => - v.content_type === - "video/mp4" - ).url - }" class="tweet-media-element tweet-media-element-quote ${ - m.type === "animated_gif" - ? "tweet-media-element-quote-gif" - : "" - } ${ - mediaClasses[ - t.quoted_status - .extended_entities.media - .length - ] - } ${ - !vars.displaySensitiveContent && - t.quoted_status.possibly_sensitive - ? "tweet-media-element-censor" - : "" - }">${ - m.type === "photo" - ? "" - : "" - }` - ) - .join("\n")} -
- ` - : "" + t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ? html` +
+ ${t.quoted_status.extended_entities.media + .map( + (m) => + `<${ + m.type === "video" + ? "disableRemotePlayback controls" + : "" + } ${ + m.type === "animated_gif" + ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' + : "" + }${ + m.type === "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } src="${ + m.type === "photo" + ? m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith(".jpg") || + m.media_url_https.endsWith(".png")) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator.connection.type === + "cellular" && + !vars.disableDataSaver + ? "?name=small" + : "") + : m.video_info.variants.find( + (v) => v.content_type === "video/mp4" + ).url + }" class="tweet-media-element tweet-media-element-quote ${ + m.type === "animated_gif" + ? "tweet-media-element-quote-gif" + : "" + } ${ + mediaClasses[ + t.quoted_status.extended_entities.media.length + ] + } ${ + !vars.displaySensitiveContent && + t.quoted_status.possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${m.type === "photo" ? "" : ""}` + ) + .join("\n")} +
+ ` + : "" } ${ - !isQuoteMatchingLanguage - ? html` - ${LOC.view_translation.message} - ` - : `` + !isQuoteMatchingLanguage + ? html` + ${LOC.view_translation.message} + ` + : `` } `; } async function constructTweet(t, c_args, options = {}) { - const tweet_top = `${
-        t.user.name
-    }`; + const tweet_top = `${
+    t.user.name
+  }`; - const tweet_header = `
${escapeHTML(t.user.name)}@${ + t.user.screen_name + }${timeElapsed(new Date(t.created_at).getTime())}${ + location.pathname.split("?")[0].split("#")[0] === "/i/bookmarks" + ? `×` + : "" + } ${ + options.mainTweet && t.user.id_str !== user.id_str + ? `` + : "" + } ${ + !options.mainTweet && !c_args.isMatchingLanguage + ? `${ + `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && + innerWidth > 650 + ? LOC.view_translation.message : "" - }">${escapeHTML(t.user.name)}@${ - t.user.screen_name - }${timeElapsed(new Date(t.created_at).getTime())}${ - location.pathname.split("?")[0].split("#")[0] === "/i/bookmarks" - ? `×` - : "" - } ${ - options.mainTweet && t.user.id_str !== user.id_str - ? `` - : "" - } ${ - !options.mainTweet && !c_args.isMatchingLanguage - ? `${ - `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && - innerWidth > 650 - ? LOC.view_translation.message - : "" - }` - : "" - }
`; + }
` + : "" + }`; - // body - var mentioned_text = ""; - if ( - c_args.mentionedUserText !== `` && - !options.threadContinuation && - !options.noTop && - !location.pathname.includes("/status/") && - !vars.useOldStyleReply - ) { - mentioned_text = ` + // body + var mentioned_text = ""; + if ( + c_args.mentionedUserText !== `` && + !options.threadContinuation && + !options.noTop && + !location.pathname.includes("/status/") && + !vars.useOldStyleReply + ) { + mentioned_text = `
${LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - c_args.mentionedUserText - .trim() - .replaceAll( - `> <`, - `>${LOC.replying_to_comma.message}<` - ) - .replace( - `>${LOC.replying_to_comma.message}<`, - `>${LOC.replying_to_and.message}<` - ) + "$SCREEN_NAME$", + c_args.mentionedUserText + .trim() + .replaceAll(`> <`, `>${LOC.replying_to_comma.message}<`) + .replace( + `>${LOC.replying_to_comma.message}<`, + `>${LOC.replying_to_and.message}<` + ) )}
`; - } + } - const body_text = `
+ const body_text = `
${ - vars.useOldStyleReply - ? /*html*/ c_args.mentionedUserText - : "" + vars.useOldStyleReply + ? /*html*/ c_args.mentionedUserText + : "" }${ - c_args.full_text ? await renderTweetBodyHTML(t) : "" - } + c_args.full_text ? await renderTweetBodyHTML(t) : "" + }
`; - const translate_text = - !c_args.isMatchingLanguage && options.mainTweet - ? ` + const translate_text = + !c_args.isMatchingLanguage && options.mainTweet + ? `
${LOC.view_translation.message} ` - : ""; + : ""; - var extended_media = - t.extended_entities && t.extended_entities.media - ? ` + var extended_media = + t.extended_entities && t.extended_entities.media + ? `
${ - t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "video" - ? ` + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? `
` - : "" + : "" } ${renderMedia(t)}
${ - t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ? `
GIF
` - : "" + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ? `
GIF
` + : "" } ${ - c_args.videos - ? ` + c_args.videos + ? `
${ - c_args.videos[0].ext && - c_args.videos[0].ext.mediaStats && - c_args.videos[0].ext.mediaStats.r && - c_args.videos[0].ext.mediaStats.r.ok - ? `${Number( - c_args.videos[0].ext.mediaStats.r.ok - .viewCount - ) - .toLocaleString() - .replace(/\s/g, ",")} ${ - LOC.views.message - } • ` - : "" + c_args.videos[0].ext && + c_args.videos[0].ext.mediaStats && + c_args.videos[0].ext.mediaStats.r && + c_args.videos[0].ext.mediaStats.r.ok + ? `${Number( + c_args.videos[0].ext.mediaStats.r.ok.viewCount + ) + .toLocaleString() + .replace(/\s/g, ",")} ${ + LOC.views.message + } • ` + : "" }${LOC.reload.message} • ${c_args.videos[0].video_info.variants - .filter((v) => v.bitrate) - .map( - (v) => - `${ - v.url.match(/\/(\d+)x/)[1] + "p" - } ` - ) - .join(" / ")} + .filter((v) => v.bitrate) + .map( + (v) => + `${v.url.match(/\/(\d+)x/)[1] + "p"} ` + ) + .join(" / ")}
` - : `` + : `` } ` - : ""; + : ""; - const card = t.card ? `
` : ""; - const quoted_tweet = t.quoted_status - ? await constructQuotedTweet( - t, - c_args.isQuoteMatchingLanguage, - c_args.quoteMentionedUserText - ) - : ""; - var limited = ""; - if ( - t.limited_actions === "limit_trusted_friends_tweet" && - (options.mainTweet || !location.pathname.includes("/status/")) - ) { - limited = ` + const card = t.card ? `
` : ""; + const quoted_tweet = t.quoted_status + ? await constructQuotedTweet( + t, + c_args.isQuoteMatchingLanguage, + c_args.quoteMentionedUserText + ) + : ""; + var limited = ""; + if ( + t.limited_actions === "limit_trusted_friends_tweet" && + (options.mainTweet || !location.pathname.includes("/status/")) + ) { + limited = `
${LOC.circle_limited_tweet.message}
`.replace( - "$SCREEN_NAME$", - tweet.trusted_circle_owner - ? tweet.trusted_circle_owner - : tweetStorage[t.conversation_id_str] - ? tweetStorage[t.conversation_id_str].user.screen_name - : t.in_reply_to_screen_name - ? t.in_reply_to_screen_name - : t.user.screen_name - ); - } - const tomb_stone = t.tombstone - ? `
${t.tombstone}
` - : ""; - const country_restrictions = - (t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY"))) || - t.withheld_scope - ? `
+ "$SCREEN_NAME$", + tweet.trusted_circle_owner + ? tweet.trusted_circle_owner + : tweetStorage[t.conversation_id_str] + ? tweetStorage[t.conversation_id_str].user.screen_name + : t.in_reply_to_screen_name + ? t.in_reply_to_screen_name + : t.user.screen_name + ); + } + const tomb_stone = t.tombstone + ? `
${t.tombstone}
` + : ""; + const country_restrictions = + (t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY"))) || + t.withheld_scope + ? `
` - : ""; + : ""; - const conversation_control = t.conversation_control - ? `
${ - t.limited_actions_text - ? t.limited_actions_text - : LOC.limited_tweet.message - }${ - t.conversation_control.policy && - (t.user.id_str === user.id_str || - (t.conversation_control.policy.toLowerCase() === - "community" && - (t.user.followed_by || - (c_args.full_text && - c_args.full_text.includes( - `@${user.screen_name}` - )))) || - (t.conversation_control.policy.toLowerCase() === - "by_invitation" && - c_args.full_text && - c_args.full_text.includes(`@${user.screen_name}`))) - ? " " + LOC.you_can_reply.message - : "" - }
` - : ""; + const conversation_control = t.conversation_control + ? `
${ + t.limited_actions_text + ? t.limited_actions_text + : LOC.limited_tweet.message + }${ + t.conversation_control.policy && + (t.user.id_str === user.id_str || + (t.conversation_control.policy.toLowerCase() === "community" && + (t.user.followed_by || + (c_args.full_text && + c_args.full_text.includes(`@${user.screen_name}`)))) || + (t.conversation_control.policy.toLowerCase() === "by_invitation" && + c_args.full_text && + c_args.full_text.includes(`@${user.screen_name}`))) + ? " " + LOC.you_can_reply.message + : "" + }
` + : ""; - const tweet_footer = options.mainTweet - ? ` + const tweet_footer = options.mainTweet + ? `
` @@ -936,8 +840,8 @@ async function constructTweet(t, c_args, options = {}) { ${t.user.blocking ? " hidden" : ""} >${ t.user.following - ? c_args.unfollowUserText - : c_args.followUserText + ? tweetConstructorArgs.unfollowUserText + : tweetConstructorArgs.followUserText } ` @@ -948,8 +852,8 @@ async function constructTweet(t, c_args, options = {}) { ? html` ${t.user.blocking - ? c_args.unblockUserText - : c_args.blockUserText} ${t.user.muting @@ -1194,7 +1098,7 @@ async function constructTweet(t, c_args, options = {}) {
`; return [ - tweet_top + tweet_header, + tweet_top, mentioned_text + body_text + translate_text + diff --git a/scripts/tweetviewer.js b/scripts/tweetviewer.js index 01096e52..cea65e03 100644 --- a/scripts/tweetviewer.js +++ b/scripts/tweetviewer.js @@ -1433,7 +1433,7 @@ class TweetViewer { >${timeElapsed(new Date(t.created_at).getTime())}
-
', '>'); - .replaceAll('"', """) - .replaceAll("'", "'") - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">"); -} - } - - return DOMPurify.sanitize(String(unsafe), { ADD_ATTR: ["target"] }); } function html(strings, ...values) { From 1fcd0bd3754652ab49d64deb854eb071fea2e160 Mon Sep 17 00:00:00 2001 From: Shinon Date: Sun, 8 Jun 2025 22:37:32 +0800 Subject: [PATCH 11/29] Templating works & visual fixes: - Moved more code to js safe templates - Move dombuilder code to tdeb.js --- layouts/header/style.css | 6 +- scripts/helpers.js | 96 +--- scripts/tdeb.js | 90 ++++ scripts/tweetConstructor.js | 930 +++++++++++++++++++++++++----------- 4 files changed, 763 insertions(+), 359 deletions(-) create mode 100644 scripts/tdeb.js diff --git a/layouts/header/style.css b/layouts/header/style.css index b09d6ddb..e585749f 100644 --- a/layouts/header/style.css +++ b/layouts/header/style.css @@ -1334,19 +1334,19 @@ span.tweet-body-text { height: 16px; margin-left: 2px; margin-right: 2px; - vertical-align: text-top; + vertical-align: baseline; } .tweet-body-text-short .emoji, .tweet-body-text-short .hashflag { width: 26px !important; height: 26px !important; - vertical-align: text-top !important; + vertical-align: baseline !important; } .tweet-body-text-long .emoji, .tweet-body-text-long .hashflag { - vertical-align: text-top !important; + vertical-align: baseline !important; } #user-name .emoji, diff --git a/scripts/helpers.js b/scripts/helpers.js index c02246a7..02b50565 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1848,92 +1848,6 @@ async function renderDiscovery(cache = true) { } } -// Shim for isArray check. (cc. https://stackoverflow.com/a/20956445) -if (typeof Array.isArray === "undefined") { - Array.isArray = function (obj) { - return Object.prototype.toString.call(obj) === "[object Array]"; - }; -} - -// Tiny Dom Element builder. Adapted from stackoverflow. - -/** - * - * @param {tag} tag The dom tag to create. Can be anything. - * @param {object} prop Any tag properties to set. - * Some values such as `dataset`, `className`/`classList`/`class` goes through a custom transformation process. - * @param {any[]} children a list of children to put in the element. - * Strings are appended as a text string, Nodes are inserted as... Well, nodes. Any falsy values are discarded. - * @returns A Node. - */ -const elNew = (tag, prop, children = []) => { - const _elCustomValue = ["dataset", "className", "classList", "class"]; - const element = document.createElement(tag); - if (prop) { - const filteredObject = Object.keys(prop).reduce(function (returnObj, key) { - if (!_elCustomValue.includes(key) && prop[key]) - returnObj[key] = prop[key]; - return returnObj; - }, {}); - const customProps = Object.keys(prop).reduce(function ( - returnObj, - elementKey - ) { - if (_elCustomValue.includes(elementKey) && prop[elementKey]) - returnObj[elementKey] = prop[elementKey]; - return returnObj; - }, - {}); - Object.assign( - element, - filteredObject - ); - if (Object.keys(customProps).length > 0) { - for (const key in customProps) { - const propValue = customProps[key]; - if (key === "dataset") { - for (const datasetKey in propValue) { - element.dataset[datasetKey] = propValue[datasetKey]; - } - } else if ( - key === "className" || - key === "classList" || - key === "class" - ) { - if (typeof propValue === "string") { - element.className = propValue; - } else if ( - typeof propValue === "object" && - Array.isArray(propValue) - ) { - element.className = propValue.filter((m) => m).join(" "); - } else { - console.error( - `Passed in a non Array/String value to ${key}. will be ignored.` - ); - } - } - } - } - } - // Cleanup children. - children = children.filter((m) => m); - - if (children.length > 0) - children.forEach((child) => { - if (child !== null) { - if (typeof child === "string") { - if (child !== "") { - element.appendChild(document.createTextNode(child)); - } - } else { - element.appendChild(child); - } - } - }); - return element; -}; - const img_template = elNew("img", { crossorigin: "anonymous", loading: "lazy", @@ -2643,6 +2557,8 @@ async function appendTweet(t, timelineContainer, options = {}) { } let followUserText, unfollowUserText, blockUserText, unblockUserText; let mentionedUserText = ``; + let _newMentionedUserText = [] + let _newQuoteMentionedUserText = []; let quoteMentionedUserText = ``; if ( LOC.follow_user.message.includes("$SCREEN_NAME$") && @@ -2675,9 +2591,9 @@ async function appendTweet(t, timelineContainer, options = {}) { if (t.in_reply_to_screen_name && t.display_text_range) { t.entities.user_mentions.forEach((user_mention) => { if (user_mention.indices[0] < t.display_text_range[0]) { + _newMentionedUserText.push(elNew("a",{href:`/${user_mention.screen_name}`},[`@${user_mention.screen_name}`])) mentionedUserText += `@${user_mention.screen_name} `; } - //else this is not reply but mention }); } if ( @@ -2687,9 +2603,9 @@ async function appendTweet(t, timelineContainer, options = {}) { ) { t.quoted_status.entities.user_mentions.forEach((user_mention) => { if (user_mention.indices[0] < t.display_text_range[0]) { - quoteMentionedUserText += `@${user_mention.screen_name} `; + _newQuoteMentionedUserText.push(`@${user_mention.screen_name}`) + quoteMentionedUserText += `@${user_mention.screen_name}`; } - //else this is not reply but mention }); } // construct the markup for the tweet. @@ -2698,9 +2614,11 @@ async function appendTweet(t, timelineContainer, options = {}) { { videos: videos, isMatchingLanguage: isMatchingLanguage, + newMentionedUserText: _newMentionedUserText, mentionedUserText: mentionedUserText, full_text: full_text, isQuoteMatchingLanguage: isQuoteMatchingLanguage, + newQuoteMentionedUserText: _newQuoteMentionedUserText, quoteMentionedUserText: quoteMentionedUserText, followUserText: followUserText, unfollowUserText: unfollowUserText, diff --git a/scripts/tdeb.js b/scripts/tdeb.js new file mode 100644 index 00000000..4911739d --- /dev/null +++ b/scripts/tdeb.js @@ -0,0 +1,90 @@ + +// Shim for isArray check. (cc. https://stackoverflow.com/a/20956445) +if (typeof Array.isArray === "undefined") { + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === "[object Array]"; + }; +} + +// Tiny Dom Element builder. Adapted from stackoverflow. + +/** + * + * @param {tag} tag The dom tag to create. Can be anything. + * @param {object} prop Any tag properties to set. + * Some values such as `dataset`, `className`/`classList`/`class` goes through a custom transformation process. + * @param {any[]|string} children a list of children to put in the element. + * Strings are appended as a text string, Nodes are inserted as... Well, nodes. Any falsy values are discarded. + * @returns A Node. + */ +const elNew = (tag, prop, children = []) => { + const _elCustomValue = ["dataset", "className", "classList", "class"]; + const element = document.createElement(tag); + if (prop) { + const filteredObject = Object.keys(prop).reduce(function (returnObj, key) { + if (!_elCustomValue.includes(key) && prop[key]) + returnObj[key] = prop[key]; + return returnObj; + }, {}); + const customProps = Object.keys(prop).reduce(function ( + returnObj, + elementKey + ) { + if (_elCustomValue.includes(elementKey) && prop[elementKey]) + returnObj[elementKey] = prop[elementKey]; + return returnObj; + }, + {}); + Object.assign(element, filteredObject); + if (Object.keys(customProps).length > 0) { + for (const key in customProps) { + const propValue = customProps[key]; + if (key === "dataset") { + for (const datasetKey in propValue) { + element.dataset[datasetKey] = propValue[datasetKey]; + } + } else if ( + key === "className" || + key === "classList" || + key === "class" + ) { + if (typeof propValue === "string") { + element.className = propValue; + } else if ( + typeof propValue === "object" && + Array.isArray(propValue) + ) { + element.className = propValue.filter((m) => m).join(" "); + } else { + console.error( + `Passed in a non Array/String value to ${key}. will be ignored.` + ); + } + } + } + } + } + if (typeof children !== "string") { + // Cleanup children. + children = children.filter((m) => m); + + if (children.length > 0) + children.forEach((child) => { + if (child !== null) { + if (typeof child === "string") { + if (child !== "") { + element.appendChild(document.createTextNode(child)); + } + } else { + element.appendChild(child); + } + } + }); + } + else { + element.appendChild(children) + } + + return element; +}; + diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index e2ef9f0b..7c94d135 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -4,6 +4,169 @@ function htmlToNodes(string) { return tmp; } +const svgPlayIcon = ` + + + + +`; + +function interleave(arr, x) { + return arr.flatMap((e) => [e, x]).slice(0, -1); +} + +/** + * + * @param {object} tweetObject The tweet object. + * @returns {Element[]} an array of html elements + */ +function renderMultiMediaNodes(tweetObject) { + // let _html = ""; + let htmlNodes = []; + if (!tweetObject.extended_entities || !tweetObject.extended_entities.media) + return htmlNodes; + + let cws = []; + + for (let i = 0; i < tweetObject.extended_entities.media.length; i++) { + let m = tweetObject.extended_entities.media[i]; + let toCensor = + !vars.displaySensitiveContent && tweetObject.possibly_sensitive; + if (m.sensitive_media_warning) { + if (m.sensitive_media_warning.graphic_violence) { + cws.push(LOC.graphic_violence.message); + toCensor = !vars.uncensorGraphicViolenceAutomatically; + } + if (m.sensitive_media_warning.adult_content) { + cws.push(LOC.adult_content.message); + toCensor = !vars.uncensorAdultContentAutomatically; + } + if (m.sensitive_media_warning.other) { + cws.push(LOC.sensitive_content.message); + toCensor = !vars.uncensorSensitiveContentAutomatically; + } + } + if (m.type === "photo") { + let [w, h] = sizeFunctions[tweetObject.extended_entities.media.length]( + m.original_info.width, + m.original_info.height + ); + const newClone = img_template.cloneNode(true); + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.src = + m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith(".jpg") || + m.media_url_https.endsWith(".png")) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ? "?name=small" + : ""); + var mediaClass = mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + htmlNodes.push(newClone); + } else if (m.type === "animated_gif") { + let [w, h] = sizeFunctions[tweetObject.extended_entities.media.length]( + m.original_info.width, + m.original_info.height + ); + let rid = m.id_str + m.media_key; + + const newClone = animated_gif_template.cloneNode(true); + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + + newClone.defaultMuted = true; + newClone.muted = true; + newClone.disableRemotePlayback = true; + if (!vars.disableGifAutoplay) newClone.autoplay = true; + else newClone.autoplay = false; + var mediaClass = mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + m.video_info.variants.forEach((variant) => { + var source = document.createElement("source"); + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }); + newClone.appendChild( + document.createTextNode(LOC.unsupported_video.message) + ); + htmlNodes.push(newClone); + } else if (m.type === "video") { + if (m.mediaStats && m.mediaStats.viewCount) { + m.ext = { + mediaStats: { r: { ok: { viewCount: m.mediaStats.viewCount } } }, + }; + } + let [w, h] = sizeFunctions[tweetObject.extended_entities.media.length]( + m.original_info.width, + m.original_info.height + ); + const newClone = video_template.cloneNode(true); + const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + + newClone.disableRemotePlayback = true; + if (vars.muteVideos) { + newClone.defaultMuted = true; + newClone.muted = true; + } else { + newClone.defaultMuted = false; + newClone.muted = false; + } + var mediaClass = mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + newClone.poster = m.media_url_https; + m.video_info.variants.forEach((variant) => { + var source = document.createElement("source"); + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }); + newClone.appendChild( + document.createTextNode(LOC.unsupported_video.message) + ); + htmlNodes.push(newClone); + } + if (i === 1 && tweetObject.extended_entities.media.length > 3) { + htmlNodes.push(elNew("br")); + } + } + + if (cws.length > 0) { + cws = [...new Set(cws)]; + cws = LOC.content_warning.message.replace("$WARNINGS$", cws.join(", ")); + htmlNodes.push(elNew("br")); + htmlNodes.push(elNew("div", { className: "tweet-media-cws" }, [cws])); + } + return htmlNodes; +} + async function constructQuotedTweet( t, isQuoteMatchingLanguage, @@ -186,6 +349,13 @@ async function constructQuotedTweet( return rootAHref.outerHTML; } +/** + * + * @param {object} t Base Tweet content + * @param {object} tweetConstructorArgs Additional parsed constructor arguments + * @param {object} options oldTwitter specific arguments. + * @returns + */ async function constructTweet(t, tweetConstructorArgs, options = {}) { const _tweetTopConst = elNew("div", { className: "tweet-top", hidden: true }); @@ -335,43 +505,51 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { tweet_top += el.outerHTML; }); + // mentionText const doMentionText = tweetConstructorArgs.mentionedUserText !== `` && + !!tweetConstructorArgs.newMentionedUserText && !options.threadContinuation && !options.noTop && !location.pathname.includes("/status/") && !vars.useOldStyleReply; var mentioned_node = null; if (doMentionText) { - const replyHtml = LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - tweetConstructorArgs.mentionedUserText - .trim() - .replaceAll(`> <`, `>${LOC.replying_to_comma.message}<`) - .replace( - `>${LOC.replying_to_comma.message}<`, - `>${LOC.replying_to_and.message}<` - ) + // Taken from StackOverflow once again. + + tweetConstructorArgs.newMentionedUserText = interleave( + tweetConstructorArgs.newMentionedUserText, + LOC.replying_to_comma.message ); + // XXX: In theory, this should be the right order. + // Previously it was [` and ` | `, ` | `, ` | ...] + // but it should be: [`, ` | `, ` ... ` and `] + // But I cannot be 100% sure about this. - Ristellise. + if (tweetConstructorArgs.newMentionedUserText.length >= 5) { + const arrLength = tweetConstructorArgs.newMentionedUserText.length; + tweetConstructorArgs.newMentionedUserText[arrLength - 2] = + LOC.replying_to_and.message; + } + mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ - elNew("span", {}, [htmlToNodes(replyHtml).content]), + elNew("span", {}, tweetConstructorArgs.newMentionedUserText), ]); } + // Main text content + const longShortClass = + vars.noBigFont || + t.full_text.length > 280 || + !options.bigFont || + (!options.mainTweet && location.pathname.includes("/status/")) + ? "tweet-body-text-long" + : "tweet-body-text-short"; const body_node = elNew( "div", { lang: t.lang, - classList: [ - "tweet-body-text", - vars.noBigFont || - t.full_text.length > 280 || - !options.bigFont || - (!options.mainTweet && location.pathname.includes("/status/")) - ? "tweet-body-text-long" - : "tweet-body-text-short", - ], + classList: ["tweet-body-text", longShortClass], }, [ elNew("span", { class: ["tweet-body-text-span"] }, [ @@ -385,280 +563,498 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ] ); + // translate icon + var translate_node = null; if (!tweetConstructorArgs.isMatchingLanguage && options.mainTweet) { - elNew("div", {}, [ + translate_node = elNew("div", {}, [ elNew("br"), elNew("span", { class: ["tweet-button", "tweet-translate"] }, [ LOC.view_translation.message, ]), ]); } + const body_text = body_node.outerHTML; + // const body_text = `
+ // ${ + // vars.useOldStyleReply + // ? /*html*/ tweetConstructorArgs.mentionedUserText + // : "" + // }${ + // tweetConstructorArgs.full_text ? await renderTweetBodyHTML(t) : "" + // } + //
`; - const body_text = `
- ${ - vars.useOldStyleReply - ? /*html*/ tweetConstructorArgs.mentionedUserText - : "" - }${ - tweetConstructorArgs.full_text ? await renderTweetBodyHTML(t) : "" - } -
`; - const translate_text = - !tweetConstructorArgs.isMatchingLanguage && options.mainTweet - ? ` -
- ${LOC.view_translation.message} - ` - : ""; + const translate_text = translate_node ? translate_node.outerHTML : ""; - var extended_media = - t.extended_entities && t.extended_entities.media - ? ` -
- ${ - t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "video" - ? ` -
- - - - - - -
- ` - : "" - } - ${renderMedia(t)} -
- ${ - t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ? `
GIF
` - : "" - } - ${ - tweetConstructorArgs.videos - ? ` -
- ${ - tweetConstructorArgs.videos[0].ext && - tweetConstructorArgs.videos[0].ext.mediaStats && - tweetConstructorArgs.videos[0].ext.mediaStats.r && - tweetConstructorArgs.videos[0].ext.mediaStats.r.ok - ? `${Number( - tweetConstructorArgs.videos[0].ext.mediaStats - .r.ok.viewCount - ) - .toLocaleString() - .replace(/\s/g, ",")} ${ - LOC.views.message - } • ` - : "" - }${LOC.reload.message} - • - ${tweetConstructorArgs.videos[0].video_info.variants - .filter((v) => v.bitrate) - .map( - (v) => - `${v.url.match(/\/(\d+)x/)[1] + "p"} ` - ) - .join(" / ")} -
- ` - : `` - } - - ` - : ""; + // render media content elements + var extended_node = null; + if (t.extended_entities && t.extended_entities.media) { + // videoOverlay if it's only a single video. + const isSingleVideo = + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? true + : false; + var videoOverlay = null; + if (isSingleVideo) { + videoOverlay = elNew( + "div", + { class: ["tweet-media-video-overlay", "tweet-button"] }, + // XXX: htmlToNodes needs to always be called, else .content will be null/undefined. + [htmlToNodes(svgPlayIcon).content] + ); + console.log(videoOverlay, videoOverlay.outerHTML); + } + // Render media nodes + var mediaNodes = renderMultiMediaNodes(t); + + //
GIF
+ var gifControl = + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ? elNew("div", { class: "tweet-media-controls" }, ["GIF"]) + : null; + var videoControls = null; + if (tweetConstructorArgs.videos) { + var viewNode = null; + if ( + tweetConstructorArgs.videos[0].ext && + tweetConstructorArgs.videos[0].ext.mediaStats && + tweetConstructorArgs.videos[0].ext.mediaStats.r && + tweetConstructorArgs.videos[0].ext.mediaStats.r.ok + ) { + viewNode = Number( + tweetConstructorArgs.videos[0].ext.mediaStats.r.ok.viewCount + ); + viewNode = elNew( + "span", + { class: ["tweet-video-views", "tweet-button"] }, + [ + `${viewNode.toLocaleString().replace(/\s/g, ",")} ${ + LOC.views.message + } • `, + ] + ); + } + + const reloadNode = elNew( + "span", + { class: ["tweet-video-reload", "tweet-button"] }, + [LOC.reload.message] + ); + var resolutionNodes = tweetConstructorArgs.videos[0].video_info.variants + .filter((v) => v.bitrate) + .map((v) => + elNew("span", { class: [], dataset: { url: v.url } }, [ + v.url.match(/\/(\d+)x/)[1] + "p", + ]) + ); + videoControls = elNew("div", { class: "tweet-media-controls" }, [ + viewNode, + reloadNode, + " • ", + ...interleave(resolutionNodes, " / "), + ]); + } + // console.log([videoOverlay, ...mediaNodes]); + extended_node = elNew("template", {}, [ + elNew("div", { class: ["tweet-media"] }, [videoOverlay, ...mediaNodes]), + gifControl, + videoControls, + elNew("span", { class: "tweet-media-data" }), + ]); + } + var extended_media = ""; + if (extended_node) { + [].forEach.call(extended_node.children, function (el) { + extended_media += el.outerHTML; + }); + } + + // card placeholder if there is a card to placeholder. const card = t.card ? `
` : ""; - const quoted_tweet = t.quoted_status - ? await constructQuotedTweet( - t, - tweetConstructorArgs.isQuoteMatchingLanguage, - tweetConstructorArgs.quoteMentionedUserText - ) - : ""; + // quoted tweet status. + let quoted_tweet = ""; + if (t.quoted_status) { + quoted_tweet = await constructQuotedTweet( + t, + tweetConstructorArgs.isQuoteMatchingLanguage, + tweetConstructorArgs.quoteMentionedUserText + ); + } + + // limited text var limited = ""; if ( t.limited_actions === "limit_trusted_friends_tweet" && (options.mainTweet || !location.pathname.includes("/status/")) ) { - limited = ` -
- ${LOC.circle_limited_tweet.message} - ${LOC.learn_more.message} -
- `.replace( - "$SCREEN_NAME$", - tweet.trusted_circle_owner - ? tweet.trusted_circle_owner - : tweetStorage[t.conversation_id_str] - ? tweetStorage[t.conversation_id_str].user.screen_name - : t.in_reply_to_screen_name - ? t.in_reply_to_screen_name - : t.user.screen_name - ); + let screen_name = null; + if (tweet.trusted_circle_owner) { + screen_name = tweet.trusted_circle_owner; + } else if (tweetStorage[t.conversation_id_str]) { + screen_name = tweetStorage[t.conversation_id_str].user.screen_name; + } else if (t.in_reply_to_screen_name) { + screen_name = t.in_reply_to_screen_name; + } else { + screen_name = t.user.screen_name; + } + limited = elNew("div", { class: ["tweet-limited"] }, [ + LOC.circle_limited_tweet.message.replace("$SCREEN_NAME$", screen_name), + elNew( + "a", + { + href: "https://help.twitter.com/en/using-twitter/twitter-circle", + target: "blank", + }, + [LOC.learn_more.message] + ), + ]).outerHTML; + + // limited = ` + //
+ // ${LOC.circle_limited_tweet.message} + // ${LOC.learn_more.message} + //
+ // `.replace( + // "$SCREEN_NAME$", + // screen_name + // ); } + // tombstoned const tomb_stone = t.tombstone - ? `
${t.tombstone}
` + ? elNew("div", { class: ["tweet-warning"] }, [t.tombstone]).outerHTML : ""; - const country_restrictions = + // country restricted text. + var country_restrictions = ""; + if ( (t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) || t.withheld_scope - ? `
- This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` - : ""; + ) { + country_restrictions = elNew("div", { class: ["tweet-warning"] }, [ + "This Tweet has been withheld in response to a report from the copyright holder.", + elNew( + "a", + { + href: "https://help.twitter.com/en/rules-and-policies/copyright-policy", + target: "_blank", + }, + ["Learn more."] + ), + ]); + } + // const country_restrictions = + // (t.withheld_in_countries && + // (t.withheld_in_countries.includes("XX") || + // t.withheld_in_countries.includes("XY"))) || + // t.withheld_scope + // ? `
+ // This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` + // : ""; - const conversation_control = t.conversation_control - ? `
${ - t.limited_actions_text - ? t.limited_actions_text - : LOC.limited_tweet.message - }${ - t.conversation_control.policy && - (t.user.id_str === user.id_str || - (t.conversation_control.policy.toLowerCase() === "community" && - (t.user.followed_by || - (tweetConstructorArgs.full_text && - tweetConstructorArgs.full_text.includes( - `@${user.screen_name}` - )))) || - (t.conversation_control.policy.toLowerCase() === "by_invitation" && - tweetConstructorArgs.full_text && - tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))) - ? " " + LOC.you_can_reply.message - : "" - }
` - : ""; + var conversation_control = ""; + if (t.conversation_control) { + const limitedActions = t.limited_actions_text + ? t.limited_actions_text + : LOC.limited_tweet.message; + var replyText = + t.conversation_control.policy && + (t.user.id_str === user.id_str || + (t.conversation_control.policy.toLowerCase() === "community" && + (t.user.followed_by || + (tweetConstructorArgs.full_text && + tweetConstructorArgs.full_text.includes( + `@${user.screen_name}` + )))) || + (t.conversation_control.policy.toLowerCase() === "by_invitation" && + tweetConstructorArgs.full_text && + tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))); + if (replyText) { + replyText = " " + LOC.you_can_reply.message; + } else { + replyText = ""; + } + conversation_control = elNew("div", { class: "tweet-warning" }, [ + limitedActions, + replyText, + ]).outerHTML; + } - const tweet_footer = options.mainTweet - ? ` - - ` - : ""; + // const conversation_control = t.conversation_control + // ? `
${ + // t.limited_actions_text + // ? t.limited_actions_text + // : LOC.limited_tweet.message + // }${ + // t.conversation_control.policy && + // (t.user.id_str === user.id_str || + // (t.conversation_control.policy.toLowerCase() === "community" && + // (t.user.followed_by || + // (tweetConstructorArgs.full_text && + // tweetConstructorArgs.full_text.includes( + // `@${user.screen_name}` + // )))) || + // (t.conversation_control.policy.toLowerCase() === "by_invitation" && + // tweetConstructorArgs.full_text && + // tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))) + // ? " " + LOC.you_can_reply.message + // : "" + // }
` + // : ""; - const tweet_date = `
${new Date(t.created_at) - .toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" }) - .toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { - day: "numeric", - month: "short", - year: "numeric", - })}  ・ ${t.source ? t.source.split(">")[1].split("<")[0] : "Unknown"}
`; + var tweet_footer = ""; + if (options.mainTweet) { + var statsArray = []; + // Replies + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}`, + class: ["tweet-footer-stat", "tweet-footer-stat-o"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.replies.message, + ]), + elNew( + "b", + { + class: ["tweet-footer-stat-count", "tweet-footer-stat-replies"], + }, + [formatLargeNumber(t.reply_count).replace(/\s/g, ",")] + ), + ] + ) + ); + // Retweets + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/retweets`, + class: ["tweet-footer-stat", "tweet-footer-stat-o"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.retweets.message, + ]), + elNew( + "b", + { + class: ["tweet-footer-stat-count", "tweet-footer-stat-r"], + }, + [formatLargeNumber(t.retweet_count).replace(/\s/g, ",")] + ), + ] + ) + ); + if ( + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ) { + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`, + class: ["tweet-footer-stat", "tweet-footer-stat-q"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.quotes.message, + ]), + elNew( + "b", + { + class: ["tweet-footer-stat-count", "tweet-footer-stat-quotes"], + }, + [formatLargeNumber(t.quote_count).replace(/\s/g, ",")] + ), + ] + ) + ); + } + // Likes + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/likes`, + class: ["tweet-footer-stat", "tweet-footer-stat-f"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + vars.heartsNotStars ? LOC.likes.message : LOC.favorites.message, + ]), + elNew( + "b", + { + class: ["tweet-footer-stat-count", "tweet-footer-stat-favorites"], + }, + [formatLargeNumber(t.favorite_count).replace(/\s/g, ",")] + ), + ] + ) + ); + + tweet_footer = elNew("div", { class: ["tweet-footer"] }, [ + elNew("div", { class: ["tweet-footer-stats"] }, statsArray), + ]).outerHTML; + } + const tweet_date = elNew( + "a", + { + hidden: !options.mainTweet ? true : false, + class: ["tweet-date"], + title: new Date(t.created_at).toLocaleString(), + href: `/${t.user.screen_name}/status/${t.id_str}`, + }, + [ + elNew("br"), + new Date(t.created_at) + .toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" }) + .toLowerCase(), + " - ", + new Date(t.created_at).toLocaleDateString(undefined, { + day: "numeric", + month: "short", + year: "numeric", + }), + "  ・ ", + t.source ? t.source.split(">")[1].split("<")[0] : "Unknown", + ] + ).outerHTML; + + // solve additional retweet actions + var retweetClasses = ["tweet-button", "tweet-interact-retweet"]; + if (t.retweeted) { + retweetClasses.push("tweet-interact-retweeted"); + } + if ( + (t.user.protected || t.limited_actions === "limit_trusted_friends_tweet") && + t.user.id_str !== user.id_str + ) { + retweetClasses.push("tweet-interact-retweet-disabled"); + } + + var likeClasses = ["tweet-button", "tweet-interact-favorite"]; + if (t.favorited) { + likeClasses.push("tweet-interact-favorited"); + } + // dropdown for retweet actions + var retweetDropdownArray = [ + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-retweet"], + }, + [ + t.retweeted ? LOC.unretweet.message : LOC.retweet.message, + !vars.disableHotkeys ? " (T)" : "", + ] + ), + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-quote"], + }, + [LOC.quote_tweet.message, !vars.disableHotkeys ? " (Q)" : ""] + ), + ]; + if (options.mainTweet) { + retweetDropdownArray.push( + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-quotes"], + }, + [LOC.see_quotes_big.message] + ) + ); + retweetDropdownArray.push( + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-retweeters"], + }, + [LOC.see_retweeters.message] + ) + ); + } + + // Tweet Interactions buttons + elNew("div", { class: ["tweet-interact"] }, [ + // reply + elNew( + "span", + { + class: ["tweet-button", "tweet-interact-reply"], + title: `${LOC.reply_btn.message}${!vars.disableHotkeys ? " (R)" : ""}`, + dataset: { val: t.reply_count }, + }, + [ + options.mainTweet + ? null + : formatLargeNumber(t.reply_count).replace(/\s/g, ","), + ] + ), + // retweet + elNew( + "span", + { + class: ["tweet-button", "tweet-interact-favorite"], + title: `${LOC.reply_btn.message}${!vars.disableHotkeys ? " (L)" : ""}`, + dataset: { val: t.retweet_count }, + }, + [ + options.mainTweet + ? null + : formatLargeNumber(t.retweet_count).replace(/\s/g, ","), + ] + ), + // Retweet dropdown box + elNew( + "div", + { + class: ["tweet-interact-retweet-menu", "dropdown-menu"], + hidden: true, + }, + retweetDropdownArray + ), + // Like actions + elNew( + "span", + { + class: likeClasses, + title: `${ + vars.heartsNotStars ? LOC.like_btn.message : LOC.favorite_btn.message + }${!vars.disableHotkeys ? " (L)" : ""}`, + dataset: { val: t.favorite_count }, + }, + [ + options.mainTweet + ? null + : formatLargeNumber(t.retweet_count).replace(/\s/g, ","), + ] + ), + ]); const tweet_interact = `
@${user_mention.screen_name} `; + mentionedUserArray.push(elNew("a",{href:`/${user_mention.screen_name}`},[`@${user_mention.screen_name}`])) } }); } @@ -2614,8 +2612,8 @@ async function appendTweet(t, timelineContainer, options = {}) { { videos: videos, isMatchingLanguage: isMatchingLanguage, - newMentionedUserText: _newMentionedUserText, - mentionedUserText: mentionedUserText, + mentionedUserTextArray: mentionedUserArray, + // mentionedUserText: mentionedUserText, full_text: full_text, isQuoteMatchingLanguage: isQuoteMatchingLanguage, newQuoteMentionedUserText: _newQuoteMentionedUserText, diff --git a/scripts/tdeb.js b/scripts/tdeb.js index 4911739d..f3c99fd3 100644 --- a/scripts/tdeb.js +++ b/scripts/tdeb.js @@ -6,6 +6,28 @@ if (typeof Array.isArray === "undefined") { }; } +/** + * + * @param {string} string html string to convert to a html content + * @returns template element. + * @note This code does not sanitize **any** html and is inheriently unsafe. + */ +function htmlToNodes(string) { + const tmp = document.createElement("template"); + tmp.innerHTML = string; + return tmp; +} + +/** + * + * @param {any[]} arr An array support flatMap and slice. + * @param {any} x the item to interleave each item into. + * @returns + */ +function interleave(arr, x) { + return arr.flatMap((e) => [e, x]).slice(0, -1); +} + // Tiny Dom Element builder. Adapted from stackoverflow. /** @@ -13,7 +35,7 @@ if (typeof Array.isArray === "undefined") { * @param {tag} tag The dom tag to create. Can be anything. * @param {object} prop Any tag properties to set. * Some values such as `dataset`, `className`/`classList`/`class` goes through a custom transformation process. - * @param {any[]|string} children a list of children to put in the element. + * @param {any[]|string} children a list of children to put in the element. Can be a single string. * Strings are appended as a text string, Nodes are inserted as... Well, nodes. Any falsy values are discarded. * @returns A Node. */ diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 7c94d135..79d60248 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1,9 +1,4 @@ -function htmlToNodes(string) { - const tmp = document.createElement("template"); - tmp.innerHTML = string; - return tmp; -} - +// svgPlayIcon. const svgPlayIcon = ` @@ -14,10 +9,6 @@ const svgPlayIcon = ` { + const [w, h] = quoteSizeFunctions[ + t.quoted_status.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + const mediaElement = elNew(m.type === "photo" ? "img" : "video", { + alt: escapeHTML(m.ext_alt_text, true), + title: escapeHTML(m.ext_alt_text, true), + crossorigin: "anonymous", + width: w, + height: h, + loading: "lazy", + }); + if (m.type == "video") { + mediaElement.disableRemotePlayback = true; + mediaElement.controls = true; + } + if (m.type == "animated_gif") { + mediaElement.disableRemotePlayback = true; + mediaElement.loop = true; + mediaElement.defaultMuted = true; + mediaElement.muted = true; + // XXX: To check if this works. + mediaElement.onclick = "if(this.paused) this.play(); else this.pause()"; + if (!vars.disableGifAutoplay) mediaElement.autoplay = true; + } + if (m.type === "photo") { + var base_url = m.media_url_https; + if ( + vars.showOriginalImages && + (m.media_url_https.endsWith(".jpg") || + m.media_url_https.endsWith(".png")) + ) { + base_url += "?name=orig"; + } else if ( + window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ) { + base_url += "?name=small"; + } + mediaElement.src = base_url; + } else { + mediaElement.src = m.video_info.variants.find( + (v) => v.content_type === "video/mp4" + ).url; + } + mediaElement.classList.add( + "tweet-media-element", + "tweet-media-element-quote" + ); + if (m.type === "animated_gif") { + mediaElement.classList.add("tweet-media-element-quote-gif"); + } + mediaElement.classList.add( + mediaClasses[t.quoted_status.extended_entities.media.length] + ); + if (!vars.displaySensitiveContent && t.quoted_status.possibly_sensitive) { + mediaElement.classList.add("tweet-media-element-censor"); + } + return mediaElement; + }); + mediaElement = elNew("div", { className: "tweet-media-quote" }, mediaMap); + } + const rootAHref = elNew( "a", { @@ -180,163 +346,14 @@ async function constructQuotedTweet( href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, }, [ - elNew("img", { - src: - t.quoted_status.user.default_profile_image && - vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(t.quoted_status.user.id_str) % 7 - }_normal.png` - ) - : t.quoted_status.user.profile_image_url_https, - alt: escapeHTML(t.quoted_status.user.name), - className: "tweet-avatar-quote", - width: "24", - height: "24", - }), + profileElement, // Header - elNew("div", { className: "tweet-header-quote" }, [ - elNew("span", { className: "tweet-header-info-quote" }, [ - // User name - elNew( - "b", - { - className: - "tweet-header-name-quote " + t.quoted_status.user.verified - ? " user-verified" - : t.quoted_status.user.id_str === "1708130407663759360" - ? " user-verified user-verified-dimden" - : "" + t.quoted_status.user.protected - ? " user-protected" - : "" + t.quoted_status.user.verified_type === "Government" - ? "user-verified-gray" - : t.quoted_status.user.verified_type === "Business" - ? "user-verified-yellow" - : t.quoted_status.user.verified_type === "Blue" - ? "user-verified-blue" - : "", - }, - [escapeHTML(t.quoted_status.user.name)] - ), - " ", - // At handle - elNew("span", { className: "tweet-header-handle-quote" }, [ - "@" + escapeHTML(t.quoted_status.user.screen_name), - ]), - ]), - ]), + nameElement, // Tweet Time Quote - elNew( - "span", - { - className: "tweet-time-quote", - dataset: { - timestamp: new Date(t.quoted_status.created_at).getTime(), - }, - title: new Date(t.quoted_status.created_at).toLocaleString(), - }, - [timeElapsed(new Date(t.quoted_status.created_at).getTime())] - ), - quoteMentionedUserText !== `` && !vars.useOldStyleReply - ? elNew("span", { className: "tweet-reply-to tweet-quote-reply-to" }, [ - LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - quoteMentionedUserText - .trim() - .replaceAll(` `, LOC.replying_to_comma.message) - .replace( - LOC.replying_to_comma.message, - LOC.replying_to_and.message - ) - ), - ]) - : null, - elNew( - "span", - { - className: - "tweet-body-text tweet-body-text-quote tweet-body-text-long", - style: "color:var(--default-text-color)!important", - }, - [vars.useOldStyleReply ? quoteMentionedUserText : ""] + - t.quoted_status.full_text - ? [htmlToNodes(await renderTweetBodyHTML(t, true)).content] - : [] - ), - t.quoted_status.extended_entities && - t.quoted_status.extended_entities.media - ? elNew( - "div", - { className: "tweet-media-quote" }, - t.quoted_status.extended_entities.media.map((m) => { - const [w, h] = quoteSizeFunctions[ - t.quoted_status.extended_entities.media.length - ](m.original_info.width, m.original_info.height); - const mediaElement = elNew(m.type === "photo" ? "img" : "video", { - alt: escapeHTML(m.ext_alt_text, true), - title: escapeHTML(m.ext_alt_text, true), - crossorigin: "anonymous", - width: w, - height: h, - loading: "lazy", - }); - if (m.type == "video") { - mediaElement.disableRemotePlayback = true; - mediaElement.controls = true; - } - if (m.type == "animated_gif") { - mediaElement.disableRemotePlayback = true; - mediaElement.loop = true; - mediaElement.defaultMuted = true; - mediaElement.muted = true; - // XXX: To check if this works. - mediaElement.onclick = - "if(this.paused) this.play(); else this.pause()"; - if (!vars.disableGifAutoplay) mediaElement.autoplay = true; - } - if (m.type === "photo") { - var base_url = m.media_url_https; - if ( - vars.showOriginalImages && - (m.media_url_https.endsWith(".jpg") || - m.media_url_https.endsWith(".png")) - ) { - base_url += "?name=orig"; - } else if ( - window.navigator && - navigator.connection && - navigator.connection.type === "cellular" && - !vars.disableDataSaver - ) { - base_url += "?name=small"; - } - mediaElement.src = base_url; - } else { - mediaElement.src = m.video_info.variants.find( - (v) => v.content_type === "video/mp4" - ).url; - } - mediaElement.classList.add( - "tweet-media-element", - "tweet-media-element-quote" - ); - if (m.type === "animated_gif") { - mediaElement.classList.add("tweet-media-element-quote-gif"); - } - mediaElement.classList.add( - mediaClasses[t.quoted_status.extended_entities.media.length] - ); - if ( - !vars.displaySensitiveContent && - t.quoted_status.possibly_sensitive - ) { - mediaElement.classList.add("tweet-media-element-censor"); - } - return mediaElement; - }) - ) - : ``, + tweetTimeElement, + newStyleReplyTo, + bodyElement, + mediaElement, !isQuoteMatchingLanguage ? elNew( "span", @@ -350,7 +367,9 @@ async function constructQuotedTweet( } /** - * + * Constructs a full tweet markup. + * NOTE: This function is very huge. + * It should be possible to chunk and split up more functions but I'd rather get *something* up for testing than to do that. - Ristellise * @param {object} t Base Tweet content * @param {object} tweetConstructorArgs Additional parsed constructor arguments * @param {object} options oldTwitter specific arguments. @@ -507,8 +526,8 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // mentionText const doMentionText = - tweetConstructorArgs.mentionedUserText !== `` && - !!tweetConstructorArgs.newMentionedUserText && + // tweetConstructorArgs.mentionedUserText !== `` && + !!tweetConstructorArgs.mentionedUserTextArray && !options.threadContinuation && !options.noTop && !location.pathname.includes("/status/") && @@ -517,8 +536,8 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { if (doMentionText) { // Taken from StackOverflow once again. - tweetConstructorArgs.newMentionedUserText = interleave( - tweetConstructorArgs.newMentionedUserText, + tweetConstructorArgs.mentionedUserTextArray = interleave( + tweetConstructorArgs.mentionedUserTextArray, LOC.replying_to_comma.message ); @@ -526,14 +545,14 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // Previously it was [` and ` | `, ` | `, ` | ...] // but it should be: [`, ` | `, ` ... ` and `] // But I cannot be 100% sure about this. - Ristellise. - if (tweetConstructorArgs.newMentionedUserText.length >= 5) { - const arrLength = tweetConstructorArgs.newMentionedUserText.length; - tweetConstructorArgs.newMentionedUserText[arrLength - 2] = + if (tweetConstructorArgs.mentionedUserTextArray.length >= 5) { + const arrLength = tweetConstructorArgs.mentionedUserTextArray.length; + tweetConstructorArgs.mentionedUserTextArray[arrLength - 2] = LOC.replying_to_and.message; } mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ - elNew("span", {}, tweetConstructorArgs.newMentionedUserText), + elNew("span", {}, tweetConstructorArgs.mentionedUserTextArray), ]); } @@ -545,23 +564,50 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { (!options.mainTweet && location.pathname.includes("/status/")) ? "tweet-body-text-long" : "tweet-body-text-short"; - const body_node = elNew( + + var bodyTextChildren = []; + if (vars.useOldStyleReply) { + bodyTextChildren.push( + ...interleave(tweetConstructorArgs.mentionedUserTextArray, " ") + ); + } + if (tweetConstructorArgs.full_text) { + if (bodyTextChildren.length > 0) { + bodyTextChildren.push(" "); + } + // XXX: renderTweetBodyHTML returns html. To refactor later. + bodyTextChildren.push(htmlToNodes(await renderTweetBodyHTML(t)).content); + } + + // const body_node = elNew( + // "div", + // { + // lang: t.lang, + // classList: ["tweet-body-text", longShortClass], + // }, + // [ + // elNew("span", { class: ["tweet-body-text-span"] }, [ + // vars.useOldStyleReply + // ? htmlToNodes(tweetConstructorArgs.mentionedUserText).content + // : null, + // tweetConstructorArgs.full_text + // ? htmlToNodes(await renderTweetBodyHTML(t)).content + // : null, + // ]), + // ] + // ); + + const bodyNodeV2 = elNew( "div", { lang: t.lang, classList: ["tweet-body-text", longShortClass], }, - [ - elNew("span", { class: ["tweet-body-text-span"] }, [ - vars.useOldStyleReply - ? htmlToNodes(tweetConstructorArgs.mentionedUserText).content - : null, - tweetConstructorArgs.full_text - ? htmlToNodes(await renderTweetBodyHTML(t)).content - : null, - ]), - ] + [elNew("span", { class: ["tweet-body-text-span"] }, bodyTextChildren)] ); + // if (body_node.outerHTML != bodyNodeV2.outerHTML) { + // console.log("nodeCompare", body_node, bodyNodeV2); + // } // translate icon var translate_node = null; @@ -573,23 +619,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ]), ]); } - const body_text = body_node.outerHTML; - // const body_text = `
- // ${ - // vars.useOldStyleReply - // ? /*html*/ tweetConstructorArgs.mentionedUserText - // : "" - // }${ - // tweetConstructorArgs.full_text ? await renderTweetBodyHTML(t) : "" - // } - //
`; + const body_text = bodyNodeV2.outerHTML; const translate_text = translate_node ? translate_node.outerHTML : ""; From 6cde4b088053c929bcbc2ccffcbbca83fbbc8d4e Mon Sep 17 00:00:00 2001 From: Shinon Date: Sun, 8 Jun 2025 23:20:36 +0800 Subject: [PATCH 13/29] middle alignment for emojis, add tdeb.js --- layouts/header/style.css | 6 +++--- manifest.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/layouts/header/style.css b/layouts/header/style.css index e585749f..57e02795 100644 --- a/layouts/header/style.css +++ b/layouts/header/style.css @@ -1334,19 +1334,19 @@ span.tweet-body-text { height: 16px; margin-left: 2px; margin-right: 2px; - vertical-align: baseline; + vertical-align: middle; } .tweet-body-text-short .emoji, .tweet-body-text-short .hashflag { width: 26px !important; height: 26px !important; - vertical-align: baseline !important; + vertical-align: middle !important; } .tweet-body-text-long .emoji, .tweet-body-text-long .hashflag { - vertical-align: baseline !important; + vertical-align: middle !important; } #user-name .emoji, diff --git a/manifest.json b/manifest.json index bdc16a4e..2e899ef5 100644 --- a/manifest.json +++ b/manifest.json @@ -89,7 +89,7 @@ "https://x.com/i/tweetdeck", "https://x.com/i/communitynotes", "https://x.com/i/broadcasts/*", "https://x.com/search-advanced", "https://x.com/x/migrate", "https://x.com/i/grok" ], - "js": ["scripts/blockBeforeInject.js", "scripts/config.js", "scripts/tweetConstructor.js", "scripts/helpers.js", "scripts/apis.js", "scripts/twchallenge.js", "scripts/injection.js"], + "js": ["scripts/blockBeforeInject.js", "scripts/config.js", "scripts/tdeb.js", "scripts/tweetConstructor.js", "scripts/helpers.js", "scripts/apis.js", "scripts/twchallenge.js", "scripts/injection.js"], "css": ["libraries/viewer.min.css"], "all_frames": true, "run_at": "document_start" From c0b9c343a5c4137d704c9008e884306995cad64d Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 00:04:05 +0800 Subject: [PATCH 14/29] Some more quote works... Though I'm missing something here. --- scripts/tweetConstructor.js | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 79d60248..30273994 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -168,7 +168,8 @@ function renderMultiMediaNodes(tweetObject) { async function constructQuotedTweet( t, isQuoteMatchingLanguage, - quoteMentionedUserText + quoteMentionedUserText, + newQuoteMentionedUserText, ) { // === Profile Element === const profileElement = elNew("img", { @@ -236,9 +237,21 @@ async function constructQuotedTweet( [timeElapsed(new Date(t.quoted_status.created_at).getTime())] ); - var newStyleReplyTo = null; - if (quoteMentionedUserText !== `` && !vars.useOldStyleReply) { - newStyleReplyTo = elNew( + var oldStyleReplyTo = null; + if (!!newQuoteMentionedUserText && !vars.useOldStyleReply) { + + newQuoteMentionedUserText = interleave( + newQuoteMentionedUserText, + LOC.replying_to_comma.message + ); + + if (newQuoteMentionedUserText.length >=5) { + const arrLength = newQuoteMentionedUserText.length; + newQuoteMentionedUserText[arrLength - 2] = + LOC.replying_to_and.message; + } + + oldStyleReplyTo = elNew( "span", { className: "tweet-reply-to tweet-quote-reply-to" }, [ @@ -351,7 +364,7 @@ async function constructQuotedTweet( nameElement, // Tweet Time Quote tweetTimeElement, - newStyleReplyTo, + oldStyleReplyTo, bodyElement, mediaElement, !isQuoteMatchingLanguage @@ -536,6 +549,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { if (doMentionText) { // Taken from StackOverflow once again. + tweetConstructorArgs.mentionedUserTextArray = interleave( tweetConstructorArgs.mentionedUserTextArray, LOC.replying_to_comma.message @@ -550,7 +564,8 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { tweetConstructorArgs.mentionedUserTextArray[arrLength - 2] = LOC.replying_to_and.message; } - + // XXX: This is actually missing a `LOC.replying_to_user.message`. + // Will need to figure out how to replace it. mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ elNew("span", {}, tweetConstructorArgs.mentionedUserTextArray), ]); From ecca169dc1960f6ddec5ca0e81ea285fbc9df5d8 Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 15:03:48 +0800 Subject: [PATCH 15/29] Potentially fix replying_to_user localization --- scripts/helpers.js | 2 +- scripts/tweetConstructor.js | 281 ++++++++++++++++++++---------------- 2 files changed, 155 insertions(+), 128 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 8347ac5e..05c65767 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -2613,7 +2613,6 @@ async function appendTweet(t, timelineContainer, options = {}) { videos: videos, isMatchingLanguage: isMatchingLanguage, mentionedUserTextArray: mentionedUserArray, - // mentionedUserText: mentionedUserText, full_text: full_text, isQuoteMatchingLanguage: isQuoteMatchingLanguage, newQuoteMentionedUserText: _newQuoteMentionedUserText, @@ -2713,6 +2712,7 @@ async function appendTweet(t, timelineContainer, options = {}) { Array.from(tweet.getElementsByClassName("tweet-video-quality")).forEach( (el) => el.addEventListener("click", () => { + console.log("Reshuffling", el) if (el.className.includes("tweet-video-quality-current")) return; localStorage.preferredQuality = parseInt(el.innerText); let vid = Array.from( diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 30273994..778f8953 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -169,7 +169,7 @@ async function constructQuotedTweet( t, isQuoteMatchingLanguage, quoteMentionedUserText, - newQuoteMentionedUserText, + newQuoteMentionedUserText ) { // === Profile Element === const profileElement = elNew("img", { @@ -239,30 +239,35 @@ async function constructQuotedTweet( var oldStyleReplyTo = null; if (!!newQuoteMentionedUserText && !vars.useOldStyleReply) { - newQuoteMentionedUserText = interleave( newQuoteMentionedUserText, LOC.replying_to_comma.message ); - if (newQuoteMentionedUserText.length >=5) { + if (newQuoteMentionedUserText.length >= 5) { const arrLength = newQuoteMentionedUserText.length; - newQuoteMentionedUserText[arrLength - 2] = - LOC.replying_to_and.message; + newQuoteMentionedUserText[arrLength - 2] = LOC.replying_to_and.message; } + var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); + textFragments = interleave( + textFragments, + newQuoteMentionedUserText + ).flat(Infinity); + oldStyleReplyTo = elNew( "span", { className: "tweet-reply-to tweet-quote-reply-to" }, - [ - LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - quoteMentionedUserText - .trim() - .replaceAll(` `, LOC.replying_to_comma.message) - .replace(LOC.replying_to_comma.message, LOC.replying_to_and.message) - ), - ] + textFragments, + // [ + // LOC.replying_to_user.message.replace( + // "$SCREEN_NAME$", + // quoteMentionedUserText + // .trim() + // .replaceAll(` `, LOC.replying_to_comma.message) + // .replace(LOC.replying_to_comma.message, LOC.replying_to_and.message) + // ), + // ] ); } @@ -410,22 +415,127 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { const tweetDateObject = new Date(t.created_at); const tweetTimestamp = tweetDateObject.getTime(); - const tweetTopConst = elNew("template", {}, [ - _tweetTopConst, + const avatarImage = elNew( + "a", + { className: "tweet-avatar-link", href: `/${t.user.screen_name}` }, + [ + elNew("img", { + onerror: `this.src = '${defaultProfilePicture}'`, + src: profilePicture, + alt: t.user.name, + className: "tweet-avatar", + width: 48, + height: 48, + }), + ] + ); + + // tweetHeaderName + let tweetHeaderClass = ["tweet-header-name"]; + + if (options.mainTweet) { + tweetHeaderClass.push("tweet-header-name-main"); + } + + if (t.user.verified || t.user.verified_type) { + tweetHeaderClass.push("user-verified"); + } else if (t.user.id_str === "1708130407663759360") { + tweetHeaderClass.push("user-verified", "user-verified-dimden"); // Add both classes + } + + if (t.user.protected) { + tweetHeaderClass.push("user-protected"); + } + + if (t.user.verified_type === "Government") { + tweetHeaderClass.push("user-verified-gray"); + } else if (t.user.verified_type === "Business") { + tweetHeaderClass.push("user-verified-yellow"); + } else if (t.user.verified_type === "Blue") { + tweetHeaderClass.push("user-verified-blue"); + } + + const screenUsername = elNew( + "a", + { + className: [ + "tweet-header-info", + options.mainTweet ? "tweet-header-info-main" : "", + ], + href: `/${t.user.screen_name}`, + }, + // The Screen / Display name + [ + elNew( + "b", + { + title: + t.user.id_str === "1708130407663759360" + ? "Old Twitter Layout extension developer" + : "", + className: tweetHeaderClass, + }, + [escapeHTML(t.user.name)] + ), + " ", + // @Handle + elNew("span", { className: "tweet-header-handle" }, [ + "@" + escapeHTML(t.user.screen_name), + ]), + ] + ); + + let tweetHeaderBlock = [ + // The Screen & username block. + screenUsername, + " ", + // Tweet Time Element elNew( "a", - { className: "tweet-avatar-link", href: `/${t.user.screen_name}` }, - [ - elNew("img", { - onerror: `this.src = '${defaultProfilePicture}'`, - src: profilePicture, - alt: t.user.name, - className: "tweet-avatar", - width: 48, - height: 48, - }), - ] + { + className: "tweet-time", + hidden: options.mainTweet ? true : false, + dataset: { + timestamp: tweetTimestamp, + }, + title: tweetDateObject.toLocaleString(), + href: `/${t.user.screen_name}/status/${t.id_str}`, + }, + [timeElapsed(tweetTimestamp)] ), + ]; + // If it's the main tweet, add a follow button. + if (options.mainTweet && t.user.id_str !== user.id_str) { + tweetHeaderBlock.push( + elNew( + "button", + { + className: [ + "tweet-button", + "nice-button", + "tweet-header-follow", + t.user.following ? "following" : "follow", + ], + }, + [t.user.following ? LOC.following_btn.message : LOC.follow.message] + ) + ); + } + // translate icon for timelines. + if (!options.mainTweet && !tweetConstructorArgs.isMatchingLanguage) { + tweetHeaderBlock.push( + elNew("span", { className: ["tweet-translate-after", "tweet-button"] }, [ + `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && + innerWidth > 650 + ? LOC.view_translation.message + : null, + ]) + ); + } + + const tweetTopConst = elNew("template", {}, [ + _tweetTopConst, + avatarImage, elNew( "div", { @@ -434,101 +544,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { options.mainTweet ? "tweet-header-main" : "", ], }, - [ - // The Screen & username block. - elNew( - "a", - { - className: [ - "tweet-header-info", - options.mainTweet ? "tweet-header-info-main" : "", - ], - href: `/${t.user.screen_name}`, - }, - // The Screen / Display name - [ - elNew( - "b", - { - title: - t.user.id_str === "1708130407663759360" - ? "Old Twitter Layout extension developer" - : "", - className: [ - "tweet-header-name", - options.mainTweet ? "tweet-header-name-main" : "", - t.user.verified || t.user.verified_type - ? "user-verified" - : t.user.id_str === "1708130407663759360" - ? "user-verified user-verified-dimden" - : "", - t.user.protected ? "user-protected" : "", - t.user.verified_type === "Government" - ? "user-verified-gray" - : t.user.verified_type === "Business" - ? "user-verified-yellow" - : t.user.verified_type === "Blue" - ? "user-verified-blue" - : "", - ], - }, - [escapeHTML(t.user.name)] - ), - " ", - // @Handle - elNew("span", { className: "tweet-header-handle" }, [ - "@" + escapeHTML(t.user.screen_name), - ]), - ] - ), - " ", - // Tweet Time Element - elNew( - "a", - { - className: "tweet-time", - hidden: options.mainTweet ? true : false, - dataset: { - timestamp: tweetTimestamp, - }, - title: tweetDateObject.toLocaleString(), - href: `/${t.user.screen_name}/status/${t.id_str}`, - }, - [timeElapsed(tweetTimestamp)] - ), - // If it's the main tweet, add a follow button. - options.mainTweet && t.user.id_str !== user.id_str - ? elNew( - "button", - { - className: [ - "tweet-button", - "nice-button", - "tweet-header-follow", - t.user.following ? "following" : "follow", - ], - }, - [ - t.user.following - ? LOC.following_btn.message - : LOC.follow.message, - ] - ) - : null, - // Add a translate icon for timelines. - !options.mainTweet && !tweetConstructorArgs.isMatchingLanguage - ? elNew( - "span", - { className: ["tweet-translate-after", "tweet-button"] }, - [ - `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && - innerWidth > 650 - ? LOC.view_translation.message - : null, - ] - ) - : null, - ] + tweetHeaderBlock ), ]); @@ -549,7 +565,6 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { if (doMentionText) { // Taken from StackOverflow once again. - tweetConstructorArgs.mentionedUserTextArray = interleave( tweetConstructorArgs.mentionedUserTextArray, LOC.replying_to_comma.message @@ -566,8 +581,15 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { } // XXX: This is actually missing a `LOC.replying_to_user.message`. // Will need to figure out how to replace it. + + var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); + textFragments = interleave( + textFragments, + tweetConstructorArgs.mentionedUserTextArray + ).flat(Infinity); + mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ - elNew("span", {}, tweetConstructorArgs.mentionedUserTextArray), + elNew("span", {}, textFragments), ]); } @@ -698,9 +720,14 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { var resolutionNodes = tweetConstructorArgs.videos[0].video_info.variants .filter((v) => v.bitrate) .map((v) => - elNew("span", { class: [], dataset: { url: v.url } }, [ - v.url.match(/\/(\d+)x/)[1] + "p", - ]) + elNew( + "span", + { + class: ["tweet-video-quality", "tweet-button"], + dataset: { url: v.url }, + }, + [v.url.match(/\/(\d+)x/)[1] + "p"] + ) ); videoControls = elNew("div", { class: "tweet-media-controls" }, [ viewNode, From 2001d74e224cda795679c50708232a165bb311e9 Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 15:27:34 +0800 Subject: [PATCH 16/29] JS Shenanigans --- scripts/tweetConstructor.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 778f8953..318a652e 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -556,7 +556,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // mentionText const doMentionText = // tweetConstructorArgs.mentionedUserText !== `` && - !!tweetConstructorArgs.mentionedUserTextArray && + tweetConstructorArgs.mentionedUserTextArray.length > 0 && !options.threadContinuation && !options.noTop && !location.pathname.includes("/status/") && @@ -564,7 +564,6 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { var mentioned_node = null; if (doMentionText) { // Taken from StackOverflow once again. - tweetConstructorArgs.mentionedUserTextArray = interleave( tweetConstructorArgs.mentionedUserTextArray, LOC.replying_to_comma.message @@ -677,10 +676,12 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // XXX: htmlToNodes needs to always be called, else .content will be null/undefined. [htmlToNodes(svgPlayIcon).content] ); - console.log(videoOverlay, videoOverlay.outerHTML); } // Render media nodes var mediaNodes = renderMultiMediaNodes(t); + if (mediaNodes.length == 0) { + console.log("Cannot render mediaNodes?",t) + } //
GIF
var gifControl = @@ -1581,7 +1582,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { return [ tweet_top, mentioned_node - ? mentioned_node.innerHTML + ? mentioned_node.outerHTML : `` + body_text + translate_text + From 15d74a89f46d8584965588c49b532aaf3dc40e5a Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 17:15:45 +0800 Subject: [PATCH 17/29] Templatify interactions --- scripts/tdeb.js | 13 +- scripts/tweetConstructor.js | 999 ++++++++++++++++++++---------------- 2 files changed, 551 insertions(+), 461 deletions(-) diff --git a/scripts/tdeb.js b/scripts/tdeb.js index f3c99fd3..0ab35c3c 100644 --- a/scripts/tdeb.js +++ b/scripts/tdeb.js @@ -1,4 +1,3 @@ - // Shim for isArray check. (cc. https://stackoverflow.com/a/20956445) if (typeof Array.isArray === "undefined") { Array.isArray = function (obj) { @@ -7,7 +6,7 @@ if (typeof Array.isArray === "undefined") { } /** - * + * * @param {string} string html string to convert to a html content * @returns template element. * @note This code does not sanitize **any** html and is inheriently unsafe. @@ -19,10 +18,10 @@ function htmlToNodes(string) { } /** - * + * * @param {any[]} arr An array support flatMap and slice. * @param {any} x the item to interleave each item into. - * @returns + * @returns */ function interleave(arr, x) { return arr.flatMap((e) => [e, x]).slice(0, -1); @@ -102,11 +101,9 @@ const elNew = (tag, prop, children = []) => { } } }); - } - else { - element.appendChild(children) + } else { + element.appendChild(document.createTextNode(children)); } return element; }; - diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 318a652e..25f6e3de 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -9,6 +9,51 @@ const svgPlayIcon = `GIF
var gifControl = @@ -819,17 +836,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { t.withheld_in_countries.includes("XY"))) || t.withheld_scope ) { - country_restrictions = elNew("div", { class: ["tweet-warning"] }, [ - "This Tweet has been withheld in response to a report from the copyright holder.", - elNew( - "a", - { - href: "https://help.twitter.com/en/rules-and-policies/copyright-policy", - target: "_blank", - }, - ["Learn more."] - ), - ]); + country_restrictions = country_restriction_node; } // const country_restrictions = // (t.withheld_in_countries && @@ -1015,7 +1022,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ] ).outerHTML; - // solve additional retweet actions + // solve additional classes var retweetClasses = ["tweet-button", "tweet-interact-retweet"]; if (t.retweeted) { retweetClasses.push("tweet-interact-retweeted"); @@ -1031,6 +1038,11 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { if (t.favorited) { likeClasses.push("tweet-interact-favorited"); } + + var bookmarkClasses = ["tweet-button", "tweet-interact-bookmark"]; + if (t.favorited) { + bookmarkClasses.push("tweet-interact-bookmarked"); + } // dropdown for retweet actions var retweetDropdownArray = [ elNew( @@ -1052,6 +1064,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ), ]; if (options.mainTweet) { + // If it's the main tweet, add quotes and retweeters retweetDropdownArray.push( elNew( "span", @@ -1072,8 +1085,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ); } - // Tweet Interactions buttons - elNew("div", { class: ["tweet-interact"] }, [ + var interactionArray = [ // reply elNew( "span", @@ -1092,8 +1104,10 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { elNew( "span", { - class: ["tweet-button", "tweet-interact-favorite"], - title: `${LOC.reply_btn.message}${!vars.disableHotkeys ? " (L)" : ""}`, + class: retweetClasses, + title: `${LOC.retweet_btn.message}${ + !vars.disableHotkeys ? " (L)" : "" + }`, dataset: { val: t.retweet_count }, }, [ @@ -1124,409 +1138,488 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { [ options.mainTweet ? null - : formatLargeNumber(t.retweet_count).replace(/\s/g, ","), + : formatLargeNumber(t.favorite_count).replace(/\s/g, ","), ] ), - ]); - - const tweet_interact = `
- ${ - options.mainTweet - ? "" - : formatLargeNumber(t.reply_count).replace(/\s/g, ",") - } - ${ - options.mainTweet - ? "" - : formatLargeNumber(t.retweet_count).replace(/\s/g, ",") - } - - ${ - options.mainTweet - ? "" - : formatLargeNumber(t.favorite_count).replace(/\s/g, ",") - } - ${ - (vars.showBookmarkCount || options.mainTweet) && - typeof t.bookmark_count !== "undefined" - ? html`${formatLargeNumber(t.bookmark_count).replace( - /\s/g, - "," - )}` - : "" - } - ${ - vars.seeTweetViews && - t.ext && - t.ext.views && - t.ext.views.r && - t.ext.views.r.ok && - t.ext.views.r.ok.count - ? html`${formatLargeNumber( - t.ext.views.r.ok.count - ).replace(/\s/g, ",")}` - : "" - } - - - ${ - options.selfThreadButton && - t.self_thread && - t.self_thread.id_str && - !options.threadContinuation && - !location.pathname.includes("/status/") - ? `${LOC.show_this_thread.message}` - : `` - } - ${ - !options.noTop && - !options.selfThreadButton && - t.in_reply_to_status_id_str && - !( - options.threadContinuation || - (options.selfThreadContinuation && - t.self_thread && - t.self_thread.id_str) - ) && - !location.pathname.includes("/status/") - ? `${LOC.show_this_thread.message}` - : `` - } -
`; + ) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { class: ["tweet-interact-more-menu-hide"] }, + t.moderated ? LOC.unhide_tweet.message : LOC.hide_tweet.message + ) + ); + } + if (t.hasModeratedReplies) { + dropDownMoreInteractionsArray.push( + elNew("span", { class: ["tweet-interact-more-menu-hidden"] }, [ + elNew( + "a", + { + target: "_blank", + href: `/${t.user.screen_name}/status/${t.id_str}/hidden?newtwitter=true`, + }, + LOC.see_hidden_replies.message + ), + ]) + ); + } + dropDownMoreInteractionsArray.push(elNew("hr")); + if (t.user.id_str !== user.id_str) { + if (!options.mainTweet) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-follow"], + hidden: t.user.blocking ? true : false, + }, + [ + t.user.following + ? tweetConstructorArgs.unfollowUserText + : tweetConstructorArgs.followUserText, + ] + ) + ); + } + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-block"], + }, + [ + t.user.blocking + ? tweetConstructorArgs.unblockUserText + : tweetConstructorArgs.blockUserText, + ] + ) + ); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-mute-user"], + }, + [ + t.user.muting + ? LOC.unmute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + : LOC.mute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ), + ] + ) + ); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-lists-action"], + }, + LOC.from_list.message + ) + ); + } + if (!location.pathname.startsWith("/i/bookmarks")) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-bookmark"], + }, + t.bookmarked ? LOC.remove_bookmark.message : LOC.bookmark_tweet.message + ) + ); + } + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-mute"], + }, + t.conversation_muted ? LOC.unmute_convo.message : LOC.mute_convo.message + ) + ); + dropDownMoreInteractionsArray.push(elNew("hr")); + if (t.feedback) { + dropDownMoreInteractionsArray.push( + ...t.feedback.map((f, i) => { + elNew( + "span", + { + class: ["tweet-interact-more-menu-feedback"], + dataset: { index: i }, + }, + [f.prompt ? f.prompt : LOC.topic_not_interested.message] + ); + }) + ); + } + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-refresh"], + }, + LOC.refresh_tweet.message + ) + ); + if ( + t.extended_entities && + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "animated_gif" + ) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-download-gif"], + dataset: { gifno: "1" }, + }, + LOC.download_gif.message + ) + ); + } + if (t.extended_entities && t.extended_entities.media.length > 1) { + dropDownMoreInteractionsArray.push( + ...t.extended_entities.media + .filter((m) => m.type === "animated_gif") + .map((m, i) => { + elNew( + "span", + { + class: ["tweet-interact-more-menu-download-gif"], + dataset: { gifno: i + 1 }, + }, + `${LOC.download_gif.message} (#${i + 1})` + ); + }) + ); + } + if (t.extended_entities && t.extended_entities.media.length > 0) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-download"], + }, + `${LOC.download_media.message}` + ) + ); + } + if (vars.developerMode) { + dropDownMoreInteractionsArray.push( + ...[ + elNew( + "span", + { + class: ["tweet-interact-more-menu-copy-user-id"], + }, + LOC.copy_user_id.message + ), + elNew( + "span", + { + class: ["tweet-interact-more-menu-copy-tweet-id"], + }, + LOC.copy_tweet_id.message + ), + elNew( + "span", + { + class: ["tweet-interact-more-menu-log"], + }, + "Log tweet object" + ), + ] + ); + } - const tweet_edit = ` - `; + interactionArray.push( + elNew( + "div", + { class: ["tweet-interact-more-menu", "dropdown-menu"], hidden: true }, + dropDownMoreInteractionsArray + ) + ); + if ( + options.selfThreadButton && + t.self_thread && + t.self_thread.id_str && + !options.threadContinuation && + !location.pathname.includes("/status/") + ) { + interactionArray.push( + elNew( + "a", + { + class: ["tweet-self-thread-button", "tweet-thread-right"], + target: "_blank", + href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, + }, + LOC.show_this_thread.message + ) + ); + } + if ( + !options.noTop && + !options.selfThreadButton && + t.in_reply_to_status_id_str && + !( + options.threadContinuation || + (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str) + ) && + !location.pathname.includes("/status/") + ) { + elNew( + "a", + { + class: ["tweet-self-thread-button", "tweet-thread-right"], + target: "_blank", + href: `/${t.in_reply_to_screen_name}/status/${t.in_reply_to_status_id_str}`, + }, + LOC.show_this_thread.message + ); + } + + // Tweet Interactions buttons + const interactionSection = elNew( + "div", + { class: ["tweet-interact"] }, + interactionArray + ); + + tweet_interact = interactionSection.outerHTML; + + const tweet_reply_node = elNew( + "div", + { class: ["tweet-edit-section", "tweet-reply"], hidden: true }, + [ + elNew("br"), + elNew( + "b", + { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, + [ + `${LOC.replying_to_tweet.message} `, + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+M" : "", + class: ["tweet-reply-upload"], + }, + [LOC.upload_media_btn.message] + ), + " ", + elNew("span", { class: ["tweet-reply-add-emoji"] }, [ + LOC.emoji_btn.message, + ]), + " ", + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+R" : "", + class: ["tweet-reply-cancel"], + }, + [LOC.cancel_btn.message] + ), + ] + ), + elNew("span", { style: "color:red", class: ["tweet-reply-error"] }, []), + elNew("textarea", { + maxlength: 25000, + class: ["tweet-reply-text"], + placeholder: LOC.reply_example.message, + }), + elNew( + "button", + { title: "CTRL+ENTER", class: ["tweet-reply-button", "nice-button"] }, + [LOC.reply.message] + ), + elNew("br"), + elNew("span", { class: ["tweet-reply-char"] }, [ + localStorage.OTisBlueVerified ? "0/25000" : "0/280", + ]), + elNew("br"), + elNew("div", { + style: "padding-bottom: 10px;", + class: ["tweet-reply-media"], + }), + ] + ); + + const tweet_quote_node = elNew( + "div", + { class: ["tweet-edit-section", "tweet-quote"], hidden: true }, + [ + elNew("br"), + elNew( + "b", + { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, + [ + `${LOC.quote_tweet.message} `, + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+M" : "", + class: ["tweet-quote-upload"], + }, + [LOC.upload_media_btn.message] + ), + " ", + elNew("span", { class: ["tweet-quote-add-emoji"] }, [ + LOC.emoji_btn.message, + ]), + " ", + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+Q" : "", + class: ["tweet-quote-cancel"], + }, + [LOC.cancel_btn.message] + ), + ] + ), + elNew("span", { style: "color:red", class: ["tweet-quote-error"] }, []), + elNew("textarea", { + maxlength: 25000, + class: ["tweet-quote-text"], + placeholder: LOC.quote_example.message, + }), + elNew( + "button", + { title: "CTRL+ENTER", class: ["tweet-quote-button", "nice-button"] }, + [LOC.quote.message] + ), + elNew("br"), + elNew("span", { class: ["tweet-quote-char"] }, [ + localStorage.OTisBlueVerified ? "0/25000" : "0/280", + ]), + elNew("br"), + elNew("div", { + style: "padding-bottom: 10px;", + class: ["tweet-quote-media"], + }), + ] + ); + + const tweet_edit = tweet_reply_node.outerHTML + tweet_quote_node.outerHTML; const replies = `
Date: Mon, 9 Jun 2025 22:16:45 +0800 Subject: [PATCH 18/29] Final-ish templating conversion --- scripts/helpers.js | 33 ++-- scripts/tweetConstructor.js | 296 ++++++++++++++++++------------------ 2 files changed, 171 insertions(+), 158 deletions(-) diff --git a/scripts/helpers.js b/scripts/helpers.js index 05c65767..583ebd4a 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -2602,12 +2602,12 @@ async function appendTweet(t, timelineContainer, options = {}) { t.quoted_status.entities.user_mentions.forEach((user_mention) => { if (user_mention.indices[0] < t.display_text_range[0]) { _newQuoteMentionedUserText.push(`@${user_mention.screen_name}`) - quoteMentionedUserText += `@${user_mention.screen_name}`; + // quoteMentionedUserText += `@${user_mention.screen_name}`; } }); } // construct the markup for the tweet. - const [topContent, actualContent] = await constructTweet( + const [topNodes, actualContent] = await constructTweet( t, { videos: videos, @@ -2616,7 +2616,7 @@ async function appendTweet(t, timelineContainer, options = {}) { full_text: full_text, isQuoteMatchingLanguage: isQuoteMatchingLanguage, newQuoteMentionedUserText: _newQuoteMentionedUserText, - quoteMentionedUserText: quoteMentionedUserText, + // quoteMentionedUserText: quoteMentionedUserText, followUserText: followUserText, unfollowUserText: unfollowUserText, blockUserText: blockUserText, @@ -2624,16 +2624,23 @@ async function appendTweet(t, timelineContainer, options = {}) { }, options ); - // tweet.appendChild() - - tweet.innerHTML = ` - ${topContent} -
- ${actualContent} -
- `; + // XXX: Seems ugly! + Array(...topNodes).forEach(m=>{ + tweet.appendChild(m) + }) + var tweetClasses = ["tweet-body",] + if (options.mainTweet) tweetClasses.push("tweet-body-main") + tweet.appendChild(elNew("article",{class:tweetClasses},actualContent)) + + + // tweet.innerHTML = ` + // ${topNodes} + //
+ // ${actualContent} + //
+ // `; // gifs let gifs = Array.from( tweet.querySelectorAll(".tweet-media-gif, .tweet-media-element-quote-gif") diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 25f6e3de..8080a350 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -30,30 +30,6 @@ const country_restriction_node = elNew("div", { class: ["tweet-warning"] }, [ ), ]); -// const developerModeDebugIteractions = [ -// elNew( -// "span", -// { -// class: ["tweet-interact-more-menu-copy-user-id"], -// }, -// LOC.copy_user_id.message -// ), -// elNew( -// "span", -// { -// class: ["tweet-interact-more-menu-copy-tweet-id"], -// }, -// LOC.copy_tweet_id.message -// ), -// elNew( -// "span", -// { -// class: ["tweet-interact-more-menu-log"], -// }, -// "Log tweet object" -// ), -// ]; - /** * * @param {object} tweetObject The tweet object. @@ -214,8 +190,10 @@ async function constructQuotedTweet( t, isQuoteMatchingLanguage, quoteMentionedUserText, - newQuoteMentionedUserText + newQuoteMentionedUserText, + asNode ) { + asNode = asNode ? asNode : false; // === Profile Element === const profileElement = elNew("img", { src: @@ -283,7 +261,7 @@ async function constructQuotedTweet( ); var oldStyleReplyTo = null; - if (!!newQuoteMentionedUserText && !vars.useOldStyleReply) { + if (newQuoteMentionedUserText.length > 0 && !vars.useOldStyleReply) { newQuoteMentionedUserText = interleave( newQuoteMentionedUserText, LOC.replying_to_comma.message @@ -303,19 +281,10 @@ async function constructQuotedTweet( "span", { className: "tweet-reply-to tweet-quote-reply-to" }, textFragments - // [ - // LOC.replying_to_user.message.replace( - // "$SCREEN_NAME$", - // quoteMentionedUserText - // .trim() - // .replaceAll(` `, LOC.replying_to_comma.message) - // .replace(LOC.replying_to_comma.message, LOC.replying_to_and.message) - // ), - // ] ); } - var textBodySpan = [vars.useOldStyleReply ? quoteMentionedUserText : ""]; + var textBodySpan = [vars.useOldStyleReply ? oldStyleReplyTo : ""]; if (t.quoted_status.full_text) { // XXX: renderTweetBodyHTML returns html. To refactor later. textBodySpan.push(htmlToNodes(await renderTweetBodyHTML(t, true)).content); @@ -425,7 +394,10 @@ async function constructQuotedTweet( : ``, ] ); - return rootAHref.outerHTML; + if (!asNode) return rootAHref.outerHTML; + else { + return rootAHref; + } } /** @@ -667,20 +639,9 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { }, [elNew("span", { class: ["tweet-body-text-span"] }, bodyTextChildren)] ); - // if (body_node.outerHTML != bodyNodeV2.outerHTML) { - // console.log("nodeCompare", body_node, bodyNodeV2); - // } - - // translate icon - const body_text = body_node.outerHTML; - - const translate_text = - !tweetConstructorArgs.isMatchingLanguage && options.mainTweet - ? translate_node.outerHTML - : ""; // render media content elements - var extended_node = null; + var extended_media_nodes = null; if (t.extended_entities && t.extended_entities.media) { // videoOverlay if it's only a single video. const isSingleVideo = @@ -755,35 +716,38 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ]); } // console.log([videoOverlay, ...mediaNodes]); - extended_node = elNew("template", {}, [ + extended_media_nodes = [ elNew("div", { class: ["tweet-media"] }, [videoOverlay, ...mediaNodes]), gifControl, videoControls, elNew("span", { class: "tweet-media-data" }), - ]); + ]; } - var extended_media = ""; - if (extended_node) { - [].forEach.call(extended_node.children, function (el) { - extended_media += el.outerHTML; - }); - } + // var extended_media = ""; + // if (extended_node) { + // [].forEach.call(extended_node.children, function (el) { + // extended_media += el.outerHTML; + // }); + // } // card placeholder if there is a card to placeholder. - const card = t.card ? `
` : ""; + // const card = t.card ? `
` : ""; + const card_node = t.card ? elNew("div", { class: ["tweet-card"] }) : null; // quoted tweet status. - let quoted_tweet = ""; + let quoted_node = ""; if (t.quoted_status) { - quoted_tweet = await constructQuotedTweet( + quoted_node = await constructQuotedTweet( t, tweetConstructorArgs.isQuoteMatchingLanguage, - tweetConstructorArgs.quoteMentionedUserText + tweetConstructorArgs.quoteMentionedUserText, + tweetConstructorArgs.newQuoteMentionedUserText, + true ); } // limited text - var limited = ""; + var limited_node = null; if ( t.limited_actions === "limit_trusted_friends_tweet" && (options.mainTweet || !location.pathname.includes("/status/")) @@ -798,7 +762,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { } else { screen_name = t.user.screen_name; } - limited = elNew("div", { class: ["tweet-limited"] }, [ + limited_node = elNew("div", { class: ["tweet-limited"] }, [ LOC.circle_limited_tweet.message.replace("$SCREEN_NAME$", screen_name), elNew( "a", @@ -808,7 +772,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { }, [LOC.learn_more.message] ), - ]).outerHTML; + ]); // limited = ` //
@@ -825,11 +789,11 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // ); } // tombstoned - const tomb_stone = t.tombstone - ? elNew("div", { class: ["tweet-warning"] }, [t.tombstone]).outerHTML - : ""; + const tombstone_node = t.tombstone + ? elNew("div", { class: ["tweet-warning"] }, [t.tombstone]) + : null; // country restricted text. - var country_restrictions = ""; + var country_restrictions = null; if ( (t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || @@ -847,7 +811,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` // : ""; - var conversation_control = ""; + var conversation_control_node = null; if (t.conversation_control) { const limitedActions = t.limited_actions_text ? t.limited_actions_text @@ -869,10 +833,10 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { } else { replyText = ""; } - conversation_control = elNew("div", { class: "tweet-warning" }, [ + conversation_control_node = elNew("div", { class: "tweet-warning" }, [ limitedActions, replyText, - ]).outerHTML; + ]); } // const conversation_control = t.conversation_control @@ -897,7 +861,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { // }
` // : ""; - var tweet_footer = ""; + var tweet_footer_node = ""; if (options.mainTweet) { var statsArray = []; // Replies @@ -994,11 +958,11 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ) ); - tweet_footer = elNew("div", { class: ["tweet-footer"] }, [ + tweet_footer_node = elNew("div", { class: ["tweet-footer"] }, [ elNew("div", { class: ["tweet-footer-stats"] }, statsArray), - ]).outerHTML; + ]); } - const tweet_date = elNew( + const tweet_date_node = elNew( "a", { hidden: !options.mainTweet ? true : false, @@ -1020,7 +984,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { "  ・ ", t.source ? t.source.split(">")[1].split("<")[0] : "Unknown", ] - ).outerHTML; + ); // solve additional classes var retweetClasses = ["tweet-button", "tweet-interact-retweet"]; @@ -1499,13 +1463,13 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { } // Tweet Interactions buttons - const interactionSection = elNew( + const interactions_node = elNew( "div", { class: ["tweet-interact"] }, interactionArray ); - tweet_interact = interactionSection.outerHTML; + // tweet_interact = ; const tweet_reply_node = elNew( "div", @@ -1620,76 +1584,118 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ); const tweet_edit = tweet_reply_node.outerHTML + tweet_quote_node.outerHTML; - - const replies = `
- ${ - options.selfThreadContinuation && - t.self_thread && - t.self_thread.id_str && - !location.pathname.includes("/status/") - ? `
- - ${LOC.show_this_thread.message} - - -
` - : ` - ${ - location.pathname.includes("/status/") - ? `

` - : "" - } - -
- ` - } -
`; + ? false + : true; + const uriHasStatus = location.pathname.includes("/status/"); + const isStandaloneTweet = + options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str && + !uriHasStatus; + var replyChildren = []; + if (isStandaloneTweet) { + replyChildren.push(elNew("br")); + replyChildren.push( + elNew( + "a", + { + class: ["tweet-self-thread-button"], + target: "_blank", + href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, + }, + LOC.show_this_thread.message + ) + ); + replyChildren.push( + elNew("span", { + class: ["tweet-self-thread-line"], + style: `margin-left: -108px;margin-top: -5px;`, + }) + ); + replyChildren.push( + elNew("div", { + class: ["tweet-self-thread-line-dots"], + style: `margin-left: -120px;margin-top: -3px;`, + }) + ); + } else { + if (uriHasStatus) { + replyChildren.push(elNew("br")); + replyChildren.push(elNew("br")); + } + replyChildren.push( + elNew("span", { + class: ["tweet-self-thread-line"], + style: uriHasStatus ? "margin-top:-10px;" : "", + }) + ); + replyChildren.push( + elNew("div", { + style: uriHasStatus ? "margin-top:-8px;" : "", + class: ["tweet-self-thread-line-dots"], + }) + ); + } + const reply_nodes = elNew( + "div", + { + class: ["tweet-self-thread-div"], + hidden: hideThreadContinuation, + }, + replyChildren + ); return [ - tweet_top, - mentioned_node - ? mentioned_node.outerHTML - : `` + - body_text + - translate_text + - extended_media + - card + - quoted_tweet + - limited + - tomb_stone + - country_restrictions + - conversation_control + - tweet_footer + - tweet_date + - tweet_interact + - tweet_edit + - replies, + tweetTopConst.children, + [ + mentioned_node, + body_node, + !tweetConstructorArgs.isMatchingLanguage && options.mainTweet + ? translate_node + : null, + extended_media_nodes, + card_node, + quoted_node, + limited_node, + tombstone_node, + (t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY"))) || + t.withheld_scope + ? country_restriction_node + : null, + conversation_control_node, + tweet_footer_node, + tweet_date_node, + interactions_node, + // tweet_edit + tweet_reply_node, + tweet_quote_node, + reply_nodes, + ].flat(1), ]; + + // return [ + // tweet_top, + // mentioned_node + // ? mentioned_node.outerHTML + // : `` + + // body_text + + // translate_text + + // extended_media + + // card + + // quoted_tweet + + // limited + + // tomb_stone + + // country_restrictions + + // conversation_control + + // tweet_footer + + // tweet_date + + // interactionSection.outerHTML + + // tweet_edit + + // reply_nodes.outerHTML, + // ]; } From 0c747f209d6fad822e83209a2d0c0c41437e46a4 Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 22:21:29 +0800 Subject: [PATCH 19/29] Minor cleanup --- layouts/header/style.css | 6 ++++++ scripts/tweetConstructor.js | 23 +---------------------- 2 files changed, 7 insertions(+), 22 deletions(-) diff --git a/layouts/header/style.css b/layouts/header/style.css index 57e02795..8bcaebcc 100644 --- a/layouts/header/style.css +++ b/layouts/header/style.css @@ -1689,6 +1689,12 @@ span.tweet-body-text { content: "\f183" } +@media only screen and (min-width: 600px) { + .tweet-interact-more-menu-separate { + display: none; + } +} + .tweet-interact-more-menu-share:before { content: "\f185" } diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 8080a350..4f73b4a0 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1675,27 +1675,6 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { tweet_reply_node, tweet_quote_node, reply_nodes, - ].flat(1), + ].flat(1), // flat required due to `extended_media_nodes` being an array. ]; - - // return [ - // tweet_top, - // mentioned_node - // ? mentioned_node.outerHTML - // : `` + - // body_text + - // translate_text + - // extended_media + - // card + - // quoted_tweet + - // limited + - // tomb_stone + - // country_restrictions + - // conversation_control + - // tweet_footer + - // tweet_date + - // interactionSection.outerHTML + - // tweet_edit + - // reply_nodes.outerHTML, - // ]; } From 2cdcbb21624bb1687eb35e9192b0a37f247f2bfa Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 22:32:21 +0800 Subject: [PATCH 20/29] Migrate link colours to slow link colours. - On request from dimden. Default value for link colours is disabled. --- _locales/en/messages.json | 2 +- layouts/home/script.js | 4 ++-- layouts/search/script.js | 2 +- layouts/settings/index.html | 2 +- layouts/settings/script.js | 8 ++++---- layouts/tweet/script.js | 2 +- scripts/config.js | 8 ++++---- scripts/helpers.js | 20 +++++++++----------- scripts/tweetviewer.js | 4 ++-- 9 files changed, 25 insertions(+), 27 deletions(-) diff --git a/_locales/en/messages.json b/_locales/en/messages.json index 44b36ad7..20af97e6 100644 --- a/_locales/en/messages.json +++ b/_locales/en/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorithmical" }, "timeline_type": { "message": "Timeline type" }, "show_topic_tweets": { "message": "Show topic tweets (algo only)" }, - "show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "Always show custom link colors instead of on hover" }, "enable_twemoji": { "message": "Enable Twemoji" }, "hearts_instead_stars": { "message": "Show hearts (likes) instead of stars (favorites)" }, diff --git a/layouts/home/script.js b/layouts/home/script.js index 64bae236..c8dc5e53 100644 --- a/layouts/home/script.js +++ b/layouts/home/script.js @@ -192,7 +192,7 @@ async function updateTimeline(mode = 'rewrite') { tl = tl.filter(t => !t.socialContext || !t.socialContext.description); } - if(vars.linkColorsInTL) { + if(vars.slowLinkColorsInTL) { let tlUsers = tl.map(t => t.user.id_str).filter(u => !linkColors[u]); let linkData = await getLinkColors(tlUsers); if(linkData) for(let i in linkData) { @@ -293,7 +293,7 @@ async function renderTimeline(options = {}) { } let data = options.data; - if(vars.linkColorsInTL) { + if(vars.slowLinkColorsInTL) { let tlUsers = data.map(t => t.user.id_str).filter(u => !linkColors[u]); let linkData = await getLinkColors(tlUsers); if(linkData) for(let i in linkData) { diff --git a/layouts/search/script.js b/layouts/search/script.js index 1496644c..4602f5d3 100644 --- a/layouts/search/script.js +++ b/layouts/search/script.js @@ -137,7 +137,7 @@ async function renderSearch(c, force = false) { window.scrollTo(0, 0); searchDiv.innerHTML = ''; } - if(vars.linkColorsInTL) { + if(vars.slowLinkColorsInTL) { let tlUsers = []; for(let i in search) { let t = search[i]; diff --git a/layouts/settings/index.html b/layouts/settings/index.html index 17484680..b58255b5 100644 --- a/layouts/settings/index.html +++ b/layouts/settings/index.html @@ -263,7 +263,7 @@

__MSG_old_twitter_look__


- +
diff --git a/layouts/settings/script.js b/layouts/settings/script.js index 79514823..f7226b1a 100644 --- a/layouts/settings/script.js +++ b/layouts/settings/script.js @@ -261,7 +261,7 @@ setTimeout(async () => { ); let linkColor = document.getElementById('link-color'); let heartsNotStars = document.getElementById('hearts-instead-stars'); - let linkColorsInTL = document.getElementById('link-colors-in-tl'); + let slowLinkColorsInTL = document.getElementById('slow-link-colors-in-tl'); let alwaysShowLinkColor = document.getElementById('always-show-link-color'); let enableTwemoji = document.getElementById('enable-twemoji'); let enableHashflags = document.getElementById('enable-hashflags'); @@ -522,9 +522,9 @@ setTimeout(async () => { disableProfileCustomizations: disableProfileCustomizations.checked }, () => { }); }); - linkColorsInTL.addEventListener('change', () => { + slowLinkColorsInTL.addEventListener('change', () => { chrome.storage.sync.set({ - linkColorsInTL: linkColorsInTL.checked + slowLinkColorsInTL: slowLinkColorsInTL.checked }, () => { }); }); alwaysShowLinkColor.addEventListener('change', () => { @@ -1018,7 +1018,7 @@ setTimeout(async () => { root.style.setProperty('--icon-font', `"edgeicons", "RosettaIcons"`); } heartsNotStars.checked = !!vars.heartsNotStars; - linkColorsInTL.checked = !!vars.linkColorsInTL; + slowLinkColorsInTL.checked = !!vars.slowLinkColorsInTL; alwaysShowLinkColor.checked = !!vars.alwaysShowLinkColor; enableTwemoji.checked = !!vars.enableTwemoji; enableHashflags.checked = !!vars.enableHashflags; diff --git a/layouts/tweet/script.js b/layouts/tweet/script.js index 5fd45d1a..63763562 100644 --- a/layouts/tweet/script.js +++ b/layouts/tweet/script.js @@ -147,7 +147,7 @@ async function updateReplies(id, c) { users[u] = tl.users[u]; }; - if(vars.linkColorsInTL) { + if(vars.slowLinkColorsInTL) { let tlUsers = []; for(let i in tl.list) { let t = tl.list[i]; diff --git a/scripts/config.js b/scripts/config.js index db48519b..812f4213 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -64,7 +64,7 @@ async function loadVars() { "linkColor", "font", "heartsNotStars", - "linkColorsInTL", + "slowLinkColorsInTL", "alwaysShowLinkColor", "enableTwemoji", "chronologicalTL", @@ -144,11 +144,11 @@ async function loadVars() { ], (data) => { // default variables - if (typeof data.linkColorsInTL !== "boolean") { - data.linkColorsInTL = true; + if (typeof data.slowLinkColorsInTL !== "boolean") { + data.slowLinkColorsInTL = false; chrome.storage.sync.set( { - linkColorsInTL: true, + slowLinkColorsInTL: false, }, () => {} ); diff --git a/scripts/helpers.js b/scripts/helpers.js index 583ebd4a..6db87678 100644 --- a/scripts/helpers.js +++ b/scripts/helpers.js @@ -1328,7 +1328,7 @@ function makeSeeableColor(color, bg_color = getBackgroundColor()) { return rgb2hex(...hslToRgb(...hsl)); } -const getLinkColors = async (ids) => { +async function getLinkColors(ids) { if (typeof ids === "string") ids = ids.split(","); ids = [...new Set(ids)]; const colours = await Promise.all([ @@ -1348,13 +1348,11 @@ const getLinkColors = async (ids) => { if (linkColors[id]) fetched.push({ id, color: linkColors[id] }); } } - if ( - toFetch.length === 0 || + if (toFetch.length === 0 || (window.navigator && navigator.connection && navigator.connection.type === "cellular" && - !vars.disableDataSaver) - ) { + !vars.disableDataSaver)) { return resolve(fetched); } @@ -1364,7 +1362,7 @@ const getLinkColors = async (ids) => { let t = setTimeout(() => controller.abort(), 1000); let res = await fetch( "https://dimden.dev/services/twitter_link_colors/v2/get_multiple/" + - toFetch.join(","), + toFetch.join(","), { signal: controller.signal, } @@ -1385,9 +1383,9 @@ const getLinkColors = async (ids) => { } let keys = Object.keys(linkColors); if (keys.length > 20000) { - chrome.storage.local.set({ linkColors: {} }, () => {}); + chrome.storage.local.set({ linkColors: {} }, () => { }); } else { - chrome.storage.local.set({ linkColors }, () => {}); + chrome.storage.local.set({ linkColors }, () => { }); } return resolve(fetched); } catch (e) { @@ -1430,7 +1428,7 @@ const getLinkColors = async (ids) => { for (let id of hasColourIds) { fetched.push({ id, color: legacyLinkColors[id] }); } - chrome.storage.local.set({ legacyLinkColors }, () => {}); + chrome.storage.local.set({ legacyLinkColors }, () => { }); resolve(fetched); }); }), @@ -1447,7 +1445,7 @@ const getLinkColors = async (ids) => { } } return linkColors; -}; +} function getOtAuthToken(cache = true) { return new Promise((resolve, reject) => { @@ -2430,7 +2428,7 @@ async function appendTweet(t, timelineContainer, options = {}) { tweet.classList.add("tweet-self-thread-continuation"); if (options.noTop) tweet.classList.add("tweet-no-top"); - if (vars.linkColorsInTL && typeof linkColors !== "undefined") { + if (vars.slowLinkColorsInTL && typeof linkColors !== "undefined") { if (linkColors[t.user.id_str]) { let sc = makeSeeableColor(linkColors[t.user.id_str]); tweet.style.setProperty("--link-color", sc); diff --git a/scripts/tweetviewer.js b/scripts/tweetviewer.js index cea65e03..9238706a 100644 --- a/scripts/tweetviewer.js +++ b/scripts/tweetviewer.js @@ -239,7 +239,7 @@ class TweetViewer { return (this.cursor = undefined); } - if (vars.linkColorsInTL) { + if (vars.slowLinkColorsInTL) { let tlUsers = []; for (let i in tl.list) { let t = tl.list[i]; @@ -1198,7 +1198,7 @@ class TweetViewer { if (options.threadContinuation) tweet.classList.add("tweet-self-thread-continuation"); if (options.noTop) tweet.classList.add("tweet-no-top"); - if (vars.linkColorsInTL) { + if (vars.slowLinkColorsInTL) { if (this.linkColors[t.user.id_str]) { let sc = makeSeeableColor(this.linkColors[t.user.id_str]); tweet.style.setProperty("--link-color", sc); From 11af2adeae652b7ca79be6a904daa40ac7fd5737 Mon Sep 17 00:00:00 2001 From: Shinon Date: Mon, 9 Jun 2025 23:26:50 +0800 Subject: [PATCH 21/29] Added date & time formatters Reading into a bit of time & date formatting, it seems that it can get a bit slow when calling toLocaleString. If the browser is old enough, Intl is not available. As such, we fallback to using toLocaleString. --- scripts/tweetConstructor.js | 116 ++++++++++++++++++++++++------------ 1 file changed, 77 insertions(+), 39 deletions(-) diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index 4f73b4a0..3567f0a4 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1,4 +1,21 @@ // svgPlayIcon. + +if (Intl && Intl.DateTimeFormat) { + var tweetTimeFormatter = new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "numeric", + }); + var tweetDateFormatter = new Intl.DateTimeFormat(undefined, { + day: "numeric", + month: "short", + year: "numeric", + }); + var tweetShortishTimeformatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "medium", + }); +} + const svgPlayIcon = ` @@ -11,25 +28,6 @@ const svgPlayIcon = `")[1].split("<")[0] : "Unknown", ] @@ -1583,7 +1603,6 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { ] ); - const tweet_edit = tweet_reply_node.outerHTML + tweet_quote_node.outerHTML; const hideThreadContinuation = options.threadContinuation || (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str) @@ -1648,6 +1667,25 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { replyChildren ); + const translate_node = elNew("div", {}, [ + elNew("br"), + elNew("span", { class: ["tweet-button", "tweet-translate"] }, [ + LOC.view_translation.message, + ]), + ]); + + const country_restriction_node = elNew("div", { class: ["tweet-warning"] }, [ + "This Tweet has been withheld in response to a report from the copyright holder.", + elNew( + "a", + { + href: "https://help.twitter.com/en/rules-and-policies/copyright-policy", + target: "_blank", + }, + ["Learn more."] + ), + ]); + return [ tweetTopConst.children, [ From bb61bcdd6f1cc1da2dc514320fd18edece729776 Mon Sep 17 00:00:00 2001 From: Shinon Date: Tue, 10 Jun 2025 13:05:45 +0800 Subject: [PATCH 22/29] Revert some transition changes. I think these got caught up in my experiment. It "works" but I don't exactly like how it's handled --- layouts/home/index.html | 2 +- layouts/home/script.js | 4 ++-- layouts/home/style.css | 12 ------------ 3 files changed, 3 insertions(+), 15 deletions(-) diff --git a/layouts/home/index.html b/layouts/home/index.html index 4ad97f43..8168c339 100644 --- a/layouts/home/index.html +++ b/layouts/home/index.html @@ -86,7 +86,7 @@

__MSG_trends__


-
`; - let followButton = userElement.querySelector(".user-item-btn"); - followButton.addEventListener("click", async () => { - if (followButton.classList.contains("following")) { - try { - await API.user.unfollow(u.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - followButton.classList.remove("following"); - followButton.classList.add("follow"); - followButton.innerText = LOC.follow.message; - } else { - try { - await API.user.follow(u.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - followButton.classList.remove("follow"); - followButton.classList.add("following"); - followButton.innerText = LOC.following_btn.message; - } - }); + let followButton = userElement.querySelector(".user-item-btn"); + followButton.addEventListener("click", async () => { + if (followButton.classList.contains("following")) { + try { + await API.user.unfollow(u.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + followButton.classList.remove("following"); + followButton.classList.add("follow"); + followButton.innerText = LOC.follow.message; + } else { + try { + await API.user.follow(u.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + followButton.classList.remove("follow"); + followButton.classList.add("following"); + followButton.innerText = LOC.following_btn.message; + } + }); - container.appendChild(userElement); - if (vars.enableTwemoji) twemoji.parse(userElement); + container.appendChild(userElement); + if (vars.enableTwemoji) twemoji.parse(userElement); } let lastTweetErrorDate = 0; const mediaClasses = [ - undefined, - "tweet-media-element-one", - "tweet-media-element-two", - "tweet-media-element-three", - "tweet-media-element-two", + undefined, + "tweet-media-element-one", + "tweet-media-element-two", + "tweet-media-element-three", + "tweet-media-element-two", ]; function calculateSize(x, y, max_x, max_y) { - let ratio = x / y; - let iw = innerWidth; - if (iw < 590) max_x = iw - 120; - if (x > max_x) { - x = max_x; - y = x / ratio; - } - if (y > max_y) { - y = max_y; - x = y * ratio; - } - return [parseInt(x), parseInt(y)]; + let ratio = x / y; + let iw = innerWidth; + if (iw < 590) max_x = iw - 120; + if (x > max_x) { + x = max_x; + y = x / ratio; + } + if (y > max_y) { + y = max_y; + x = y * ratio; + } + return [parseInt(x), parseInt(y)]; } const sizeFunctions = [ - undefined, - (w, h) => calculateSize(w, h, 450, 500), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => - innerWidth < 590 - ? calculateSize(w, h, 225, 400) - : calculateSize(w, h, 150, 250), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), - (w, h) => calculateSize(w, h, 225, 400), + undefined, + (w, h) => calculateSize(w, h, 450, 500), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => + innerWidth < 590 + ? calculateSize(w, h, 225, 400) + : calculateSize(w, h, 150, 250), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), + (w, h) => calculateSize(w, h, 225, 400), ]; const quoteSizeFunctions = [ - undefined, - (w, h) => calculateSize(w, h, 400, 400), - (w, h) => calculateSize(w, h, 200, 400), - (w, h) => calculateSize(w, h, 125, 200), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), - (w, h) => calculateSize(w, h, 100, 150), + undefined, + (w, h) => calculateSize(w, h, 400, 400), + (w, h) => calculateSize(w, h, 200, 400), + (w, h) => calculateSize(w, h, 125, 200), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), + (w, h) => calculateSize(w, h, 100, 150), ]; async function appendTweet(t, timelineContainer, options = {}) { - if (typeof t !== "object") { - console.error("Tweet is undefined", t, timelineContainer, options); - return; - } - if (typeof t.user !== "object") { - console.error("Tweet user is undefined", t, timelineContainer, options); - return; - } - try { - if (typeof seenReplies !== "undefined" && !options.ignoreSeen) { - if (seenReplies.includes(t.id_str)) return; - seenReplies.push(t.id_str); + if (typeof t !== "object") { + console.error("Tweet is undefined", t, timelineContainer, options); + return; } - if (typeof seenThreads !== "undefined" && !options.ignoreSeen) { - if (seenThreads.includes(t.id_str)) return; + if (typeof t.user !== "object") { + console.error("Tweet user is undefined", t, timelineContainer, options); + return; } - if (t.socialContext) { - options.top = {}; - if (t.socialContext.contextType === "Like") { - if (!vars.heartsNotStars) { - LOC.replacer_liked_to_favorited.message.split("|").forEach((el) => { - t.socialContext.text = t.socialContext.text.replace( - new RegExp(el.split("->")[0], "g"), - el.split("->")[1] - ); - }); + try { + if (typeof seenReplies !== "undefined" && !options.ignoreSeen) { + if (seenReplies.includes(t.id_str)) return; + seenReplies.push(t.id_str); } - options.top.text = `<${ - t.socialContext.landingUrl.url.split("=")[1] - ? `a href="/i/user/${t.socialContext.landingUrl.url.split("=")[1]}"` - : "span" - }>${t.socialContext.text}`; - if (vars.heartsNotStars) { - options.top.icon = "\uf015"; - options.top.color = "rgb(249, 24, 128)"; - } else { - options.top.icon = "\uf001"; - options.top.color = "#ffac33"; + if (typeof seenThreads !== "undefined" && !options.ignoreSeen) { + if (seenThreads.includes(t.id_str)) return; + } + if (t.socialContext) { + options.top = {}; + if (t.socialContext.contextType === "Like") { + if (!vars.heartsNotStars) { + LOC.replacer_liked_to_favorited.message + .split("|") + .forEach((el) => { + t.socialContext.text = t.socialContext.text.replace( + new RegExp(el.split("->")[0], "g"), + el.split("->")[1] + ); + }); + } + options.top.text = `<${ + t.socialContext.landingUrl.url.split("=")[1] + ? `a href="/i/user/${ + t.socialContext.landingUrl.url.split("=")[1] + }"` + : "span" + }>${t.socialContext.text}`; + if (vars.heartsNotStars) { + options.top.icon = "\uf015"; + options.top.color = "rgb(249, 24, 128)"; + } else { + options.top.icon = "\uf001"; + options.top.color = "#ffac33"; + } + } else if (t.socialContext.contextType === "Follow") { + options.top.text = t.socialContext.text; + options.top.icon = "\uf002"; + options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; + } else if (t.socialContext.contextType === "Conversation") { + options.top.text = t.socialContext.text; + options.top.icon = "\uf005"; + options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; + } else if (t.socialContext.contextType === "Sparkle") { + options.top.text = t.socialContext.text; + options.top.icon = "\uf011"; + options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; + } else if (t.socialContext.topic_id) { + options.top.text = `${t.socialContext.name}`; + options.top.icon = "\uf008"; + options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; + } else { + options.top = null; //otherwise, undefined values in options.top + console.log(t.socialContext); + } } - } else if (t.socialContext.contextType === "Follow") { - options.top.text = t.socialContext.text; - options.top.icon = "\uf002"; - options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; - } else if (t.socialContext.contextType === "Conversation") { - options.top.text = t.socialContext.text; - options.top.icon = "\uf005"; - options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; - } else if (t.socialContext.contextType === "Sparkle") { - options.top.text = t.socialContext.text; - options.top.icon = "\uf011"; - options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; - } else if (t.socialContext.topic_id) { - options.top.text = `${t.socialContext.name}`; - options.top.icon = "\uf008"; - options.top.color = isDarkModeEnabled ? "#7e5eff" : "#3300FF"; - } else { - options.top = null; //otherwise, undefined values in options.top - console.log(t.socialContext); - } - } - - // verification - if (t.user.ext_verified_type) { - t.user.verified_type = t.user.ext_verified_type; - t.user.verified = true; - } - if ( - vars.twitterBlueCheckmarks && - t.user.ext && - t.user.ext.isBlueVerified && - t.user.ext.isBlueVerified.r && - t.user.ext.isBlueVerified.r.ok - ) { - t.user.verified_type = "Blue"; - t.user.verified = true; - } - if ( - t.user && - t.user.ext && - t.user.ext.verifiedType && - t.user.ext.verifiedType.r && - t.user.ext.verifiedType.r.ok - ) { - t.user.verified_type = t.user.ext.verifiedType.r.ok; - t.user.verified = true; - } - if (!vars.twitterBlueCheckmarks && t.user.verified_type === "Blue") { - delete t.user.verified_type; - t.user.verified = false; - } - if ( - !vars.twitterBlueCheckmarks && - t.quoted_status && - t.quoted_status.user.verified_type === "Blue" - ) { - delete t.quoted_status.user.verified_type; - t.quoted_status.user.verified = false; - } - if (t.quoted_status) { - if ( - t.quoted_status.user.verified_type === "Blue" && - !vars.twitterBlueCheckmarks - ) { - delete t.quoted_status.user.verified_type; - t.quoted_status.user.verified = false; - } - } - - if (typeof tweets !== "undefined") tweets.push(["tweet", t, options]); - const tweet = document.createElement("div"); - tweet.tweet = t; - t.element = tweet; - t.options = options; - if ( - !options.mainTweet && - typeof mainTweetLikers !== "undefined" && - !location.pathname.includes("retweets/with_comments") && - !document.querySelector(".modal") - ) { - tweet.addEventListener("click", async (e) => { + // verification + if (t.user.ext_verified_type) { + t.user.verified_type = t.user.ext_verified_type; + t.user.verified = true; + } if ( - !e.target.closest(".tweet-button") && - !e.target.closest(".tweet-body-text-span") && - !e.target.closest(".tweet-edit-section") && - !e.target.closest(".dropdown-menu") && - !e.target.closest(".tweet-media-element") && - !e.target.closest("a") && - !e.target.closest("button") + vars.twitterBlueCheckmarks && + t.user.ext && + t.user.ext.isBlueVerified && + t.user.ext.isBlueVerified.r && + t.user.ext.isBlueVerified.r.ok ) { - document.getElementById("loading-box").hidden = false; - savePageData(); - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}` - ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let restored = await restorePageData(); - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (subpage === "tweet" && !restored) { - updateReplies(id); - } else if (subpage === "likes") { - updateLikes(id); - } else if (subpage === "retweets") { - updateRetweets(id); - } else if (subpage === "retweets_with_comments") { - updateRetweetsWithComments(id); - } - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; + t.user.verified_type = "Blue"; + t.user.verified = true; } - }); - } else { - if (!options.mainTweet) { - tweet.addEventListener("click", (e) => { - if ( - !e.target.closest(".tweet-button") && - !e.target.closest(".tweet-body-text-span") && - !e.target.closest(".tweet-edit-section") && - !e.target.closest(".dropdown-menu") && - !e.target.closest(".tweet-media-element") && - !e.target.closest("a") && - !e.target.closest("button") - ) { - let tweetData = t; - if (tweetData.retweeted_status) - tweetData = tweetData.retweeted_status; - tweet.classList.add("tweet-preload"); - let selection = window.getSelection(); + if ( + t.user && + t.user.ext && + t.user.ext.verifiedType && + t.user.ext.verifiedType.r && + t.user.ext.verifiedType.r.ok + ) { + t.user.verified_type = t.user.ext.verifiedType.r.ok; + t.user.verified = true; + } + if (!vars.twitterBlueCheckmarks && t.user.verified_type === "Blue") { + delete t.user.verified_type; + t.user.verified = false; + } + if ( + !vars.twitterBlueCheckmarks && + t.quoted_status && + t.quoted_status.user.verified_type === "Blue" + ) { + delete t.quoted_status.user.verified_type; + t.quoted_status.user.verified = false; + } + if (t.quoted_status) { if ( - selection.toString().length > 0 && - selection.focusNode && - selection.focusNode.closest( - `div.tweet[data-tweet-id="${tweetData.id_str}"]` - ) + t.quoted_status.user.verified_type === "Blue" && + !vars.twitterBlueCheckmarks ) { - return; + delete t.quoted_status.user.verified_type; + t.quoted_status.user.verified = false; } - new TweetViewer(user, tweetData); - } - }); - } - } - tweet.addEventListener("mousedown", (e) => { - if (e.button === 1) { - // tweet-media-element is clickable, since it should open the tweet in a new tab. - if ( - !e.target.closest(".tweet-button") && - !e.target.closest(".tweet-edit-section") && - !e.target.closest(".dropdown-menu") && - !e.target.closest("a") && - !e.target.closest("button") - ) { - e.preventDefault(); - openInNewTab(`/${t.user.screen_name}/status/${t.id_str}`); } - } - }); - tweet.tabIndex = -1; - tweet.className = `tweet ${ - options.mainTweet - ? "tweet-main" - : location.pathname.includes("/status/") - ? "tweet-replying" - : "" - }`.trim(); - tweet.dataset.tweetId = t.id_str; - tweet.dataset.userId = t.user.id_str; - try { - if (!activeTweet) { - tweet.classList.add("tweet-active"); - activeTweet = tweet; - } - } catch (e) {} - - if (t.nonReply) { - tweet.classList.add("tweet-non-reply"); - } - if (t.threadContinuation) { - options.threadContinuation = true; - } - if (t.noTop) { - options.noTop = true; - } - if (options.threadContinuation) - tweet.classList.add("tweet-self-thread-continuation"); - if (options.selfThreadContinuation) - tweet.classList.add("tweet-self-thread-continuation"); - - if (options.noTop) tweet.classList.add("tweet-no-top"); - if (vars.slowLinkColorsInTL && typeof linkColors !== "undefined") { - if (linkColors[t.user.id_str]) { - let sc = makeSeeableColor(linkColors[t.user.id_str]); - tweet.style.setProperty("--link-color", sc); - if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); - } else { + if (typeof tweets !== "undefined") tweets.push(["tweet", t, options]); + const tweet = document.createElement("div"); + tweet.tweet = t; + t.element = tweet; + t.options = options; + if ( - t.user.profile_link_color && - t.user.profile_link_color !== "1DA1F2" + !options.mainTweet && + typeof mainTweetLikers !== "undefined" && + !location.pathname.includes("retweets/with_comments") && + !document.querySelector(".modal") ) { - let sc = makeSeeableColor(t.user.profile_link_color); - tweet.style.setProperty("--link-color", sc); - if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + tweet.addEventListener("click", async (e) => { + if ( + !e.target.closest(".tweet-button") && + !e.target.closest(".tweet-body-text-span") && + !e.target.closest(".tweet-edit-section") && + !e.target.closest(".dropdown-menu") && + !e.target.closest(".tweet-media-element") && + !e.target.closest("a") && + !e.target.closest("button") + ) { + document.getElementById("loading-box").hidden = false; + savePageData(); + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let restored = await restorePageData(); + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (subpage === "tweet" && !restored) { + updateReplies(id); + } else if (subpage === "likes") { + updateLikes(id); + } else if (subpage === "retweets") { + updateRetweets(id); + } else if (subpage === "retweets_with_comments") { + updateRetweetsWithComments(id); + } + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + } + }); + } else { + if (!options.mainTweet) { + tweet.addEventListener("click", (e) => { + if ( + !e.target.closest(".tweet-button") && + !e.target.closest(".tweet-body-text-span") && + !e.target.closest(".tweet-edit-section") && + !e.target.closest(".dropdown-menu") && + !e.target.closest(".tweet-media-element") && + !e.target.closest("a") && + !e.target.closest("button") + ) { + let tweetData = t; + if (tweetData.retweeted_status) + tweetData = tweetData.retweeted_status; + tweet.classList.add("tweet-preload"); + let selection = window.getSelection(); + if ( + selection.toString().length > 0 && + selection.focusNode && + selection.focusNode.closest( + `div.tweet[data-tweet-id="${tweetData.id_str}"]` + ) + ) { + return; + } + new TweetViewer(user, tweetData); + } + }); + } } - } - } - let full_text = t.full_text ? t.full_text : ""; - let tweetLanguage = t.lang; // originally i used i18n api to detect languages simply because i didn't know of t.lang existence - if (!tweetLanguage) { - tweetLanguage = "und"; - } - if (tweetLanguage.includes("-")) { - let [lang, country] = tweetLanguage.split("-"); - tweetLanguage = `${lang}_${country.toUpperCase()}`; - } - let isMatchingLanguage = languageMatches(t.lang); - let isQuoteMatchingLanguage = - !!t.quoted_status && languageMatches(t.quoted_status.lang); - let videos = - t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.filter((m) => m.type === "video"); - if (!videos || videos.length === 0) { - videos = undefined; - } - if (videos) { - for (let v of videos) { - if (!v.video_info) continue; - v.video_info.variants = v.video_info.variants.sort((a, b) => { - if (!b.bitrate) return -1; - return b.bitrate - a.bitrate; + tweet.addEventListener("mousedown", (e) => { + if (e.button === 1) { + // tweet-media-element is clickable, since it should open the tweet in a new tab. + if ( + !e.target.closest(".tweet-button") && + !e.target.closest(".tweet-edit-section") && + !e.target.closest(".dropdown-menu") && + !e.target.closest("a") && + !e.target.closest("button") + ) { + e.preventDefault(); + openInNewTab(`/${t.user.screen_name}/status/${t.id_str}`); + } + } }); - if (typeof vars.savePreferredQuality !== "boolean") { - chrome.storage.sync.set( - { - savePreferredQuality: true, - }, - () => {} - ); - vars.savePreferredQuality = true; + tweet.tabIndex = -1; + tweet.className = `tweet ${ + options.mainTweet + ? "tweet-main" + : location.pathname.includes("/status/") + ? "tweet-replying" + : "" + }`.trim(); + tweet.dataset.tweetId = t.id_str; + tweet.dataset.userId = t.user.id_str; + try { + if (!activeTweet) { + tweet.classList.add("tweet-active"); + activeTweet = tweet; + } + } catch (e) {} + + if (t.nonReply) { + tweet.classList.add("tweet-non-reply"); } - if (localStorage.preferredQuality && vars.savePreferredQuality) { - let closestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return Math.abs( - parseInt(curr.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) < - Math.abs( - parseInt(prev.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) - ? curr - : prev; - }); - let preferredQualityVariantIndex = v.video_info.variants.findIndex( - (v) => v.url === closestQuality.url - ); - if (preferredQualityVariantIndex !== -1) { - let preferredQualityVariant = - v.video_info.variants[preferredQualityVariantIndex]; - v.video_info.variants.splice(preferredQualityVariantIndex, 1); - v.video_info.variants.unshift(preferredQualityVariant); - } - } else if ( - window.navigator && - navigator.connection && - navigator.connection.type === "cellular" && - !vars.disableDataSaver + + if (t.threadContinuation) { + options.threadContinuation = true; + } + if (t.noTop) { + options.noTop = true; + } + if (options.threadContinuation) + tweet.classList.add("tweet-self-thread-continuation"); + if (options.selfThreadContinuation) + tweet.classList.add("tweet-self-thread-continuation"); + + if (options.noTop) tweet.classList.add("tweet-no-top"); + if (vars.slowLinkColorsInTL && typeof linkColors !== "undefined") { + if (linkColors[t.user.id_str]) { + let sc = makeSeeableColor(linkColors[t.user.id_str]); + tweet.style.setProperty("--link-color", sc); + if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + } else { + if ( + t.user.profile_link_color && + t.user.profile_link_color !== "1DA1F2" + ) { + let sc = makeSeeableColor(t.user.profile_link_color); + tweet.style.setProperty("--link-color", sc); + if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + } + } + } + let full_text = t.full_text ? t.full_text : ""; + let tweetLanguage = t.lang; // originally i used i18n api to detect languages simply because i didn't know of t.lang existence + if (!tweetLanguage) { + tweetLanguage = "und"; + } + if (tweetLanguage.includes("-")) { + let [lang, country] = tweetLanguage.split("-"); + tweetLanguage = `${lang}_${country.toUpperCase()}`; + } + let isMatchingLanguage = languageMatches(t.lang); + let isQuoteMatchingLanguage = + !!t.quoted_status && languageMatches(t.quoted_status.lang); + let videos = + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.filter((m) => m.type === "video"); + if (!videos || videos.length === 0) { + videos = undefined; + } + if (videos) { + for (let v of videos) { + if (!v.video_info) continue; + v.video_info.variants = v.video_info.variants.sort((a, b) => { + if (!b.bitrate) return -1; + return b.bitrate - a.bitrate; + }); + if (typeof vars.savePreferredQuality !== "boolean") { + chrome.storage.sync.set( + { + savePreferredQuality: true, + }, + () => {} + ); + vars.savePreferredQuality = true; + } + if ( + localStorage.preferredQuality && + vars.savePreferredQuality + ) { + let closestQuality = v.video_info.variants + .filter((v) => v.bitrate) + .reduce((prev, curr) => { + return Math.abs( + parseInt(curr.url.match(/\/(\d+)x/)[1]) - + parseInt(localStorage.preferredQuality) + ) < + Math.abs( + parseInt(prev.url.match(/\/(\d+)x/)[1]) - + parseInt(localStorage.preferredQuality) + ) + ? curr + : prev; + }); + let preferredQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === closestQuality.url + ); + if (preferredQualityVariantIndex !== -1) { + let preferredQualityVariant = + v.video_info.variants[preferredQualityVariantIndex]; + v.video_info.variants.splice( + preferredQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift(preferredQualityVariant); + } + } else if ( + window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ) { + let lowestQuality = v.video_info.variants + .filter((v) => v.bitrate) + .reduce((prev, curr) => { + return parseInt(curr.bitrate) < + parseInt(prev.bitrate) + ? curr + : prev; + }); + let lowestQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === lowestQuality.url + ); + if (lowestQualityVariantIndex !== -1) { + let lowestQualityVariant = + v.video_info.variants[lowestQualityVariantIndex]; + v.video_info.variants.splice( + lowestQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift(lowestQualityVariant); + } + } + } + } + if (full_text.includes("Learn more")) { + console.log(t); + } + if ( + t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY")) ) { - let lowestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return parseInt(curr.bitrate) < parseInt(prev.bitrate) - ? curr - : prev; - }); - let lowestQualityVariantIndex = v.video_info.variants.findIndex( - (v) => v.url === lowestQuality.url - ); - if (lowestQualityVariantIndex !== -1) { - let lowestQualityVariant = - v.video_info.variants[lowestQualityVariantIndex]; - v.video_info.variants.splice(lowestQualityVariantIndex, 1); - v.video_info.variants.unshift(lowestQualityVariant); - } + full_text = ""; } - } - } - if (full_text.includes("Learn more")) { - console.log(t); - } - if ( - t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY")) - ) { - full_text = ""; - } - if (!t.quoted_status) { - //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then) - try { - if (t.quoted_status_result && t.quoted_status_result.result.tweet) { - t.quoted_status = t.quoted_status_result.result.tweet.legacy; - t.quoted_status.user = - t.quoted_status_result.result.tweet.core.user_results.result.legacy; - } /* else if(t.quoted_status_id_str) { + if (!t.quoted_status) { + //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then) + try { + if ( + t.quoted_status_result && + t.quoted_status_result.result.tweet + ) { + t.quoted_status = + t.quoted_status_result.result.tweet.legacy; + t.quoted_status.user = + t.quoted_status_result.result.tweet.core.user_results.result.legacy; + } /* else if(t.quoted_status_id_str) { t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str); console.log(t.quoted_status); }*/ - } catch { - t.quoted_status = undefined; - } - } - let followUserText, unfollowUserText, blockUserText, unblockUserText; - let mentionedUserArray = [] - let _newQuoteMentionedUserText = []; - let quoteMentionedUserText = ``; - if ( - LOC.follow_user.message.includes("$SCREEN_NAME$") && - LOC.unfollow_user.message.includes("$SCREEN_NAME$") && - LOC.block_user.message.includes("$SCREEN_NAME$") && - LOC.unblock_user.message.includes("$SCREEN_NAME$") - ) { - followUserText = `${LOC.follow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - unfollowUserText = `${LOC.unfollow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - blockUserText = `${LOC.block_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - unblockUserText = `${LOC.unblock_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - } else { - followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; - unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; - blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; - unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; - } - if (t.in_reply_to_screen_name && t.display_text_range) { - t.entities.user_mentions.forEach((user_mention) => { - if (user_mention.indices[0] < t.display_text_range[0]) { - mentionedUserArray.push(elNew("a",{href:`/${user_mention.screen_name}`},[`@${user_mention.screen_name}`])) - } - }); - } - if ( - t.quoted_status && - t.quoted_status.in_reply_to_screen_name && - t.display_text_range - ) { - t.quoted_status.entities.user_mentions.forEach((user_mention) => { - if (user_mention.indices[0] < t.display_text_range[0]) { - _newQuoteMentionedUserText.push(`@${user_mention.screen_name}`) - // quoteMentionedUserText += `@${user_mention.screen_name}`; + } catch { + t.quoted_status = undefined; + } } - }); - } - // construct the markup for the tweet. - const [topNodes, actualContent] = await constructTweet( - t, - { - videos: videos, - isMatchingLanguage: isMatchingLanguage, - mentionedUserTextArray: mentionedUserArray, - full_text: full_text, - isQuoteMatchingLanguage: isQuoteMatchingLanguage, - newQuoteMentionedUserText: _newQuoteMentionedUserText, - // quoteMentionedUserText: quoteMentionedUserText, - followUserText: followUserText, - unfollowUserText: unfollowUserText, - blockUserText: blockUserText, - unblockUserText: unblockUserText, - }, - options - ); - // XXX: Seems ugly! - Array(...topNodes).forEach(m=>{ - tweet.appendChild(m) - }) - var tweetClasses = ["tweet-body",] - if (options.mainTweet) tweetClasses.push("tweet-body-main") - tweet.appendChild(elNew("article",{class:tweetClasses},actualContent)) - - - // tweet.innerHTML = ` - // ${topNodes} - //
- // ${actualContent} - //
- // `; - // gifs - let gifs = Array.from( - tweet.querySelectorAll(".tweet-media-gif, .tweet-media-element-quote-gif") - ); - if (gifs.length) { - gifs.forEach((gif) => { - gif.addEventListener("click", () => { - if (gif.paused) gif.play(); - else gif.pause(); - }); - }); - } - // video - let vidOverlay = tweet.getElementsByClassName( - "tweet-media-video-overlay" - )[0]; - if (vidOverlay) { - vidOverlay.addEventListener("click", async () => { - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - try { - let res = await fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length - if (!res.headers.get("content-length")) await sleep(1000); - } catch (e) { - console.error(e); + let followUserText, unfollowUserText, blockUserText, unblockUserText; + let mentionedUserArray = []; + let _newQuoteMentionedUserText = []; + let quoteMentionedUserText = ``; + if ( + LOC.follow_user.message.includes("$SCREEN_NAME$") && + LOC.unfollow_user.message.includes("$SCREEN_NAME$") && + LOC.block_user.message.includes("$SCREEN_NAME$") && + LOC.unblock_user.message.includes("$SCREEN_NAME$") + ) { + followUserText = `${LOC.follow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + unfollowUserText = `${LOC.unfollow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + blockUserText = `${LOC.block_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + unblockUserText = `${LOC.unblock_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + } else { + followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; + unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; + blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; + unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; } - vid.play(); - vid.controls = true; - vid.classList.remove("tweet-media-element-censor"); - vidOverlay.style.display = "none"; - }); - } - if (videos) { - let videoErrors = 0; - let vids = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO"); - vids[0].addEventListener("error", () => { - if (videoErrors >= 3) return; - videoErrors++; - setTimeout(() => { - vids[0].load(); - }, 25); - }); - vids[0].onloadstart = () => { - let src = vids[0].currentSrc; - Array.from(tweet.getElementsByClassName("tweet-video-quality")).forEach( - (el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - } - ); - tweet - .getElementsByClassName("tweet-video-reload")[0] - .addEventListener("click", () => { - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - let time = vid.currentTime; - let paused = vid.paused; - vid.load(); - vid.onloadstart = () => { - let src = vid.currentSrc; - vid.currentTime = time; - if (!paused) vid.play(); - Array.from( - tweet.getElementsByClassName("tweet-video-quality") - ).forEach((el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - else el.classList.remove("tweet-video-quality-current"); - }); - }; - }); - Array.from(tweet.getElementsByClassName("tweet-video-quality")).forEach( - (el) => - el.addEventListener("click", () => { - console.log("Reshuffling", el) - if (el.className.includes("tweet-video-quality-current")) return; - localStorage.preferredQuality = parseInt(el.innerText); - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - let time = vid.currentTime; - let paused = vid.paused; - for (let v of videos) { - let closestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return Math.abs( - parseInt(curr.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) < - Math.abs( - parseInt(prev.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) - ? curr - : prev; - }); - let preferredQualityVariantIndex = - v.video_info.variants.findIndex( - (v) => v.url === closestQuality.url - ); - if (preferredQualityVariantIndex !== -1) { - let preferredQualityVariant = - v.video_info.variants[preferredQualityVariantIndex]; - v.video_info.variants.splice(preferredQualityVariantIndex, 1); - v.video_info.variants.unshift(preferredQualityVariant); + if (t.in_reply_to_screen_name && t.display_text_range) { + t.entities.user_mentions.forEach((user_mention) => { + if (user_mention.indices[0] < t.display_text_range[0]) { + mentionedUserArray.push( + elNew("a", { href: `/${user_mention.screen_name}` }, [ + `@${user_mention.screen_name}`, + ]) + ); } - } - tweet.getElementsByClassName("tweet-media")[0].innerHTML = html` - ${t.extended_entities.media - .map( - (m) => - `<${m.type === "photo" ? "img" : "video"} ${ - m.ext_alt_text - ? `alt="${escapeHTML( - m.ext_alt_text, - true - )}" title="${escapeHTML(m.ext_alt_text, true)}"` - : "" - } crossorigin="anonymous" width="${ - sizeFunctions[t.extended_entities.media.length]( - m.original_info.width, - m.original_info.height - )[0] - }" height="${ - sizeFunctions[t.extended_entities.media.length]( - m.original_info.width, - m.original_info.height - )[1] - }" loading="lazy" ${ - m.type === "video" ? "controls" : "" - } ${ - m.type === "animated_gif" - ? 'loop muted onclick="if(this.paused) this.play(); else this.pause()"' - : "" - }${ - m.type === "animated_gif" && !vars.disableGifAutoplay - ? " autoplay" - : "" - } ${ - m.type === "photo" ? `src="${m.media_url_https}"` : "" - } class="tweet-media-element ${ - mediaClasses[t.extended_entities.media.length] - } ${ - !vars.displaySensitiveContent && t.possibly_sensitive - ? "tweet-media-element-censor" - : "" - }">${ - m.type === "video" || m.type === "animated_gif" - ? ` - ${m.video_info.variants - .map( - (v) => - `` - ) - .join("\n")} - ${LOC.unsupported_video.message} - ` - : "" - }` - ) - .join("\n")} - `; - vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - vid.onloadstart = () => { - let src = vid.currentSrc; - vid.currentTime = time; - if (!paused) vid.play(); - Array.from( - tweet.getElementsByClassName("tweet-video-quality") - ).forEach((el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - else el.classList.remove("tweet-video-quality-current"); - }); - }; - vid.addEventListener("mousedown", (e) => { - if (e.button === 1) { - e.preventDefault(); - window.open(vid.currentSrc, "_blank"); + }); + } + if ( + t.quoted_status && + t.quoted_status.in_reply_to_screen_name && + t.display_text_range + ) { + t.quoted_status.entities.user_mentions.forEach((user_mention) => { + if (user_mention.indices[0] < t.display_text_range[0]) { + _newQuoteMentionedUserText.push( + `@${user_mention.screen_name}` + ); + // quoteMentionedUserText += `@${user_mention.screen_name}`; } - }); - }) - ); - }; - for (let vid of vids) { - if (!vars.muteVideos && typeof vars.volume === "number") { - vid.volume = vars.volume; + }); } - vid.onvolumechange = () => { - chrome.storage.sync.set( + // construct the markup for the tweet. + const [topNodes, actualContent] = await constructTweet( + t, { - volume: vid.volume, + videos: videos, + isMatchingLanguage: isMatchingLanguage, + mentionedUserTextArray: mentionedUserArray, + full_text: full_text, + isQuoteMatchingLanguage: isQuoteMatchingLanguage, + newQuoteMentionedUserText: _newQuoteMentionedUserText, + // quoteMentionedUserText: quoteMentionedUserText, + followUserText: followUserText, + unfollowUserText: unfollowUserText, + blockUserText: blockUserText, + unblockUserText: unblockUserText, }, - () => {} - ); - if (vars.muteVideos) return; - let allVids = document.getElementsByTagName("video"); - for (let i = 0; i < allVids.length; i++) { - allVids[i].volume = vid.volume; - } - }; - vid.addEventListener("mousedown", (e) => { - if (e.button === 1) { - e.preventDefault(); - window.open(vid.currentSrc, "_blank"); - } + options + ); + // XXX: Seems ugly! + Array(...topNodes).forEach((m) => { + tweet.appendChild(m); }); - } - } + var tweetClasses = ["tweet-body"]; + if (options.mainTweet) tweetClasses.push("tweet-body-main"); + tweet.appendChild( + elNew("article", { class: tweetClasses }, actualContent) + ); - let footerFavorites = tweet.getElementsByClassName( - "tweet-footer-favorites" - )[0]; - if (t.card) { - generateCard(t, tweet, user); - } - if (options.top) { - tweet.querySelector(".tweet-top").hidden = false; - const icon = document.createElement("span"); - icon.innerText = options.top.icon; - icon.classList.add("tweet-top-icon"); - icon.style.color = options.top.color; - - const span = document.createElement("span"); - span.classList.add("tweet-top-text"); - span.innerHTML = options.top.text; - if (options.top.class) { - span.classList.add(options.top.class); - tweet.classList.add(`tweet-top-${options.top.class}`); - } - tweet.querySelector(".tweet-top").append(icon, span); - } - if (options.mainTweet) { - let likers = - vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 - ? mainTweetLikers.slice(0, 6) - : mainTweetLikers.slice(0, 8); - for (let i in likers) { - let liker = likers[i]; - let a = document.createElement("a"); - a.href = `/${liker.screen_name}`; - let likerImg = document.createElement("img"); - likerImg.src = `${ - liker.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(liker.id_str) % 7 - }_normal.png` - ) - : liker.profile_image_url_https - }`; - likerImg.classList.add("tweet-footer-favorites-img"); - likerImg.title = liker.name + " (@" + liker.screen_name + ")"; - likerImg.width = 24; - likerImg.height = 24; - a.dataset.id = liker.id_str; - a.appendChild(likerImg); - footerFavorites.appendChild(a); - } - let likesLink = tweet.getElementsByClassName("tweet-footer-stat-f")[0]; - likesLink.addEventListener("click", (e) => { - e.preventDefault(); - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/likes` + // tweet.innerHTML = ` + // ${topNodes} + //
+ // ${actualContent} + //
+ // `; + // gifs + let gifs = Array.from( + tweet.querySelectorAll( + ".tweet-media-gif, .tweet-media-element-quote-gif" + ) ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - updateLikes(id); - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - let retweetsLink = tweet.getElementsByClassName("tweet-footer-stat-r")[0]; - retweetsLink.addEventListener("click", (e) => { - e.preventDefault(); - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets` + if (gifs.length) { + gifs.forEach((gif) => { + gif.addEventListener("click", () => { + if (gif.paused) gif.play(); + else gif.pause(); + }); + }); + } + // video + let vidOverlay = tweet.getElementsByClassName( + "tweet-media-video-overlay" + )[0]; + if (vidOverlay) { + vidOverlay.addEventListener("click", async () => { + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0].children + ).filter((e) => e.tagName === "VIDEO")[0]; + try { + let res = await fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length + if (!res.headers.get("content-length")) await sleep(1000); + } catch (e) { + console.error(e); + } + vid.play(); + vid.controls = true; + vid.classList.remove("tweet-media-element-censor"); + vidOverlay.style.display = "none"; + }); + } + if (videos) { + let videoErrors = 0; + let vids = Array.from( + tweet.getElementsByClassName("tweet-media")[0].children + ).filter((e) => e.tagName === "VIDEO"); + vids[0].addEventListener("error", () => { + if (videoErrors >= 3) return; + videoErrors++; + setTimeout(() => { + vids[0].load(); + }, 25); + }); + vids[0].onloadstart = () => { + let src = vids[0].currentSrc; + Array.from( + tweet.getElementsByClassName("tweet-video-quality") + ).forEach((el) => { + if (el.dataset.url === src) + el.classList.add("tweet-video-quality-current"); + }); + tweet + .getElementsByClassName("tweet-video-reload")[0] + .addEventListener("click", () => { + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + let time = vid.currentTime; + let paused = vid.paused; + vid.load(); + vid.onloadstart = () => { + let src = vid.currentSrc; + vid.currentTime = time; + if (!paused) vid.play(); + Array.from( + tweet.getElementsByClassName( + "tweet-video-quality" + ) + ).forEach((el) => { + if (el.dataset.url === src) + el.classList.add( + "tweet-video-quality-current" + ); + else + el.classList.remove( + "tweet-video-quality-current" + ); + }); + }; + }); + Array.from( + tweet.getElementsByClassName("tweet-video-quality") + ).forEach((el) => + el.addEventListener("click", () => { + console.log("Reshuffling", el); + if ( + el.className.includes("tweet-video-quality-current") + ) + return; + localStorage.preferredQuality = parseInt(el.innerText); + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + let time = vid.currentTime; + let paused = vid.paused; + for (let v of videos) { + let closestQuality = v.video_info.variants + .filter((v) => v.bitrate) + .reduce((prev, curr) => { + return Math.abs( + parseInt( + curr.url.match(/\/(\d+)x/)[1] + ) - + parseInt( + localStorage.preferredQuality + ) + ) < + Math.abs( + parseInt( + prev.url.match(/\/(\d+)x/)[1] + ) - + parseInt( + localStorage.preferredQuality + ) + ) + ? curr + : prev; + }); + let preferredQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === closestQuality.url + ); + if (preferredQualityVariantIndex !== -1) { + let preferredQualityVariant = + v.video_info.variants[ + preferredQualityVariantIndex + ]; + v.video_info.variants.splice( + preferredQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift( + preferredQualityVariant + ); + } + } + tweet.getElementsByClassName( + "tweet-media" + )[0].innerHTML = html` + ${t.extended_entities.media + .map( + (m) => + `<${ + m.type === "photo" ? "img" : "video" + } ${ + m.ext_alt_text + ? `alt="${escapeHTML( + m.ext_alt_text, + true + )}" title="${escapeHTML( + m.ext_alt_text, + true + )}"` + : "" + } crossorigin="anonymous" width="${ + sizeFunctions[ + t.extended_entities.media.length + ]( + m.original_info.width, + m.original_info.height + )[0] + }" height="${ + sizeFunctions[ + t.extended_entities.media.length + ]( + m.original_info.width, + m.original_info.height + )[1] + }" loading="lazy" ${ + m.type === "video" ? "controls" : "" + } ${ + m.type === "animated_gif" + ? 'loop muted onclick="if(this.paused) this.play(); else this.pause()"' + : "" + }${ + m.type === "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } ${ + m.type === "photo" + ? `src="${m.media_url_https}"` + : "" + } class="tweet-media-element ${ + mediaClasses[ + t.extended_entities.media.length + ] + } ${ + !vars.displaySensitiveContent && + t.possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${ + m.type === "video" || + m.type === "animated_gif" + ? ` + ${m.video_info.variants + .map( + (v) => + `` + ) + .join("\n")} + ${LOC.unsupported_video.message} + ` + : "" + }` + ) + .join("\n")} + `; + vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + vid.onloadstart = () => { + let src = vid.currentSrc; + vid.currentTime = time; + if (!paused) vid.play(); + Array.from( + tweet.getElementsByClassName( + "tweet-video-quality" + ) + ).forEach((el) => { + if (el.dataset.url === src) + el.classList.add( + "tweet-video-quality-current" + ); + else + el.classList.remove( + "tweet-video-quality-current" + ); + }); + }; + vid.addEventListener("mousedown", (e) => { + if (e.button === 1) { + e.preventDefault(); + window.open(vid.currentSrc, "_blank"); + } + }); + }) + ); + }; + for (let vid of vids) { + if (!vars.muteVideos && typeof vars.volume === "number") { + vid.volume = vars.volume; + } + vid.onvolumechange = () => { + chrome.storage.sync.set( + { + volume: vid.volume, + }, + () => {} + ); + if (vars.muteVideos) return; + let allVids = document.getElementsByTagName("video"); + for (let i = 0; i < allVids.length; i++) { + allVids[i].volume = vid.volume; + } + }; + vid.addEventListener("mousedown", (e) => { + if (e.button === 1) { + e.preventDefault(); + window.open(vid.currentSrc, "_blank"); + } + }); + } + } + + let footerFavorites = tweet.getElementsByClassName( + "tweet-footer-favorites" + )[0]; + if (t.card) { + generateCard(t, tweet, user); + } + if (options.top) { + tweet.querySelector(".tweet-top").hidden = false; + const icon = document.createElement("span"); + icon.innerText = options.top.icon; + icon.classList.add("tweet-top-icon"); + icon.style.color = options.top.color; + + const span = document.createElement("span"); + span.classList.add("tweet-top-text"); + span.innerHTML = options.top.text; + if (options.top.class) { + span.classList.add(options.top.class); + tweet.classList.add(`tweet-top-${options.top.class}`); + } + tweet.querySelector(".tweet-top").append(icon, span); + } + if (options.mainTweet) { + let likers = + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ? mainTweetLikers.slice(0, 6) + : mainTweetLikers.slice(0, 8); + for (let i in likers) { + let liker = likers[i]; + let a = document.createElement("a"); + a.href = `/${liker.screen_name}`; + let likerImg = document.createElement("img"); + likerImg.src = `${ + liker.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(liker.id_str) % 7 + }_normal.png` + ) + : liker.profile_image_url_https + }`; + likerImg.classList.add("tweet-footer-favorites-img"); + likerImg.title = liker.name + " (@" + liker.screen_name + ")"; + likerImg.width = 24; + likerImg.height = 24; + a.dataset.id = liker.id_str; + a.appendChild(likerImg); + footerFavorites.appendChild(a); + } + let likesLink = tweet.getElementsByClassName( + "tweet-footer-stat-f" + )[0]; + likesLink.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/likes` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + updateLikes(id); + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + }); + let retweetsLink = tweet.getElementsByClassName( + "tweet-footer-stat-r" + )[0]; + retweetsLink.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + updateRetweets(id); + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + }); + if ( + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ) { + let quotesLink = tweet.getElementsByClassName( + "tweet-footer-stat-q" + )[0]; + quotesLink.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + updateRetweetsWithComments(id); + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + }); + } + let repliesLink = tweet.getElementsByClassName( + "tweet-footer-stat-o" + )[0]; + repliesLink.addEventListener("click", (e) => { + e.preventDefault(); + if ( + location.href === + `/${t.user.screen_name}/status/${t.id_str}` + ) + return; + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + updateReplies(id); + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + }); + } + if (options.mainTweet && t.user.id_str !== user.id_str) { + const tweetFollow = tweet.getElementsByClassName( + "tweet-header-follow" + )[0]; + tweetFollow.addEventListener("click", async () => { + if (t.user.following) { + try { + await API.user.unfollow(t.user.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetFollow.innerText = LOC.follow.message; + tweetFollow.classList.remove("following"); + tweetFollow.classList.add("follow"); + t.user.following = false; + } else { + try { + await API.user.follow(t.user.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetFollow.innerText = LOC.unfollow.message; + tweetFollow.classList.remove("follow"); + tweetFollow.classList.add("following"); + t.user.following = true; + } + }); + } + const tweetBody = tweet.getElementsByClassName("tweet-body")[0]; + const tweetBodyText = + tweet.getElementsByClassName("tweet-body-text")[0]; + const tweetTranslate = + tweet.getElementsByClassName("tweet-translate")[0]; + const tweetTranslateAfter = tweet.getElementsByClassName( + "tweet-translate-after" + )[0]; + const tweetQuoteTranslate = tweet.getElementsByClassName( + "tweet-quote-translate" + )[0]; + const tweetBodyQuote = + tweet.getElementsByClassName("tweet-body-quote")[0]; + const tweetMediaQuote = + tweet.getElementsByClassName("tweet-media-quote")[0]; + const tweetBodyQuoteText = tweet.getElementsByClassName( + "tweet-body-text-quote" + )[0]; + const tweetDeleteBookmark = tweet.getElementsByClassName( + "tweet-delete-bookmark" + )[0]; + + const tweetReplyCancel = + tweet.getElementsByClassName("tweet-reply-cancel")[0]; + const tweetReplyUpload = + tweet.getElementsByClassName("tweet-reply-upload")[0]; + const tweetReplyAddEmoji = tweet.getElementsByClassName( + "tweet-reply-add-emoji" + )[0]; + const tweetReply = tweet.getElementsByClassName("tweet-reply")[0]; + const tweetReplyButton = + tweet.getElementsByClassName("tweet-reply-button")[0]; + const tweetReplyError = + tweet.getElementsByClassName("tweet-reply-error")[0]; + const tweetReplyText = + tweet.getElementsByClassName("tweet-reply-text")[0]; + const tweetReplyChar = + tweet.getElementsByClassName("tweet-reply-char")[0]; + const tweetReplyMedia = + tweet.getElementsByClassName("tweet-reply-media")[0]; + + const tweetInteract = tweet.getElementsByClassName("tweet-interact")[0]; + const tweetInteractReply = tweet.getElementsByClassName( + "tweet-interact-reply" + )[0]; + const tweetInteractRetweet = tweet.getElementsByClassName( + "tweet-interact-retweet" + )[0]; + const tweetInteractFavorite = tweet.getElementsByClassName( + "tweet-interact-favorite" + )[0]; + const tweetInteractBookmark = tweet.getElementsByClassName( + "tweet-interact-bookmark" + )[0]; + const tweetInteractMore = tweet.getElementsByClassName( + "tweet-interact-more" + )[0]; + + const tweetFooter = tweet.getElementsByClassName("tweet-footer")[0]; + const tweetFooterReplies = tweet.getElementsByClassName( + "tweet-footer-stat-replies" + )[0]; + const tweetFooterRetweets = tweet.getElementsByClassName( + "tweet-footer-stat-retweets" + )[0]; + const tweetFooterFavorites = tweet.getElementsByClassName( + "tweet-footer-stat-favorites" + )[0]; + + const tweetQuote = tweet.getElementsByClassName("tweet-quote")[0]; + const tweetQuoteCancel = + tweet.getElementsByClassName("tweet-quote-cancel")[0]; + const tweetQuoteUpload = + tweet.getElementsByClassName("tweet-quote-upload")[0]; + const tweetQuoteAddEmoji = tweet.getElementsByClassName( + "tweet-quote-add-emoji" + )[0]; + const tweetQuoteButton = + tweet.getElementsByClassName("tweet-quote-button")[0]; + const tweetQuoteError = + tweet.getElementsByClassName("tweet-quote-error")[0]; + const tweetQuoteText = + tweet.getElementsByClassName("tweet-quote-text")[0]; + const tweetQuoteChar = + tweet.getElementsByClassName("tweet-quote-char")[0]; + const tweetQuoteMedia = + tweet.getElementsByClassName("tweet-quote-media")[0]; + + const tweetInteractRetweetMenu = tweet.getElementsByClassName( + "tweet-interact-retweet-menu" + )[0]; + const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-retweet" + )[0]; + const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-quote" + )[0]; + const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-quotes" + )[0]; + const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-retweeters" + )[0]; + + const tweetInteractMoreMenu = tweet.getElementsByClassName( + "tweet-interact-more-menu" + )[0]; + const tweetInteractMoreMenuCopy = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy" + )[0]; + const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy-tweet-id" + )[0]; + const tweetInteractMoreMenuLog = tweet.getElementsByClassName( + "tweet-interact-more-menu-log" + )[0]; + const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy-user-id" + )[0]; + const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName( + "tweet-interact-more-menu-embed" + )[0]; + const tweetInteractMoreMenuShare = tweet.getElementsByClassName( + "tweet-interact-more-menu-share" + )[0]; + const tweetInteractMoreMenuShareDMs = tweet.getElementsByClassName( + "tweet-interact-more-menu-share-dms" + )[0]; + const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName( + "tweet-interact-more-menu-newtwitter" + )[0]; + const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName( + "tweet-interact-more-menu-analytics" + )[0]; + const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName( + "tweet-interact-more-menu-refresh" + )[0]; + const tweetInteractMoreMenuMute = tweet.getElementsByClassName( + "tweet-interact-more-menu-mute" + )[0]; + const tweetInteractMoreMenuDownload = tweet.getElementsByClassName( + "tweet-interact-more-menu-download" + )[0]; + const tweetInteractMoreMenuDownloadGifs = Array.from( + tweet.getElementsByClassName( + "tweet-interact-more-menu-download-gif" + ) ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - updateRetweets(id); - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - if ( - vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 - ) { - let quotesLink = tweet.getElementsByClassName("tweet-footer-stat-q")[0]; - quotesLink.addEventListener("click", (e) => { - e.preventDefault(); - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` - ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - updateRetweetsWithComments(id); - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - } - let repliesLink = tweet.getElementsByClassName("tweet-footer-stat-o")[0]; - repliesLink.addEventListener("click", (e) => { - e.preventDefault(); - if (location.href === `/${t.user.screen_name}/status/${t.id_str}`) - return; - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}` + const tweetInteractMoreMenuDelete = tweet.getElementsByClassName( + "tweet-interact-more-menu-delete" + )[0]; + const tweetInteractMoreMenuPin = tweet.getElementsByClassName( + "tweet-interact-more-menu-pin" + )[0]; + const tweetInteractMoreMenuFollow = tweet.getElementsByClassName( + "tweet-interact-more-menu-follow" + )[0]; + const tweetInteractMoreMenuBlock = tweet.getElementsByClassName( + "tweet-interact-more-menu-block" + )[0]; + const tweetInteractMoreMenuMuteUser = tweet.getElementsByClassName( + "tweet-interact-more-menu-mute-user" + )[0]; + const tweetInteractMoreMenuListsAction = tweet.getElementsByClassName( + "tweet-interact-more-menu-lists-action" + )[0]; + const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName( + "tweet-interact-more-menu-bookmark" + )[0]; + const tweetInteractMoreMenuFeedbacks = Array.from( + tweet.getElementsByClassName("tweet-interact-more-menu-feedback") ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - updateReplies(id); - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - } - if (options.mainTweet && t.user.id_str !== user.id_str) { - const tweetFollow = tweet.getElementsByClassName( - "tweet-header-follow" - )[0]; - tweetFollow.addEventListener("click", async () => { - if (t.user.following) { - try { - await API.user.unfollow(t.user.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetFollow.innerText = LOC.follow.message; - tweetFollow.classList.remove("following"); - tweetFollow.classList.add("follow"); - t.user.following = false; - } else { - try { - await API.user.follow(t.user.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetFollow.innerText = LOC.unfollow.message; - tweetFollow.classList.remove("follow"); - tweetFollow.classList.add("following"); - t.user.following = true; - } - }); - } - const tweetBody = tweet.getElementsByClassName("tweet-body")[0]; - const tweetBodyText = tweet.getElementsByClassName("tweet-body-text")[0]; - const tweetTranslate = tweet.getElementsByClassName("tweet-translate")[0]; - const tweetTranslateAfter = tweet.getElementsByClassName( - "tweet-translate-after" - )[0]; - const tweetQuoteTranslate = tweet.getElementsByClassName( - "tweet-quote-translate" - )[0]; - const tweetBodyQuote = tweet.getElementsByClassName("tweet-body-quote")[0]; - const tweetMediaQuote = - tweet.getElementsByClassName("tweet-media-quote")[0]; - const tweetBodyQuoteText = tweet.getElementsByClassName( - "tweet-body-text-quote" - )[0]; - const tweetDeleteBookmark = tweet.getElementsByClassName( - "tweet-delete-bookmark" - )[0]; - - const tweetReplyCancel = - tweet.getElementsByClassName("tweet-reply-cancel")[0]; - const tweetReplyUpload = - tweet.getElementsByClassName("tweet-reply-upload")[0]; - const tweetReplyAddEmoji = tweet.getElementsByClassName( - "tweet-reply-add-emoji" - )[0]; - const tweetReply = tweet.getElementsByClassName("tweet-reply")[0]; - const tweetReplyButton = - tweet.getElementsByClassName("tweet-reply-button")[0]; - const tweetReplyError = - tweet.getElementsByClassName("tweet-reply-error")[0]; - const tweetReplyText = tweet.getElementsByClassName("tweet-reply-text")[0]; - const tweetReplyChar = tweet.getElementsByClassName("tweet-reply-char")[0]; - const tweetReplyMedia = - tweet.getElementsByClassName("tweet-reply-media")[0]; - - const tweetInteract = tweet.getElementsByClassName("tweet-interact")[0]; - const tweetInteractReply = tweet.getElementsByClassName( - "tweet-interact-reply" - )[0]; - const tweetInteractRetweet = tweet.getElementsByClassName( - "tweet-interact-retweet" - )[0]; - const tweetInteractFavorite = tweet.getElementsByClassName( - "tweet-interact-favorite" - )[0]; - const tweetInteractBookmark = tweet.getElementsByClassName( - "tweet-interact-bookmark" - )[0]; - const tweetInteractMore = tweet.getElementsByClassName( - "tweet-interact-more" - )[0]; - - const tweetFooter = tweet.getElementsByClassName("tweet-footer")[0]; - const tweetFooterReplies = tweet.getElementsByClassName( - "tweet-footer-stat-replies" - )[0]; - const tweetFooterRetweets = tweet.getElementsByClassName( - "tweet-footer-stat-retweets" - )[0]; - const tweetFooterFavorites = tweet.getElementsByClassName( - "tweet-footer-stat-favorites" - )[0]; - - const tweetQuote = tweet.getElementsByClassName("tweet-quote")[0]; - const tweetQuoteCancel = - tweet.getElementsByClassName("tweet-quote-cancel")[0]; - const tweetQuoteUpload = - tweet.getElementsByClassName("tweet-quote-upload")[0]; - const tweetQuoteAddEmoji = tweet.getElementsByClassName( - "tweet-quote-add-emoji" - )[0]; - const tweetQuoteButton = - tweet.getElementsByClassName("tweet-quote-button")[0]; - const tweetQuoteError = - tweet.getElementsByClassName("tweet-quote-error")[0]; - const tweetQuoteText = tweet.getElementsByClassName("tweet-quote-text")[0]; - const tweetQuoteChar = tweet.getElementsByClassName("tweet-quote-char")[0]; - const tweetQuoteMedia = - tweet.getElementsByClassName("tweet-quote-media")[0]; - - const tweetInteractRetweetMenu = tweet.getElementsByClassName( - "tweet-interact-retweet-menu" - )[0]; - const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-retweet" - )[0]; - const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-quote" - )[0]; - const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-quotes" - )[0]; - const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-retweeters" - )[0]; - - const tweetInteractMoreMenu = tweet.getElementsByClassName( - "tweet-interact-more-menu" - )[0]; - const tweetInteractMoreMenuCopy = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy" - )[0]; - const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy-tweet-id" - )[0]; - const tweetInteractMoreMenuLog = tweet.getElementsByClassName( - "tweet-interact-more-menu-log" - )[0]; - const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy-user-id" - )[0]; - const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName( - "tweet-interact-more-menu-embed" - )[0]; - const tweetInteractMoreMenuShare = tweet.getElementsByClassName( - "tweet-interact-more-menu-share" - )[0]; - const tweetInteractMoreMenuShareDMs = tweet.getElementsByClassName( - "tweet-interact-more-menu-share-dms" - )[0]; - const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName( - "tweet-interact-more-menu-newtwitter" - )[0]; - const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName( - "tweet-interact-more-menu-analytics" - )[0]; - const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName( - "tweet-interact-more-menu-refresh" - )[0]; - const tweetInteractMoreMenuMute = tweet.getElementsByClassName( - "tweet-interact-more-menu-mute" - )[0]; - const tweetInteractMoreMenuDownload = tweet.getElementsByClassName( - "tweet-interact-more-menu-download" - )[0]; - const tweetInteractMoreMenuDownloadGifs = Array.from( - tweet.getElementsByClassName("tweet-interact-more-menu-download-gif") - ); - const tweetInteractMoreMenuDelete = tweet.getElementsByClassName( - "tweet-interact-more-menu-delete" - )[0]; - const tweetInteractMoreMenuPin = tweet.getElementsByClassName( - "tweet-interact-more-menu-pin" - )[0]; - const tweetInteractMoreMenuFollow = tweet.getElementsByClassName( - "tweet-interact-more-menu-follow" - )[0]; - const tweetInteractMoreMenuBlock = tweet.getElementsByClassName( - "tweet-interact-more-menu-block" - )[0]; - const tweetInteractMoreMenuMuteUser = tweet.getElementsByClassName( - "tweet-interact-more-menu-mute-user" - )[0]; - const tweetInteractMoreMenuListsAction = tweet.getElementsByClassName( - "tweet-interact-more-menu-lists-action" - )[0]; - const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName( - "tweet-interact-more-menu-bookmark" - )[0]; - const tweetInteractMoreMenuFeedbacks = Array.from( - tweet.getElementsByClassName("tweet-interact-more-menu-feedback") - ); - const tweetInteractMoreMenuHide = tweet.getElementsByClassName( - "tweet-interact-more-menu-hide" - )[0]; - const tweetInteractMoreMenuSeparate = tweet.getElementsByClassName( - "tweet-interact-more-menu-separate" - )[0]; - - if (tweetInteractMoreMenuLog) - tweetInteractMoreMenuLog.addEventListener("click", () => { - console.log(t); - }); - - if (tweetInteractMoreMenuSeparate) - tweetInteractMoreMenuSeparate.addEventListener("click", () => { - tweetBodyText.style = ` + const tweetInteractMoreMenuHide = tweet.getElementsByClassName( + "tweet-interact-more-menu-hide" + )[0]; + const tweetInteractMoreMenuSeparate = tweet.getElementsByClassName( + "tweet-interact-more-menu-separate" + )[0]; + + if (tweetInteractMoreMenuLog) + tweetInteractMoreMenuLog.addEventListener("click", () => { + console.log(t); + }); + + if (tweetInteractMoreMenuSeparate) + tweetInteractMoreMenuSeparate.addEventListener("click", () => { + tweetBodyText.style = ` padding-top: 20px!important; display: block; font-size: 26px; line-height: unset; padding-bottom: 20px; `; - tweetInteractMoreMenuSeparate.style.display = "none"; - }); + tweetInteractMoreMenuSeparate.style.display = "none"; + }); - // Lists - if (tweetInteractMoreMenuListsAction) - tweetInteractMoreMenuListsAction.addEventListener("click", async () => { - createModal(` + // Lists + if (tweetInteractMoreMenuListsAction) + tweetInteractMoreMenuListsAction.addEventListener( + "click", + async () => { + createModal(`

${LOC.from_list.message}

`); - let lists = await API.list.getOwnerships(user.id_str, t.user.id_str); - let container = document.getElementById("modal-lists"); - for (let i in lists) { - let l = lists[i]; - let listElement = document.createElement("div"); - listElement.classList.add("list-item"); - listElement.innerHTML = html` - -
- -
- `; - container.appendChild(listElement); - listElement - .getElementsByClassName("nice-button")[0] - .addEventListener("click", async () => { - if (l.is_member) { - await API.list.removeMember(l.id_str, t.user.id_str); - l.is_member = false; - listElement.getElementsByClassName("nice-button")[0].innerText = - LOC.add.message; - } else { - await API.list.addMember(l.id_str, t.user.id_str); - l.is_member = true; - listElement.getElementsByClassName("nice-button")[0].innerText = - LOC.remove.message; - } - l.is_member = !l.is_member; + let lists = await API.list.getOwnerships( + user.id_str, + t.user.id_str + ); + let container = document.getElementById("modal-lists"); + for (let i in lists) { + let l = lists[i]; + let listElement = document.createElement("div"); + listElement.classList.add("list-item"); + listElement.innerHTML = html` + +
+ +
+ `; + container.appendChild(listElement); + listElement + .getElementsByClassName("nice-button")[0] + .addEventListener("click", async () => { + if (l.is_member) { + await API.list.removeMember( + l.id_str, + t.user.id_str + ); + l.is_member = false; + listElement.getElementsByClassName( + "nice-button" + )[0].innerText = LOC.add.message; + } else { + await API.list.addMember( + l.id_str, + t.user.id_str + ); + l.is_member = true; + listElement.getElementsByClassName( + "nice-button" + )[0].innerText = LOC.remove.message; + } + l.is_member = !l.is_member; + }); + } + } + ); + + // moderating tweets + if (tweetInteractMoreMenuHide) + tweetInteractMoreMenuHide.addEventListener("click", async () => { + if (t.moderated) { + try { + await API.tweet.unmoderate(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetInteractMoreMenuHide.innerText = + LOC.hide_tweet.message; + t.moderated = false; + } else { + let sure = confirm(LOC.hide_tweet_sure.message); + if (!sure) return; + try { + await API.tweet.moderate(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetInteractMoreMenuHide.innerText = + LOC.unhide_tweet.message; + t.moderated = true; + } }); + + // community notes + if (t.birdwatch && !vars.hideCommunityNotes) { + if (t.birdwatch.subtitle) { + let div = document.createElement("div"); + div.classList.add("tweet-birdwatch", "box"); + let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); + for ( + let e = t.birdwatch.subtitle.entities.length - 1; + e >= 0; + e-- + ) { + let entity = t.birdwatch.subtitle.entities[e]; + if (!entity.ref) continue; + text = arrayInsert(text, entity.toIndex, ""); + text = arrayInsert( + text, + entity.fromIndex, + `` + ); + } + text = text.join(""); + + div.innerHTML = html` +
+ ${escapeHTML(t.birdwatch.title)} +
+
+ ${text} +
+ `; + + if (tweetFooter) tweetFooter.before(div); + else tweetInteract.before(div); + } } - }); - - // moderating tweets - if (tweetInteractMoreMenuHide) - tweetInteractMoreMenuHide.addEventListener("click", async () => { - if (t.moderated) { - try { - await API.tweet.unmoderate(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetInteractMoreMenuHide.innerText = LOC.hide_tweet.message; - t.moderated = false; - } else { - let sure = confirm(LOC.hide_tweet_sure.message); - if (!sure) return; - try { - await API.tweet.moderate(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetInteractMoreMenuHide.innerText = LOC.unhide_tweet.message; - t.moderated = true; + + // rtl languages + if (rtlLanguages.includes(t.lang)) { + tweetBody.classList.add("rtl"); } - }); - - // community notes - if (t.birdwatch && !vars.hideCommunityNotes) { - if (t.birdwatch.subtitle) { - let div = document.createElement("div"); - div.classList.add("tweet-birdwatch", "box"); - let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); - for (let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) { - let entity = t.birdwatch.subtitle.entities[e]; - if (!entity.ref) continue; - text = arrayInsert(text, entity.toIndex, "
"); - text = arrayInsert( - text, - entity.fromIndex, - `` - ); + + // Quote body + if (tweetMediaQuote) + tweetMediaQuote.addEventListener("click", (e) => { + if (e && e.target && e.target.tagName === "VIDEO") { + e.preventDefault(); + e.stopPropagation(); + e.stopImmediatePropagation(); + if (e.target.paused) { + e.target.play(); + } else { + e.target.pause(); + } + } + }); + if (tweetBodyQuote) { + if (typeof mainTweetLikers !== "undefined") { + tweetBodyQuote.addEventListener("click", (e) => { + e.preventDefault(); + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (subpage === "tweet") { + updateReplies(id); + } else if (subpage === "likes") { + updateLikes(id); + } else if (subpage === "retweets") { + updateRetweets(id); + } else if (subpage === "retweets_with_comments") { + updateRetweetsWithComments(id); + } + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + }); + } else { + tweetBodyQuote.addEventListener("click", (e) => { + e.preventDefault(); + if ( + e.target.className && + e.target.className.includes("tweet-media-element") + ) { + if ( + !e.target.src.includes("?name=") && + !e.target.src.endsWith(":orig") && + !e.target.src.startsWith("data:") + ) { + e.target.src += "?name=orig"; + } else if (e.target.src.includes("?name=small")) { + e.target.src = e.target.src.replace( + "?name=small", + "?name=large" + ); + } + new Viewer(e.target.parentElement, { + transition: false, + zoomRatio: 0.3, + }); + e.target.click(); + return; + } + new TweetViewer(user, t.quoted_status); + }); + } + if (rtlLanguages.includes(t.quoted_status.lang)) { + tweetBodyQuoteText.classList.add("rtl"); + } else { + tweetBodyQuoteText.classList.add("ltr"); + } + if (tweetQuoteTranslate) { + let quoteTranslating = false; + tweetQuoteTranslate.addEventListener("click", async (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); + + if (t.quoted_status.translated || quoteTranslating) return; + quoteTranslating = true; + let translated = await API.tweet.translate( + t.quoted_status.id_str + ); + quoteTranslating = false; + t.quoted_status.translated = true; + tweetQuoteTranslate.hidden = true; + if (!translated.translated_lang || !translated.text) return; + let tt = t.full_text + .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") + .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") + .trim(); + if (translated.text.trim() === tt) return; + if ( + translated.text.trim() === + tt.replace(/(hihi)|(hehe)/g, "lol") + ) + return; // lol + const { hideOriginalLanguages } = + await chrome.storage.sync.get("hideOriginalLanguages"); + let translatedMessage; + if (LOC.translated_from.message.includes("$LANGUAGE$")) { + translatedMessage = LOC.translated_from.message.replace( + "$LANGUAGE$", + `[${translated.translated_lang}]` + ); + } else { + translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; + } + if (translated.text.length > 600) { + translated.text = + translated.text.substring(0, 600) + "..."; + } + if (hideOriginalLanguages) { + translatedMessage = ""; + tweetBodyQuoteText.innerHTML = ""; + } + tweetBodyQuoteText.innerHTML += + `${translatedMessage}:` + + `${escapeHTML( + translated.text + )}`; + if (vars.enableTwemoji) twemoji.parse(tweetBodyQuoteText); + }); + if ( + options.translate || + vars.autotranslateProfiles.includes( + t.quoted_status.user.id_str + ) || + (typeof toAutotranslate !== "undefined" && + toAutotranslate) || + (vars.autotranslateLanguages.includes( + t.quoted_status.lang + ) && + vars.autotranslationMode === "whitelist") || + (!vars.autotranslateLanguages.includes( + t.quoted_status.lang + ) && + vars.autotranslationMode === "blacklist") + ) { + onVisible(tweet, () => { + if (!t.quoted_status.translated) { + if (tweetQuoteTranslate) + tweetQuoteTranslate.click(); + } + }); + } + } } - text = text.join(""); - - div.innerHTML = html` -
- ${escapeHTML(t.birdwatch.title)} -
-
- ${text} -
- `; - if (tweetFooter) tweetFooter.before(div); - else tweetInteract.before(div); - } - } + // Translate + t.translated = false; + let translating = false; + if (tweetTranslate || tweetTranslateAfter) { + (tweetTranslate + ? tweetTranslate + : tweetTranslateAfter + ).addEventListener("click", async () => { + if (t.translated || translating) return; + translating = true; + let translated = await API.tweet.translate(t.id_str); + translating = false; + t.translated = true; + (tweetTranslate + ? tweetTranslate + : tweetTranslateAfter + ).hidden = true; + if (!translated.translated_lang || !translated.text) return; + let tt = t.full_text + .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") + .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") + .trim(); + if (translated.text.trim() === tt) return; + if ( + translated.text.trim() === + tt.replace(/(hihi)|(hehe)/g, "lol") + ) + return; // lol + const { hideOriginalLanguages } = await chrome.storage.sync.get( + "hideOriginalLanguages" + ); - // rtl languages - if (rtlLanguages.includes(t.lang)) { - tweetBody.classList.add("rtl"); - } + let translatedMessage; + if (LOC.translated_from.message.includes("$LANGUAGE$")) { + translatedMessage = LOC.translated_from.message.replace( + "$LANGUAGE$", + `[${translated.translated_lang}]` + ); + } else { + translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; + } + let translatedT = { + full_text: translated.text, + entities: translated.entities, + }; + let translatedFrom = document.createElement("span"); + translatedFrom.classList.add("translated-from"); + translatedFrom.innerText = translatedMessage; + + let translatedText = document.createElement("span"); + translatedText.classList.add("tweet-translated-text"); + translatedText.innerHTML = await renderTweetBodyHTML( + translatedT + ); + if (hideOriginalLanguages) { + tweetBodyText.innerHTML = ""; + tweetBodyText.append(translatedText); + } else { + tweetBodyText.append( + document.createElement("br"), + translatedFrom, + translatedText + ); + } + if (vars.enableTwemoji) twemoji.parse(tweetBodyText); + }); + if ( + options.translate || + vars.autotranslateProfiles.includes(t.user.id_str) || + (typeof toAutotranslate !== "undefined" && toAutotranslate) || + (vars.autotranslateLanguages.includes(t.lang) && + vars.autotranslationMode === "whitelist") || + (!vars.autotranslateLanguages.includes(t.lang) && + vars.autotranslationMode === "blacklist") + ) { + onVisible(tweet, () => { + if (!t.translated) { + if (tweetTranslate) tweetTranslate.click(); + else if (tweetTranslateAfter) + tweetTranslateAfter.click(); + } + }); + } + } + + // Bookmarks + let switchingBookmark = false; + let switchBookmark = () => { + if (switchingBookmark) return; + switchingBookmark = true; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + if (t.bookmarked) { + API.bookmarks + .delete(t.id_str) + .then(() => { + toast.info(LOC.unbookmarked_tweet.message); + switchingBookmark = false; + if (tweetDeleteBookmark) { + tweet.remove(); + if (timelineContainer.children.length === 0) { + timelineContainer.innerHTML = html`
+ ${LOC.empty.message} +
`; + document.getElementById( + "delete-all" + ).hidden = true; + } + return; + } + t.bookmarked = false; + t.bookmark_count--; + tweetInteractMoreMenuBookmark.innerText = + LOC.bookmark_tweet.message; + if (tweetInteractBookmark) { + tweetInteractBookmark.classList.remove( + "tweet-interact-bookmarked" + ); + tweetInteractBookmark.innerText = formatLargeNumber( + t.bookmark_count + ).replace(/\s/g, ","); + tweetInteractBookmark.dataset.val = + t.bookmark_count; + } + }) + .catch((e) => { + switchingBookmark = false; + console.error(e); + alert(e); + }); + } else { + API.bookmarks + .create(t.id_str) + .then(() => { + toast.info(LOC.bookmarked_tweet.message); + switchingBookmark = false; + t.bookmarked = true; + t.bookmark_count++; + tweetInteractMoreMenuBookmark.innerText = + LOC.remove_bookmark.message; + if (tweetInteractBookmark) { + tweetInteractBookmark.classList.add( + "tweet-interact-bookmarked" + ); + tweetInteractBookmark.innerText = formatLargeNumber( + t.bookmark_count + ).replace(/\s/g, ","); + tweetInteractBookmark.dataset.val = + t.bookmark_count; + } + }) + .catch((e) => { + switchingBookmark = false; + console.error(e); + alert(e); + }); + } + }; + if (tweetInteractBookmark) + tweetInteractBookmark.addEventListener("click", switchBookmark); + if (tweetInteractMoreMenuBookmark) + tweetInteractMoreMenuBookmark.addEventListener( + "click", + switchBookmark + ); + if (tweetDeleteBookmark) + tweetDeleteBookmark.addEventListener("click", async () => { + await API.bookmarks.delete(t.id_str); + tweet.remove(); + if (timelineContainer.children.length === 0) { + timelineContainer.innerHTML = html`
+ ${LOC.empty.message} +
`; + document.getElementById("delete-all").hidden = true; + } + }); + + // Media + if (t.extended_entities && t.extended_entities.media) { + const tweetMedia = tweet.getElementsByClassName("tweet-media")[0]; + tweetMedia.addEventListener("click", (e) => { + if ( + e.target.className && + e.target.className.includes("tweet-media-element-censor") + ) { + return e.target.classList.remove( + "tweet-media-element-censor" + ); + } + if (e.target.tagName === "IMG") { + if ( + !e.target.src.includes("?name=") && + !e.target.src.endsWith(":orig") && + !e.target.src.startsWith("data:") + ) { + e.target.src += "?name=orig"; + } else if (e.target.src.includes("?name=small")) { + e.target.src = e.target.src.replace( + "?name=small", + "?name=large" + ); + } + new Viewer(tweetMedia, { + transition: false, + zoomRatio: 0.3, + }); + e.target.click(); + } + }); + if ( + typeof pageUser !== "undefined" && + pageUser.id_str === t.user.id_str + ) { + let profileMediaDiv = + document.getElementById("profile-media-div"); + if (!options || !options.top || !options.top.text) + t.extended_entities.media.forEach((m) => { + if (profileMediaDiv.children.length >= 6) return; + let ch = Array.from(profileMediaDiv.children); + if (ch.find((c) => c.src === m.media_url_https)) return; + const media = document.createElement("img"); + media.classList.add( + "tweet-media-element", + "tweet-media-element-four", + "profile-media-preview" + ); + if ( + !vars.displaySensitiveContent && + t.possibly_sensitive + ) + media.classList.add("tweet-media-element-censor"); + media.src = m.media_url_https; + if (m.ext_alt_text) media.alt = m.ext_alt_text; + media.addEventListener("click", async () => { + new TweetViewer(user, t); + }); + profileMediaDiv.appendChild(media); + }); + } + } + + // Emojis + [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach((e) => { + e.addEventListener("click", (e) => { + let isReply = e.target.className === "tweet-reply-add-emoji"; + createEmojiPicker( + isReply ? tweetReply : tweetQuote, + isReply ? tweetReplyText : tweetQuoteText, + {} + ); + }); + }); + + // Reply + tweetReplyCancel.addEventListener("click", () => { + tweetReply.hidden = true; + tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); + }); + let replyMedia = []; + tweetReply.addEventListener("drop", (e) => { + handleDrop(e, replyMedia, tweetReplyMedia); + }); + tweetReply.addEventListener("paste", (event) => { + let items = ( + event.clipboardData || event.originalEvent.clipboardData + ).items; + for (let index in items) { + let item = items[index]; + if (item.kind === "file") { + let file = item.getAsFile(); + handleFiles([file], replyMedia, tweetReplyMedia); + } + } + }); + tweetReplyUpload.addEventListener("click", () => { + getMedia(replyMedia, tweetReplyMedia); + tweetReplyText.focus(); + }); + tweetInteractReply.addEventListener("click", () => { + if (options.mainTweet) { + document.getElementById("new-tweet").click(); + document.getElementById("new-tweet-text").focus(); + return; + } + if (!tweetQuote.hidden) tweetQuote.hidden = true; + if (tweetReply.hidden) { + tweetInteractReply.classList.add( + "tweet-interact-reply-clicked" + ); + } else { + tweetInteractReply.classList.remove( + "tweet-interact-reply-clicked" + ); + } + tweetReply.hidden = !tweetReply.hidden; + setTimeout(() => { + tweetReplyText.focus(); + }); + }); + tweetReplyText.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.ctrlKey) { + tweetReplyButton.click(); + } + }); + tweetReplyText.addEventListener("input", (e) => { + let tweet = twttr.txt.parseTweet(tweetReplyText.value); + if (localStorage.OTisBlueVerified) { + return (tweetReplyChar.innerText = `${tweet.weightedLength}/25000`); + } + tweetReplyChar.innerText = `${tweet.weightedLength}/280`; + if (tweet.weightedLength > 265) { + tweetReplyChar.style.color = "#c26363"; + } else { + tweetReplyChar.style.color = ""; + } + if (tweet.weightedLength > 280) { + tweetReplyChar.style.color = "red"; + tweetReplyButton.disabled = true; + } else { + tweetReplyChar.style.color = ""; + tweetReplyButton.disabled = false; + } + }); + tweetReplyButton.addEventListener("click", async () => { + tweetReplyError.innerHTML = ""; + let text = tweetReplyText.value; + if (text.length === 0 && replyMedia.length === 0) return; + tweetReplyButton.disabled = true; + let uploadedMedia = []; + for (let i in replyMedia) { + let media = replyMedia[i]; + try { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = false; + let mediaId; + if (!media.div.dataset.mediaId) { + mediaId = await API.uploadMedia({ + media_type: media.type, + media_category: media.category, + media: media.data, + alt: media.alt, + cw: media.cw, + loadCallback: (data) => { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = `${data.text} (${data.progress}%)`; + }, + }); + } else { + mediaId = media.div.dataset.mediaId; + } + uploadedMedia.push(mediaId); + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = LOC.uploaded.message; + media.div.dataset.mediaId = mediaId; + } catch (e) { + console.error(e); + alert(e); + for (let j in replyMedia) { + let media = replyMedia[j]; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = true; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = ""; + } + tweetReplyButton.disabled = false; + return; // cancel tweeting + } + } + let tweetObject = { + status: text, + in_reply_to_status_id: t.id_str, + }; + if (uploadedMedia.length > 0) { + tweetObject.media_ids = uploadedMedia.join(","); + } + let tweetData; + try { + tweetData = await API.tweet.postV2(tweetObject); + } catch (e) { + tweetReplyError.innerHTML = + (e && e.message ? e.message : e) + "
"; + tweetReplyButton.disabled = false; + return; + } + if (!tweetData) { + tweetReplyButton.disabled = false; + tweetReplyError.innerHTML = html`${LOC.error_sending_tweet + .message}
`; + return; + } + tweetReplyChar.innerText = localStorage.OTisBlueVerified + ? "0/25000" + : "0/280"; + tweetReplyText.value = ""; + tweetReply.hidden = true; + tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); + if (!options.mainTweet) { + tweetInteractReply.dataset.val = + parseInt(tweetInteractReply.dataset.val) + 1; + if (vars.showExactValues || t.reply_count < 10000) + tweetInteractReply.innerText = formatLargeNumber( + parseInt( + tweetInteractReply.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + tweetFooterReplies.dataset.val = + parseInt(tweetFooterReplies.dataset.val) + 1; + if (vars.showExactValues || t.reply_count < 10000) + tweetFooterReplies.innerText = formatLargeNumber( + parseInt( + tweetFooterReplies.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + if (typeof repliesToIgnore !== "undefined") { + repliesToIgnore.push(tweetData.id_str); + } + tweetData._ARTIFICIAL = true; + if (tweet.getElementsByClassName("tweet-self-thread-div")[0]) + tweet.getElementsByClassName( + "tweet-self-thread-div" + )[0].hidden = false; + tweetReplyButton.disabled = false; + tweetReplyMedia.innerHTML = []; + replyMedia = []; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + appendTweet(tweetData, document.getElementById("timeline"), { + noTop: true, + after: tweet, + }); + }); - // Quote body - if (tweetMediaQuote) - tweetMediaQuote.addEventListener("click", (e) => { - if (e && e.target && e.target.tagName === "VIDEO") { - e.preventDefault(); - e.stopPropagation(); - e.stopImmediatePropagation(); - if (e.target.paused) { - e.target.play(); - } else { - e.target.pause(); - } + // Retweet / Quote Tweet + let retweetClicked = false; + tweetQuoteCancel.addEventListener("click", () => { + tweetQuote.hidden = true; + }); + tweetInteractRetweet.addEventListener("click", async () => { + if ( + tweetInteractRetweet.classList.contains( + "tweet-interact-retweet-disabled" + ) + ) { + return; + } + if (!tweetQuote.hidden) { + tweetQuote.hidden = true; + return; + } + if (tweetInteractRetweetMenu.hidden) { + tweetInteractRetweetMenu.hidden = false; + tweetInteractRetweetMenu.style.marginTop = "-35px"; + } + if (retweetClicked) return; + retweetClicked = true; + setTimeout(() => { + document.body.addEventListener( + "click", + () => { + retweetClicked = false; + setTimeout( + () => (tweetInteractRetweetMenu.hidden = true), + 50 + ); + }, + { once: true } + ); + }, 50); + }); + t.renderRetweetsUp = (tweetData) => { + tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message; + tweetInteractRetweet.classList.add("tweet-interact-retweeted"); + t.retweeted = true; + t.retweet_count++; + t.newTweetId = tweetData.id_str; + if (!options.mainTweet) { + tweetInteractRetweet.dataset.val = + parseInt(tweetInteractRetweet.dataset.val) + 1; + if (vars.showExactValues || t.retweet_count < 10000) + tweetInteractRetweet.innerText = formatLargeNumber( + parseInt( + tweetInteractRetweet.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + if (vars.showExactValues || t.retweet_count < 10000) + tweetFooterRetweets.innerText = formatLargeNumber( + parseInt( + tweetFooterRetweets.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + }; + t.renderRetweetsDown = () => { + tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; + tweetInteractRetweet.classList.remove("tweet-interact-retweeted"); + t.retweeted = false; + t.retweet_count--; + if (!options.mainTweet) { + tweetInteractRetweet.dataset.val = + parseInt(tweetInteractRetweet.dataset.val) - 1; + if (vars.showExactValues || t.retweet_count < 10000) + tweetInteractRetweet.innerText = formatLargeNumber( + parseInt( + tweetInteractRetweet.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } else { + if (vars.showExactValues || t.retweet_count < 10000) + tweetFooterRetweets.innerText = formatLargeNumber( + parseInt( + tweetFooterRetweets.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } + delete t.newTweetId; + }; + tweetInteractRetweetMenuRetweet.addEventListener("click", async () => { + if (!t.retweeted) { + let tweetData; + try { + tweetData = await API.tweet.retweet(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + if (!tweetData) { + return; + } + t.renderRetweetsUp(tweetData); + } else { + let tweetData; + try { + tweetData = await API.tweet.unretweet( + t.retweeted_status + ? t.retweeted_status.id_str + : t.id_str + ); + } catch (e) { + console.error(e); + return; + } + if (!tweetData) { + return; + } + if (t.current_user_retweet) { + if ( + options.top && + options.top.icon && + options.top.icon === "\uf006" + ) { + tweet.remove(); + if (typeof timeline !== "undefined") { + let index = timeline.data.findIndex( + (tweet) => + tweet.retweeted_status && + tweet.retweeted_status.id_str === + t.id_str && + !tweet.current_user_retweet + ); + if (index > -1) { + timeline.data.splice(index, 1); + let originalTweet = timeline.data.find( + (tweet) => tweet.id_str === t.id_str + ); + if (originalTweet) { + delete originalTweet.current_user_retweet; + originalTweet.renderRetweetsDown(); + } + } + } + } else { + let retweetedElement = Array.from( + document.getElementsByClassName("tweet") + ).find( + (te) => + te.dataset.tweetId === t.id_str && + te.getElementsByClassName("retweet-label")[0] + ); + if (retweetedElement) { + retweetedElement.remove(); + } + if (typeof timeline !== "undefined") { + let index = timeline.data.findIndex( + (tweet) => + tweet.retweeted_status && + tweet.retweeted_status.id_str === + t.id_str && + !tweet.current_user_retweet + ); + if (index > -1) { + timeline.data.splice(index, 1); + } + } + } + } + t.renderRetweetsDown(); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + if (options.mainTweet) { + tweetInteractRetweetMenuQuotes.addEventListener( + "click", + async () => { + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (subpage === "tweet") { + updateReplies(id); + } else if (subpage === "likes") { + updateLikes(id); + } else if (subpage === "retweets") { + updateRetweets(id); + } else if (subpage === "retweets_with_comments") { + updateRetweetsWithComments(id); + } + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + } + ); + tweetInteractRetweetMenuRetweeters.addEventListener( + "click", + async () => { + document.getElementById("loading-box").hidden = false; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets` + ); + updateSubpage(); + mediaToUpload = []; + linkColors = {}; + cursor = undefined; + seenReplies = []; + mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (subpage === "tweet") { + updateReplies(id); + } else if (subpage === "likes") { + updateLikes(id); + } else if (subpage === "retweets") { + updateRetweets(id); + } else if (subpage === "retweets_with_comments") { + updateRetweetsWithComments(id); + } + renderDiscovery(); + renderTrends(); + currentLocation = location.pathname; + } + ); } - }); - if (tweetBodyQuote) { - if (typeof mainTweetLikers !== "undefined") { - tweetBodyQuote.addEventListener("click", (e) => { - e.preventDefault(); - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}` - ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (subpage === "tweet") { - updateReplies(id); - } else if (subpage === "likes") { - updateLikes(id); - } else if (subpage === "retweets") { - updateRetweets(id); - } else if (subpage === "retweets_with_comments") { - updateRetweetsWithComments(id); - } - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; + tweetInteractRetweetMenuQuote.addEventListener("click", async () => { + if (!tweetReply.hidden) { + tweetInteractReply.classList.remove( + "tweet-interact-reply-clicked" + ); + tweetReply.hidden = true; + } + tweetQuote.hidden = false; + setTimeout(() => { + tweetQuoteText.focus(); + }); + }); + let quoteMedia = []; + tweetQuote.addEventListener("drop", (e) => { + handleDrop(e, quoteMedia, tweetQuoteMedia); }); - } else { - tweetBodyQuote.addEventListener("click", (e) => { - e.preventDefault(); - if ( - e.target.className && - e.target.className.includes("tweet-media-element") - ) { + tweetQuote.addEventListener("paste", (event) => { + let items = ( + event.clipboardData || event.originalEvent.clipboardData + ).items; + for (let index in items) { + let item = items[index]; + if (item.kind === "file") { + let file = item.getAsFile(); + handleFiles([file], quoteMedia, tweetQuoteMedia); + } + } + }); + tweetQuoteUpload.addEventListener("click", () => { + getMedia(quoteMedia, tweetQuoteMedia); + }); + tweetQuoteText.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.ctrlKey) { + tweetQuoteButton.click(); + } + }); + tweetQuoteText.addEventListener("input", (e) => { + let tweet = twttr.txt.parseTweet(tweetQuoteText.value); + + if (localStorage.OTisBlueVerified) { + return (tweetQuoteChar.innerText = `${tweet.weightedLength}/25000`); + } + tweetQuoteChar.innerText = `${tweet.weightedLength}/280`; + if (tweet.weightedLength > 265) { + tweetQuoteChar.style.color = "#c26363"; + } else { + tweetQuoteChar.style.color = ""; + } + if (tweet.weightedLength > 280) { + tweetQuoteChar.style.color = "red"; + tweetQuoteButton.disabled = true; + } else { + tweetQuoteButton.disabled = false; + } + }); + tweetQuoteButton.addEventListener("click", async () => { + let text = tweetQuoteText.value; + tweetQuoteError.innerHTML = ""; + if (text.length === 0 && quoteMedia.length === 0) return; + tweetQuoteButton.disabled = true; + let uploadedMedia = []; + for (let i in quoteMedia) { + let media = quoteMedia[i]; + try { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = false; + let mediaId; + if (!media.div.dataset.mediaId) { + mediaId = await API.uploadMedia({ + media_type: media.type, + media_category: media.category, + media: media.data, + alt: media.alt, + cw: media.cw, + loadCallback: (data) => { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = `${data.text} (${data.progress}%)`; + }, + }); + } else { + mediaId = media.div.dataset.mediaId; + } + uploadedMedia.push(mediaId); + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = LOC.uploaded.message; + media.div.dataset.mediaId = mediaId; + } catch (e) { + console.error(e); + alert(e); + for (let j in quoteMedia) { + let media = quoteMedia[j]; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = true; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = ""; + } + tweetQuoteButton.disabled = false; + return; // cancel tweeting + } + } + let tweetObject = { + status: text, + attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`, + }; + if (uploadedMedia.length > 0) { + tweetObject.media_ids = uploadedMedia.join(","); + } + let tweetData; + try { + tweetData = await API.tweet.postV2(tweetObject); + } catch (e) { + tweetQuoteError.innerHTML = + (e && e.message ? e.message : e) + "
"; + tweetQuoteButton.disabled = false; + return; + } + if (!tweetData) { + tweetQuoteError.innerHTML = html`${LOC.error_sending_tweet}
`; + tweetQuoteButton.disabled = false; + return; + } + tweetQuoteText.value = ""; + tweetQuoteChar.innerText = localStorage.OTisBlueVerified + ? "0/25000" + : "0/280"; + tweetQuote.hidden = true; + tweetData._ARTIFICIAL = true; + quoteMedia = []; + tweetQuoteButton.disabled = false; + tweetQuoteMedia.innerHTML = ""; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + if (typeof timeline !== "undefined") + timeline.data.unshift(tweetData); + else appendTweet(tweetData, timelineContainer, { prepend: true }); + }); + + // Favorite + t.renderFavoritesDown = () => { + t.favorited = false; + t.favorite_count--; + if (!options.mainTweet) { + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) - 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetInteractFavorite.innerText = formatLargeNumber( + parseInt( + tweetInteractFavorite.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } else { + if ( + mainTweetLikers.find( + (liker) => liker.id_str === user.id_str + ) + ) { + mainTweetLikers.splice( + mainTweetLikers.findIndex( + (liker) => liker.id_str === user.id_str + ), + 1 + ); + let likerImg = footerFavorites.querySelector( + `a[data-id="${user.id_str}"]` + ); + if (likerImg) likerImg.remove(); + } + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) - 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetFooterFavorites.innerText = formatLargeNumber( + parseInt( + tweetFooterFavorites.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } + tweetInteractFavorite.classList.remove("tweet-interact-favorited"); + }; + t.renderFavoritesUp = () => { + t.favorited = true; + t.favorite_count++; + if (!options.mainTweet) { + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) + 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetInteractFavorite.innerText = formatLargeNumber( + parseInt( + tweetInteractFavorite.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + if ( + footerFavorites.children.length < 8 && + !mainTweetLikers.find( + (liker) => liker.id_str === user.id_str + ) + ) { + let a = document.createElement("a"); + a.href = `/${user.screen_name}`; + let likerImg = document.createElement("img"); + likerImg.src = `${ + user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(user.id_str) % 7 + }_normal.png` + ) + : user.profile_image_url_https + }`; + likerImg.classList.add("tweet-footer-favorites-img"); + likerImg.title = user.name + " (@" + user.screen_name + ")"; + likerImg.width = 24; + likerImg.height = 24; + a.dataset.id = user.id_str; + a.appendChild(likerImg); + footerFavorites.appendChild(a); + mainTweetLikers.push(user); + } + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) + 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetFooterFavorites.innerText = formatLargeNumber( + parseInt( + tweetFooterFavorites.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + tweetInteractFavorite.classList.add("tweet-interact-favorited"); + }; + tweetInteractFavorite.addEventListener("click", () => { + if (t.favorited) { + t.renderFavoritesDown(); + API.tweet.unfavorite(t.id_str).catch((e) => { + console.error(e); + alert(e); + t.renderFavoritesUp(); + }); + } else { + t.renderFavoritesUp(); + API.tweet.favorite(t.id_str).catch((e) => { + console.error(e); + if ( + e && + e.errors && + e.errors[0] && + e.errors[0].code === 139 + ) { + return; + } + alert(e); + t.renderFavoritesDown(); + }); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + + // More + let moreClicked = false; + tweetInteractMore.addEventListener("click", () => { + if (tweetInteractMoreMenu.hidden) { + tweetInteractMoreMenu.hidden = false; + } + if (moreClicked) return; + moreClicked = true; + setTimeout(() => { + document.body.addEventListener( + "click", + () => { + moreClicked = false; + setTimeout( + () => (tweetInteractMoreMenu.hidden = true), + 50 + ); + }, + { once: true } + ); + }, 50); + }); + if (tweetInteractMoreMenuFollow) + tweetInteractMoreMenuFollow.addEventListener("click", async () => { + if (t.user.following) { + try { + await API.user.unfollow(t.user.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + t.user.following = false; + tweetInteractMoreMenuFollow.innerText = followUserText; + let event = new CustomEvent("tweetAction", { + detail: { + action: "unfollow", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + try { + await API.user.follow(t.user.screen_name); + } catch (e) { + console.error(e); + alert(e); + return; + } + t.user.following = true; + tweetInteractMoreMenuFollow.innerText = unfollowUserText; + let event = new CustomEvent("tweetAction", { + detail: { + action: "follow", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + if (tweetInteractMoreMenuBlock) + tweetInteractMoreMenuBlock.addEventListener("click", async () => { + if (t.user.blocking) { + await API.user.unblock(t.user.id_str); + t.user.blocking = false; + if (LOC.block_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuBlock.innerText = + LOC.block_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; + } + tweetInteractMoreMenuFollow.hidden = false; + let event = new CustomEvent("tweetAction", { + detail: { + action: "unblock", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + let blockMessage; + if (LOC.block_sure.message.includes("$SCREEN_NAME$")) { + blockMessage = LOC.block_sure.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; + } + let c = confirm(blockMessage); + if (!c) return; + await API.user.block(t.user.id_str); + t.user.blocking = true; + if (LOC.unblock_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuBlock.innerText = + LOC.unblock_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; + } + tweetInteractMoreMenuFollow.hidden = true; + t.user.following = false; + tweetInteractMoreMenuFollow.innerText = followUserText; + let event = new CustomEvent("tweetAction", { + detail: { + action: "block", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + if (tweetInteractMoreMenuMuteUser) + tweetInteractMoreMenuMuteUser.addEventListener( + "click", + async () => { + if (t.user.muting) { + await API.user.unmute(t.user.id_str); + t.user.muting = false; + tweetInteractMoreMenuMuteUser.innerText = + LOC.mute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + + toast.info( + LOC.unmuted_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + ); + } else { + await API.user.mute(t.user.id_str); + t.user.muting = true; + tweetInteractMoreMenuMuteUser.innerText = + LOC.unmute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + + toast.info( + LOC.muted_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + ); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + } + ); + tweetInteractMoreMenuCopy.addEventListener("click", () => { + navigator.clipboard.writeText( + `https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}` + ); + }); + if (tweetInteractMoreMenuCopyTweetId) + tweetInteractMoreMenuCopyTweetId.addEventListener("click", () => { + navigator.clipboard.writeText(t.id_str); + }); + if (tweetInteractMoreMenuCopyUserId) + tweetInteractMoreMenuCopyUserId.addEventListener("click", () => { + navigator.clipboard.writeText(t.user.id_str); + }); + if (tweetInteractMoreMenuShare) + tweetInteractMoreMenuShare.addEventListener("click", () => { + navigator.share({ + url: `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`, + }); + }); + tweetInteractMoreMenuShareDMs.addEventListener("click", () => { + tweetUrlToShareInDMs = `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`; + document.getElementById("messages").click(); + setTimeout(() => { + let title = document.querySelector( + "div.inbox h1.nice-header.larger" + ); + title.innerText = LOC.share_tweet_to.message; + }); + }); + tweetInteractMoreMenuNewtwitter.addEventListener("click", () => { + openInNewTab( + `/${t.user.screen_name}/status/${t.id_str}?newtwitter=true` + ); + }); + tweetInteractMoreMenuEmbed.addEventListener("click", () => { + openInNewTab( + `https://publish.${location.hostname}/?query=https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}&widget=tweet` + ); + }); + if (t.user.id_str === user.id_str) { + tweetInteractMoreMenuAnalytics.addEventListener("click", () => { + openInNewTab( + `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true` + ); + }); + tweetInteractMoreMenuDelete.addEventListener("click", async () => { + let sure = confirm(LOC.delete_sure.message); + if (!sure) return; + try { + await API.tweet.delete(t.id_str); + } catch (e) { + alert(e); + console.error(e); + return; + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + Array.from( + document.querySelectorAll( + `div.tweet[data-tweet-id="${t.id_str}"]` + ) + ).forEach((tweet) => { + tweet.remove(); + }); + if (options.mainTweet) { + let tweets = Array.from( + document.getElementsByClassName("tweet") + ); + if (tweets.length === 0) { + location.href = "/home"; + } else { + location.href = + tweets[0].getElementsByClassName( + "tweet-time" + )[0].href; + } + } + if (typeof timeline !== "undefined") { + timeline.data = timeline.data.filter( + (tweet) => tweet.id_str !== t.id_str + ); + } + if (options.after && !options.disableAfterReplyCounter) { + if ( + options.after.getElementsByClassName( + "tweet-self-thread-div" + )[0] + ) + options.after.getElementsByClassName( + "tweet-self-thread-div" + )[0].hidden = true; + if (!options.after.classList.contains("tweet-main")) + options.after.getElementsByClassName( + "tweet-interact-reply" + )[0].innerText = ( + +options.after.getElementsByClassName( + "tweet-interact-reply" + )[0].innerText - 1 + ).toString(); + else + options.after.getElementsByClassName( + "tweet-footer-stat-replies" + )[0].innerText = ( + +options.after.getElementsByClassName( + "tweet-footer-stat-replies" + )[0].innerText - 1 + ).toString(); + } + if (typeof fixTweetThreadLine !== "undefined") + setTimeout(fixTweetThreadLine, 100); + }); + if (tweetInteractMoreMenuPin) + tweetInteractMoreMenuPin.addEventListener("click", async () => { + if (pinnedTweet && pinnedTweet.id_str === t.id_str) { + await API.tweet.unpin(t.id_str); + pinnedTweet = null; + tweet.remove(); + let tweetTime = new Date(t.created_at).getTime(); + let beforeTweet = Array.from( + document.getElementsByClassName("tweet") + ).find((i) => { + let timestamp = + +i.getElementsByClassName("tweet-time")[0] + .dataset.timestamp; + return timestamp < tweetTime; + }); + if (beforeTweet) { + appendTweet(t, timelineContainer, { + after: beforeTweet, + disableAfterReplyCounter: true, + }); + } + return; + } else { + await API.tweet.pin(t.id_str); + pinnedTweet = t; + let pinnedTweetElement = Array.from( + document.getElementsByClassName("tweet") + ).find((i) => { + let topText = + i.getElementsByClassName("tweet-top-text")[0]; + return ( + topText && topText.className.includes("pinned") + ); + }); + if (pinnedTweetElement) { + pinnedTweetElement.remove(); + } + tweet.remove(); + appendTweet(t, timelineContainer, { + prepend: true, + top: { + text: LOC.pinned_tweet.message, + icon: "\uf003", + color: "var(--link-color)", + class: "pinned", + }, + }); + return; + } + }); + } + tweetInteractMoreMenuRefresh.addEventListener("click", async () => { + let tweetData; + try { + tweetData = await API.tweet.getV2(t.id_str); + } catch (e) { + console.error(e); + return; + } + if (!tweetData) { + return; + } + if (typeof timeline !== "undefined") { + let tweetIndex = timeline.data.findIndex( + (tweet) => tweet.id_str === t.id_str + ); + if (tweetIndex !== -1) { + timeline.data[tweetIndex] = tweetData; + } + } + if ( + tweetInteractFavorite.className.includes( + "tweet-interact-favorited" + ) && + !tweetData.favorited + ) { + tweetInteractFavorite.classList.remove( + "tweet-interact-favorited" + ); + } + if ( + tweetInteractRetweet.className.includes( + "tweet-interact-retweeted" + ) && + !tweetData.retweeted + ) { + tweetInteractRetweet.classList.remove( + "tweet-interact-retweeted" + ); + } if ( - !e.target.src.includes("?name=") && - !e.target.src.endsWith(":orig") && - !e.target.src.startsWith("data:") + !tweetInteractFavorite.className.includes( + "tweet-interact-favorited" + ) && + tweetData.favorited ) { - e.target.src += "?name=orig"; - } else if (e.target.src.includes("?name=small")) { - e.target.src = e.target.src.replace("?name=small", "?name=large"); + tweetInteractFavorite.classList.add("tweet-interact-favorited"); } - new Viewer(e.target.parentElement, { - transition: false, - zoomRatio: 0.3, - }); - e.target.click(); - return; - } - new TweetViewer(user, t.quoted_status); - }); - } - if (rtlLanguages.includes(t.quoted_status.lang)) { - tweetBodyQuoteText.classList.add("rtl"); - } else { - tweetBodyQuoteText.classList.add("ltr"); - } - if (tweetQuoteTranslate) { - let quoteTranslating = false; - tweetQuoteTranslate.addEventListener("click", async (e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - - if (t.quoted_status.translated || quoteTranslating) return; - quoteTranslating = true; - let translated = await API.tweet.translate(t.quoted_status.id_str); - quoteTranslating = false; - t.quoted_status.translated = true; - tweetQuoteTranslate.hidden = true; - if (!translated.translated_lang || !translated.text) return; - let tt = t.full_text - .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") - .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") - .trim(); - if (translated.text.trim() === tt) return; - if (translated.text.trim() === tt.replace(/(hihi)|(hehe)/g, "lol")) - return; // lol - const { hideOriginalLanguages } = await chrome.storage.sync.get( - "hideOriginalLanguages" - ); - let translatedMessage; - if (LOC.translated_from.message.includes("$LANGUAGE$")) { - translatedMessage = LOC.translated_from.message.replace( - "$LANGUAGE$", - `[${translated.translated_lang}]` + if ( + !tweetInteractRetweet.className.includes( + "tweet-interact-retweeted" + ) && + tweetData.retweeted + ) { + tweetInteractRetweet.classList.add("tweet-interact-retweeted"); + } + if (!options.mainTweet) { + tweetInteractFavorite.innerText = tweetData.favorite_count; + tweetInteractRetweet.innerText = tweetData.retweet_count; + tweetInteractReply.innerText = tweetData.reply_count; + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} ); - } else { - translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; - } - if (translated.text.length > 600) { - translated.text = translated.text.substring(0, 600) + "..."; - } - if (hideOriginalLanguages) { - translatedMessage = ""; - tweetBodyQuoteText.innerHTML = ""; - } - tweetBodyQuoteText.innerHTML += - `${translatedMessage}:` + - `${escapeHTML( - translated.text - )}`; - if (vars.enableTwemoji) twemoji.parse(tweetBodyQuoteText); }); - if ( - options.translate || - vars.autotranslateProfiles.includes(t.quoted_status.user.id_str) || - (typeof toAutotranslate !== "undefined" && toAutotranslate) || - (vars.autotranslateLanguages.includes(t.quoted_status.lang) && - vars.autotranslationMode === "whitelist") || - (!vars.autotranslateLanguages.includes(t.quoted_status.lang) && - vars.autotranslationMode === "blacklist") - ) { - onVisible(tweet, () => { - if (!t.quoted_status.translated) { - if (tweetQuoteTranslate) tweetQuoteTranslate.click(); + tweetInteractMoreMenuMute.addEventListener("click", async () => { + if (t.conversation_muted) { + await API.tweet.unmute(t.id_str); + toast.info(LOC.unmuted_convo.message); + t.conversation_muted = false; + tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; + } else { + await API.tweet.mute(t.id_str); + toast.info(LOC.muted_convo.message); + t.conversation_muted = true; + tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; } - }); - } - } - } - - // Translate - t.translated = false; - let translating = false; - if (tweetTranslate || tweetTranslateAfter) { - (tweetTranslate ? tweetTranslate : tweetTranslateAfter).addEventListener( - "click", - async () => { - if (t.translated || translating) return; - translating = true; - let translated = await API.tweet.translate(t.id_str); - translating = false; - t.translated = true; - (tweetTranslate ? tweetTranslate : tweetTranslateAfter).hidden = true; - if (!translated.translated_lang || !translated.text) return; - let tt = t.full_text - .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") - .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") - .trim(); - if (translated.text.trim() === tt) return; - if (translated.text.trim() === tt.replace(/(hihi)|(hehe)/g, "lol")) - return; // lol - const { hideOriginalLanguages } = await chrome.storage.sync.get( - "hideOriginalLanguages" - ); - - let translatedMessage; - if (LOC.translated_from.message.includes("$LANGUAGE$")) { - translatedMessage = LOC.translated_from.message.replace( - "$LANGUAGE$", - `[${translated.translated_lang}]` - ); - } else { - translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; - } - let translatedT = { - full_text: translated.text, - entities: translated.entities, - }; - let translatedFrom = document.createElement("span"); - translatedFrom.classList.add("translated-from"); - translatedFrom.innerText = translatedMessage; - - let translatedText = document.createElement("span"); - translatedText.classList.add("tweet-translated-text"); - translatedText.innerHTML = await renderTweetBodyHTML(translatedT); - if (hideOriginalLanguages) { - tweetBodyText.innerHTML = ""; - tweetBodyText.append(translatedText); - } else { - tweetBodyText.append( - document.createElement("br"), - translatedFrom, - translatedText + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} ); - } - if (vars.enableTwemoji) twemoji.parse(tweetBodyText); - } - ); - if ( - options.translate || - vars.autotranslateProfiles.includes(t.user.id_str) || - (typeof toAutotranslate !== "undefined" && toAutotranslate) || - (vars.autotranslateLanguages.includes(t.lang) && - vars.autotranslationMode === "whitelist") || - (!vars.autotranslateLanguages.includes(t.lang) && - vars.autotranslationMode === "blacklist") - ) { - onVisible(tweet, () => { - if (!t.translated) { - if (tweetTranslate) tweetTranslate.click(); - else if (tweetTranslateAfter) tweetTranslateAfter.click(); - } }); - } - } - - // Bookmarks - let switchingBookmark = false; - let switchBookmark = () => { - if (switchingBookmark) return; - switchingBookmark = true; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - if (t.bookmarked) { - API.bookmarks - .delete(t.id_str) - .then(() => { - toast.info(LOC.unbookmarked_tweet.message); - switchingBookmark = false; - if (tweetDeleteBookmark) { - tweet.remove(); - if (timelineContainer.children.length === 0) { - timelineContainer.innerHTML = html`
- ${LOC.empty.message} -
`; - document.getElementById("delete-all").hidden = true; - } - return; - } - t.bookmarked = false; - t.bookmark_count--; - tweetInteractMoreMenuBookmark.innerText = - LOC.bookmark_tweet.message; - if (tweetInteractBookmark) { - tweetInteractBookmark.classList.remove( - "tweet-interact-bookmarked" - ); - tweetInteractBookmark.innerText = formatLargeNumber( - t.bookmark_count - ).replace(/\s/g, ","); - tweetInteractBookmark.dataset.val = t.bookmark_count; - } - }) - .catch((e) => { - switchingBookmark = false; - console.error(e); - alert(e); - }); - } else { - API.bookmarks - .create(t.id_str) - .then(() => { - toast.info(LOC.bookmarked_tweet.message); - switchingBookmark = false; - t.bookmarked = true; - t.bookmark_count++; - tweetInteractMoreMenuBookmark.innerText = - LOC.remove_bookmark.message; - if (tweetInteractBookmark) { - tweetInteractBookmark.classList.add("tweet-interact-bookmarked"); - tweetInteractBookmark.innerText = formatLargeNumber( - t.bookmark_count - ).replace(/\s/g, ","); - tweetInteractBookmark.dataset.val = t.bookmark_count; - } - }) - .catch((e) => { - switchingBookmark = false; - console.error(e); - alert(e); - }); - } - }; - if (tweetInteractBookmark) - tweetInteractBookmark.addEventListener("click", switchBookmark); - if (tweetInteractMoreMenuBookmark) - tweetInteractMoreMenuBookmark.addEventListener("click", switchBookmark); - if (tweetDeleteBookmark) - tweetDeleteBookmark.addEventListener("click", async () => { - await API.bookmarks.delete(t.id_str); - tweet.remove(); - if (timelineContainer.children.length === 0) { - timelineContainer.innerHTML = html`
- ${LOC.empty.message} -
`; - document.getElementById("delete-all").hidden = true; + let downloading = false; + if (t.extended_entities && t.extended_entities.media.length > 0) { + tweetInteractMoreMenuDownload.addEventListener("click", () => { + if (downloading) return; + downloading = true; + t.extended_entities.media.forEach((item, index) => { + let url = + item.type === "photo" + ? item.media_url_https + : item.video_info.variants[0].url; + url = new URL(url); + if (item.type === "photo") { + url.searchParams.set("name", "orig"); // force original resolution + } + _fetch(url) + .then((res) => res.blob()) + .then((blob) => { + downloading = false; + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + + let ts = new Date(t.created_at) + .toISOString() + .split("T")[0]; + let extension = url.pathname.split(".").pop(); + let _index = + t.extended_entities.media.length > 1 + ? "_" + (index + 1) + : ""; + let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; + let filename_template = vars.customDownloadTemplate; + + // use the filename from the user's custom download template, if any + if ( + filename_template && + filename_template.length > 0 + ) { + const filesave_map = { + user_screen_name: t.user.screen_name, + user_name: t.user.name, + extension: extension, + timestamp: ts, + id: t.id_str, + index: _index, + filename: url.pathname.substring( + url.pathname.lastIndexOf("/") + 1, + url.pathname.lastIndexOf(".") + ), + }; + filename = filename_template.replace( + /\{([\w]+)\}/g, + (_, key) => filesave_map[key] + ); + } + + a.download = filename; + a.click(); + a.remove(); + }) + .catch((e) => { + downloading = false; + console.error(e); + }); + }); + }); } - }); - - // Media - if (t.extended_entities && t.extended_entities.media) { - const tweetMedia = tweet.getElementsByClassName("tweet-media")[0]; - tweetMedia.addEventListener("click", (e) => { if ( - e.target.className && - e.target.className.includes("tweet-media-element-censor") + t.extended_entities && + t.extended_entities.media.some((m) => m.type === "animated_gif") ) { - return e.target.classList.remove("tweet-media-element-censor"); - } - if (e.target.tagName === "IMG") { - if ( - !e.target.src.includes("?name=") && - !e.target.src.endsWith(":orig") && - !e.target.src.startsWith("data:") - ) { - e.target.src += "?name=orig"; - } else if (e.target.src.includes("?name=small")) { - e.target.src = e.target.src.replace("?name=small", "?name=large"); - } - new Viewer(tweetMedia, { - transition: false, - zoomRatio: 0.3, - }); - e.target.click(); - } - }); - if ( - typeof pageUser !== "undefined" && - pageUser.id_str === t.user.id_str - ) { - let profileMediaDiv = document.getElementById("profile-media-div"); - if (!options || !options.top || !options.top.text) - t.extended_entities.media.forEach((m) => { - if (profileMediaDiv.children.length >= 6) return; - let ch = Array.from(profileMediaDiv.children); - if (ch.find((c) => c.src === m.media_url_https)) return; - const media = document.createElement("img"); - media.classList.add( - "tweet-media-element", - "tweet-media-element-four", - "profile-media-preview" - ); - if (!vars.displaySensitiveContent && t.possibly_sensitive) - media.classList.add("tweet-media-element-censor"); - media.src = m.media_url_https; - if (m.ext_alt_text) media.alt = m.ext_alt_text; - media.addEventListener("click", async () => { - new TweetViewer(user, t); - }); - profileMediaDiv.appendChild(media); - }); - } - } - - // Emojis - [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach((e) => { - e.addEventListener("click", (e) => { - let isReply = e.target.className === "tweet-reply-add-emoji"; - createEmojiPicker( - isReply ? tweetReply : tweetQuote, - isReply ? tweetReplyText : tweetQuoteText, - {} - ); - }); - }); - - // Reply - tweetReplyCancel.addEventListener("click", () => { - tweetReply.hidden = true; - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - }); - let replyMedia = []; - tweetReply.addEventListener("drop", (e) => { - handleDrop(e, replyMedia, tweetReplyMedia); - }); - tweetReply.addEventListener("paste", (event) => { - let items = (event.clipboardData || event.originalEvent.clipboardData) - .items; - for (let index in items) { - let item = items[index]; - if (item.kind === "file") { - let file = item.getAsFile(); - handleFiles([file], replyMedia, tweetReplyMedia); - } - } - }); - tweetReplyUpload.addEventListener("click", () => { - getMedia(replyMedia, tweetReplyMedia); - tweetReplyText.focus(); - }); - tweetInteractReply.addEventListener("click", () => { - if (options.mainTweet) { - document.getElementById("new-tweet").click(); - document.getElementById("new-tweet-text").focus(); - return; - } - if (!tweetQuote.hidden) tweetQuote.hidden = true; - if (tweetReply.hidden) { - tweetInteractReply.classList.add("tweet-interact-reply-clicked"); - } else { - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - } - tweetReply.hidden = !tweetReply.hidden; - setTimeout(() => { - tweetReplyText.focus(); - }); - }); - tweetReplyText.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { - tweetReplyButton.click(); - } - }); - tweetReplyText.addEventListener("input", (e) => { - let tweet = twttr.txt.parseTweet(tweetReplyText.value); - if (localStorage.OTisBlueVerified) { - return (tweetReplyChar.innerText = `${tweet.weightedLength}/25000`); - } - tweetReplyChar.innerText = `${tweet.weightedLength}/280`; - if (tweet.weightedLength > 265) { - tweetReplyChar.style.color = "#c26363"; - } else { - tweetReplyChar.style.color = ""; - } - if (tweet.weightedLength > 280) { - tweetReplyChar.style.color = "red"; - tweetReplyButton.disabled = true; - } else { - tweetReplyChar.style.color = ""; - tweetReplyButton.disabled = false; - } - }); - tweetReplyButton.addEventListener("click", async () => { - tweetReplyError.innerHTML = ""; - let text = tweetReplyText.value; - if (text.length === 0 && replyMedia.length === 0) return; - tweetReplyButton.disabled = true; - let uploadedMedia = []; - for (let i in replyMedia) { - let media = replyMedia[i]; - try { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = false; - let mediaId; - if (!media.div.dataset.mediaId) { - mediaId = await API.uploadMedia({ - media_type: media.type, - media_category: media.category, - media: media.data, - alt: media.alt, - cw: media.cw, - loadCallback: (data) => { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = `${data.text} (${data.progress}%)`; - }, - }); - } else { - mediaId = media.div.dataset.mediaId; - } - uploadedMedia.push(mediaId); - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = LOC.uploaded.message; - media.div.dataset.mediaId = mediaId; - } catch (e) { - console.error(e); - alert(e); - for (let j in replyMedia) { - let media = replyMedia[j]; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = true; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = ""; - } - tweetReplyButton.disabled = false; - return; // cancel tweeting - } - } - let tweetObject = { - status: text, - in_reply_to_status_id: t.id_str, - }; - if (uploadedMedia.length > 0) { - tweetObject.media_ids = uploadedMedia.join(","); - } - let tweetData; - try { - tweetData = await API.tweet.postV2(tweetObject); - } catch (e) { - tweetReplyError.innerHTML = (e && e.message ? e.message : e) + "
"; - tweetReplyButton.disabled = false; - return; - } - if (!tweetData) { - tweetReplyButton.disabled = false; - tweetReplyError.innerHTML = html`${LOC.error_sending_tweet - .message}
`; - return; - } - tweetReplyChar.innerText = localStorage.OTisBlueVerified - ? "0/25000" - : "0/280"; - tweetReplyText.value = ""; - tweetReply.hidden = true; - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - if (!options.mainTweet) { - tweetInteractReply.dataset.val = - parseInt(tweetInteractReply.dataset.val) + 1; - if (vars.showExactValues || t.reply_count < 10000) - tweetInteractReply.innerText = formatLargeNumber( - parseInt( - tweetInteractReply.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - tweetFooterReplies.dataset.val = - parseInt(tweetFooterReplies.dataset.val) + 1; - if (vars.showExactValues || t.reply_count < 10000) - tweetFooterReplies.innerText = formatLargeNumber( - parseInt( - tweetFooterReplies.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } - if (typeof repliesToIgnore !== "undefined") { - repliesToIgnore.push(tweetData.id_str); - } - tweetData._ARTIFICIAL = true; - if (tweet.getElementsByClassName("tweet-self-thread-div")[0]) - tweet.getElementsByClassName("tweet-self-thread-div")[0].hidden = false; - tweetReplyButton.disabled = false; - tweetReplyMedia.innerHTML = []; - replyMedia = []; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - appendTweet(tweetData, document.getElementById("timeline"), { - noTop: true, - after: tweet, - }); - }); - - // Retweet / Quote Tweet - let retweetClicked = false; - tweetQuoteCancel.addEventListener("click", () => { - tweetQuote.hidden = true; - }); - tweetInteractRetweet.addEventListener("click", async () => { - if ( - tweetInteractRetweet.classList.contains( - "tweet-interact-retweet-disabled" - ) - ) { - return; - } - if (!tweetQuote.hidden) { - tweetQuote.hidden = true; - return; - } - if (tweetInteractRetweetMenu.hidden) { - tweetInteractRetweetMenu.hidden = false; - tweetInteractRetweetMenu.style.marginTop = "-35px"; - } - if (retweetClicked) return; - retweetClicked = true; - setTimeout(() => { - document.body.addEventListener( - "click", - () => { - retweetClicked = false; - setTimeout(() => (tweetInteractRetweetMenu.hidden = true), 50); - }, - { once: true } - ); - }, 50); - }); - t.renderRetweetsUp = (tweetData) => { - tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message; - tweetInteractRetweet.classList.add("tweet-interact-retweeted"); - t.retweeted = true; - t.retweet_count++; - t.newTweetId = tweetData.id_str; - if (!options.mainTweet) { - tweetInteractRetweet.dataset.val = - parseInt(tweetInteractRetweet.dataset.val) + 1; - if (vars.showExactValues || t.retweet_count < 10000) - tweetInteractRetweet.innerText = formatLargeNumber( - parseInt( - tweetInteractRetweet.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - if (vars.showExactValues || t.retweet_count < 10000) - tweetFooterRetweets.innerText = formatLargeNumber( - parseInt( - tweetFooterRetweets.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } - }; - t.renderRetweetsDown = () => { - tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; - tweetInteractRetweet.classList.remove("tweet-interact-retweeted"); - t.retweeted = false; - t.retweet_count--; - if (!options.mainTweet) { - tweetInteractRetweet.dataset.val = - parseInt(tweetInteractRetweet.dataset.val) - 1; - if (vars.showExactValues || t.retweet_count < 10000) - tweetInteractRetweet.innerText = formatLargeNumber( - parseInt( - tweetInteractRetweet.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } else { - if (vars.showExactValues || t.retweet_count < 10000) - tweetFooterRetweets.innerText = formatLargeNumber( - parseInt( - tweetFooterRetweets.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } - delete t.newTweetId; - }; - tweetInteractRetweetMenuRetweet.addEventListener("click", async () => { - if (!t.retweeted) { - let tweetData; - try { - tweetData = await API.tweet.retweet(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - if (!tweetData) { - return; - } - t.renderRetweetsUp(tweetData); - } else { - let tweetData; - try { - tweetData = await API.tweet.unretweet( - t.retweeted_status ? t.retweeted_status.id_str : t.id_str - ); - } catch (e) { - console.error(e); - return; - } - if (!tweetData) { - return; - } - if (t.current_user_retweet) { - if ( - options.top && - options.top.icon && - options.top.icon === "\uf006" - ) { - tweet.remove(); - if (typeof timeline !== "undefined") { - let index = timeline.data.findIndex( - (tweet) => - tweet.retweeted_status && - tweet.retweeted_status.id_str === t.id_str && - !tweet.current_user_retweet - ); - if (index > -1) { - timeline.data.splice(index, 1); - let originalTweet = timeline.data.find( - (tweet) => tweet.id_str === t.id_str - ); - if (originalTweet) { - delete originalTweet.current_user_retweet; - originalTweet.renderRetweetsDown(); - } - } - } - } else { - let retweetedElement = Array.from( - document.getElementsByClassName("tweet") - ).find( - (te) => - te.dataset.tweetId === t.id_str && - te.getElementsByClassName("retweet-label")[0] + tweetInteractMoreMenuDownloadGifs.forEach((dgb) => + dgb.addEventListener("click", (e) => { + if (downloading) return; + downloading = true; + let n = parseInt(e.target.dataset.gifno) - 1; + let videos = Array.from( + tweet.getElementsByClassName("tweet-media-gif") + ); + let video = videos[n]; + let canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + let ctx = canvas.getContext("2d"); + if (video.duration > 10 && !confirm(LOC.long_vid.message)) { + return (downloading = false); + } + let mde = + tweet.getElementsByClassName("tweet-media-data")[0]; + mde.innerText = LOC.initialization.message + "..."; + let gif = new GIF({ + workers: 4, + quality: 15, + debug: true, + workerScript: window.gifWorkerUrl, + }); + video.currentTime = 0; + video.loop = false; + let isFirst = true; + let step = 50; + let interval = setInterval(async () => { + if (isFirst) { + video.currentTime = 0; + isFirst = false; + await sleep(5); + } + mde.innerText = `${ + LOC.initialization.message + }... (${Math.round( + ((video.currentTime / video.duration) * 100) | 0 + )}%)`; + if (video.currentTime + step / 1000 >= video.duration) { + clearInterval(interval); + gif.on("working", (frame, frames) => { + mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; + }); + gif.on("finished", (blob) => { + mde.innerText = ""; + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + + let ts = new Date(t.created_at) + .toISOString() + .split("T")[0]; + let extension = "gif"; + let _index = + t.extended_entities.media.length > 1 + ? "_" + (index + 1) + : ""; + let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; + let filename_template = + vars.customDownloadTemplate; + + // use the filename from the user's custom download template, if any + if ( + filename_template && + filename_template.length > 0 + ) { + const filesave_map = { + user_screen_name: t.user.screen_name, + user_name: t.user.name, + extension: extension, + timestamp: ts, + id: t.id_str, + index: _index, + filename: url.pathname.substring( + url.pathname.lastIndexOf("/") + 1, + url.pathname.lastIndexOf(".") + ), + }; + filename = filename_template.replace( + /\{([\w]+)\}/g, + (_, key) => filesave_map[key] + ); + } + a.download = filename; + + document.body.append(a); + a.click(); + a.remove(); + downloading = false; + video.loop = true; + video.play(); + }); + gif.render(); + return; + } + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + let imgData = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + gif.addFrame(imgData, { delay: step }); + }, step); + }) ); - if (retweetedElement) { - retweetedElement.remove(); - } - if (typeof timeline !== "undefined") { - let index = timeline.data.findIndex( - (tweet) => - tweet.retweeted_status && - tweet.retweeted_status.id_str === t.id_str && - !tweet.current_user_retweet - ); - if (index > -1) { - timeline.data.splice(index, 1); - } - } - } - } - t.renderRetweetsDown(); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (options.mainTweet) { - tweetInteractRetweetMenuQuotes.addEventListener("click", async () => { - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` - ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (subpage === "tweet") { - updateReplies(id); - } else if (subpage === "likes") { - updateLikes(id); - } else if (subpage === "retweets") { - updateRetweets(id); - } else if (subpage === "retweets_with_comments") { - updateRetweetsWithComments(id); - } - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - tweetInteractRetweetMenuRetweeters.addEventListener("click", async () => { - document.getElementById("loading-box").hidden = false; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets` - ); - updateSubpage(); - mediaToUpload = []; - linkColors = {}; - cursor = undefined; - seenReplies = []; - mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (subpage === "tweet") { - updateReplies(id); - } else if (subpage === "likes") { - updateLikes(id); - } else if (subpage === "retweets") { - updateRetweets(id); - } else if (subpage === "retweets_with_comments") { - updateRetweetsWithComments(id); - } - renderDiscovery(); - renderTrends(); - currentLocation = location.pathname; - }); - } - tweetInteractRetweetMenuQuote.addEventListener("click", async () => { - if (!tweetReply.hidden) { - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - tweetReply.hidden = true; - } - tweetQuote.hidden = false; - setTimeout(() => { - tweetQuoteText.focus(); - }); - }); - let quoteMedia = []; - tweetQuote.addEventListener("drop", (e) => { - handleDrop(e, quoteMedia, tweetQuoteMedia); - }); - tweetQuote.addEventListener("paste", (event) => { - let items = (event.clipboardData || event.originalEvent.clipboardData) - .items; - for (let index in items) { - let item = items[index]; - if (item.kind === "file") { - let file = item.getAsFile(); - handleFiles([file], quoteMedia, tweetQuoteMedia); } - } - }); - tweetQuoteUpload.addEventListener("click", () => { - getMedia(quoteMedia, tweetQuoteMedia); - }); - tweetQuoteText.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { - tweetQuoteButton.click(); - } - }); - tweetQuoteText.addEventListener("input", (e) => { - let tweet = twttr.txt.parseTweet(tweetQuoteText.value); - - if (localStorage.OTisBlueVerified) { - return (tweetQuoteChar.innerText = `${tweet.weightedLength}/25000`); - } - tweetQuoteChar.innerText = `${tweet.weightedLength}/280`; - if (tweet.weightedLength > 265) { - tweetQuoteChar.style.color = "#c26363"; - } else { - tweetQuoteChar.style.color = ""; - } - if (tweet.weightedLength > 280) { - tweetQuoteChar.style.color = "red"; - tweetQuoteButton.disabled = true; - } else { - tweetQuoteButton.disabled = false; - } - }); - tweetQuoteButton.addEventListener("click", async () => { - let text = tweetQuoteText.value; - tweetQuoteError.innerHTML = ""; - if (text.length === 0 && quoteMedia.length === 0) return; - tweetQuoteButton.disabled = true; - let uploadedMedia = []; - for (let i in quoteMedia) { - let media = quoteMedia[i]; - try { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = false; - let mediaId; - if (!media.div.dataset.mediaId) { - mediaId = await API.uploadMedia({ - media_type: media.type, - media_category: media.category, - media: media.data, - alt: media.alt, - cw: media.cw, - loadCallback: (data) => { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = `${data.text} (${data.progress}%)`; - }, + if (tweetInteractMoreMenuFeedbacks) + tweetInteractMoreMenuFeedbacks.forEach((feedbackButton) => { + let feedback = t.feedback[feedbackButton.dataset.index]; + if (!feedback) return; + feedbackButton.addEventListener("click", () => { + chrome.storage.local.remove(["algoTimeline"], () => {}); + if ( + feedback.richBehavior && + feedback.richBehavior.markNotInterestedTopic + ) { + fetch( + `/i/api/graphql/OiKldXdrDrSjh36WO9_3Xw/TopicNotInterested`, + { + method: "post", + headers: { + "content-type": "application/json", + authorization: + OLDTWITTER_CONFIG.public_token, + "x-twitter-active-user": "yes", + "x-csrf-token": OLDTWITTER_CONFIG.csrf, + "x-twitter-auth-type": "OAuth2Session", + }, + body: JSON.stringify({ + variables: { + topicId: + feedback.richBehavior + .markNotInterestedTopic.topicId, + undo: false, + }, + queryId: "OiKldXdrDrSjh36WO9_3Xw", + }), + credentials: "include", + } + ) + .then((i) => i.json()) + .then(() => {}); + } + fetch(`/i/api${feedback.feedbackUrl}`, { + method: "post", + headers: { + "content-type": "application/x-www-form-urlencoded", + authorization: OLDTWITTER_CONFIG.public_token, + "x-twitter-active-user": "yes", + "x-csrf-token": OLDTWITTER_CONFIG.csrf, + "x-twitter-auth-type": "OAuth2Session", + }, + body: `feedback_type=${feedback.feedbackType}&feedback_metadata=${t.feedbackMetadata}&undo=false`, + credentials: "include", + }) + .then((i) => i.json()) + .then((i) => { + alert( + feedback.confirmation + ? feedback.confirmation + : LOC.feedback_thanks.message + ); + tweet.remove(); + }); + }); }); - } else { - mediaId = media.div.dataset.mediaId; - } - uploadedMedia.push(mediaId); - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = LOC.uploaded.message; - media.div.dataset.mediaId = mediaId; - } catch (e) { - console.error(e); - alert(e); - for (let j in quoteMedia) { - let media = quoteMedia[j]; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = true; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = ""; - } - tweetQuoteButton.disabled = false; - return; // cancel tweeting - } - } - let tweetObject = { - status: text, - attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`, - }; - if (uploadedMedia.length > 0) { - tweetObject.media_ids = uploadedMedia.join(","); - } - let tweetData; - try { - tweetData = await API.tweet.postV2(tweetObject); - } catch (e) { - tweetQuoteError.innerHTML = (e && e.message ? e.message : e) + "
"; - tweetQuoteButton.disabled = false; - return; - } - if (!tweetData) { - tweetQuoteError.innerHTML = html`${LOC.error_sending_tweet}
`; - tweetQuoteButton.disabled = false; - return; - } - tweetQuoteText.value = ""; - tweetQuoteChar.innerText = localStorage.OTisBlueVerified - ? "0/25000" - : "0/280"; - tweetQuote.hidden = true; - tweetData._ARTIFICIAL = true; - quoteMedia = []; - tweetQuoteButton.disabled = false; - tweetQuoteMedia.innerHTML = ""; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - if (typeof timeline !== "undefined") timeline.data.unshift(tweetData); - else appendTweet(tweetData, timelineContainer, { prepend: true }); - }); - // Favorite - t.renderFavoritesDown = () => { - t.favorited = false; - t.favorite_count--; - if (!options.mainTweet) { - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) - 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetInteractFavorite.innerText = formatLargeNumber( - parseInt( - tweetInteractFavorite.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } else { - if (mainTweetLikers.find((liker) => liker.id_str === user.id_str)) { - mainTweetLikers.splice( - mainTweetLikers.findIndex((liker) => liker.id_str === user.id_str), - 1 - ); - let likerImg = footerFavorites.querySelector( - `a[data-id="${user.id_str}"]` - ); - if (likerImg) likerImg.remove(); - } - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) - 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetFooterFavorites.innerText = formatLargeNumber( - parseInt( - tweetFooterFavorites.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } - tweetInteractFavorite.classList.remove("tweet-interact-favorited"); - }; - t.renderFavoritesUp = () => { - t.favorited = true; - t.favorite_count++; - if (!options.mainTweet) { - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) + 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetInteractFavorite.innerText = formatLargeNumber( - parseInt( - tweetInteractFavorite.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - if ( - footerFavorites.children.length < 8 && - !mainTweetLikers.find((liker) => liker.id_str === user.id_str) - ) { - let a = document.createElement("a"); - a.href = `/${user.screen_name}`; - let likerImg = document.createElement("img"); - likerImg.src = `${ - user.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(user.id_str) % 7 - }_normal.png` - ) - : user.profile_image_url_https - }`; - likerImg.classList.add("tweet-footer-favorites-img"); - likerImg.title = user.name + " (@" + user.screen_name + ")"; - likerImg.width = 24; - likerImg.height = 24; - a.dataset.id = user.id_str; - a.appendChild(likerImg); - footerFavorites.appendChild(a); - mainTweetLikers.push(user); + if (options.noInsert) { + return tweet; } - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) + 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetFooterFavorites.innerText = formatLargeNumber( - parseInt( - tweetFooterFavorites.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } - tweetInteractFavorite.classList.add("tweet-interact-favorited"); - }; - tweetInteractFavorite.addEventListener("click", () => { - if (t.favorited) { - t.renderFavoritesDown(); - API.tweet.unfavorite(t.id_str).catch((e) => { - console.error(e); - alert(e); - t.renderFavoritesUp(); - }); - } else { - t.renderFavoritesUp(); - API.tweet.favorite(t.id_str).catch((e) => { - console.error(e); - if (e && e.errors && e.errors[0] && e.errors[0].code === 139) { - return; - } - alert(e); - t.renderFavoritesDown(); - }); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - // More - let moreClicked = false; - tweetInteractMore.addEventListener("click", () => { - if (tweetInteractMoreMenu.hidden) { - tweetInteractMoreMenu.hidden = false; - } - if (moreClicked) return; - moreClicked = true; - setTimeout(() => { - document.body.addEventListener( - "click", - () => { - moreClicked = false; - setTimeout(() => (tweetInteractMoreMenu.hidden = true), 50); - }, - { once: true } - ); - }, 50); - }); - if (tweetInteractMoreMenuFollow) - tweetInteractMoreMenuFollow.addEventListener("click", async () => { - if (t.user.following) { - try { - await API.user.unfollow(t.user.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - t.user.following = false; - tweetInteractMoreMenuFollow.innerText = followUserText; - let event = new CustomEvent("tweetAction", { - detail: { - action: "unfollow", - tweet: t, - }, - }); - document.dispatchEvent(event); - } else { - try { - await API.user.follow(t.user.screen_name); - } catch (e) { - console.error(e); - alert(e); - return; - } - t.user.following = true; - tweetInteractMoreMenuFollow.innerText = unfollowUserText; - let event = new CustomEvent("tweetAction", { - detail: { - action: "follow", - tweet: t, - }, - }); - document.dispatchEvent(event); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (tweetInteractMoreMenuBlock) - tweetInteractMoreMenuBlock.addEventListener("click", async () => { - if (t.user.blocking) { - await API.user.unblock(t.user.id_str); - t.user.blocking = false; - if (LOC.block_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuBlock.innerText = - LOC.block_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; - } - tweetInteractMoreMenuFollow.hidden = false; - let event = new CustomEvent("tweetAction", { - detail: { - action: "unblock", - tweet: t, - }, - }); - document.dispatchEvent(event); + if (options.after) { + options.after.after(tweet); + } else if (options.before) { + options.before.before(tweet); + } else if (options.prepend) { + timelineContainer.prepend(tweet); } else { - let blockMessage; - if (LOC.block_sure.message.includes("$SCREEN_NAME$")) { - blockMessage = LOC.block_sure.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; - } - let c = confirm(blockMessage); - if (!c) return; - await API.user.block(t.user.id_str); - t.user.blocking = true; - if (LOC.unblock_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuBlock.innerText = - LOC.unblock_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; - } - tweetInteractMoreMenuFollow.hidden = true; - t.user.following = false; - tweetInteractMoreMenuFollow.innerText = followUserText; - let event = new CustomEvent("tweetAction", { - detail: { - action: "block", - tweet: t, - }, - }); - document.dispatchEvent(event); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (tweetInteractMoreMenuMuteUser) - tweetInteractMoreMenuMuteUser.addEventListener("click", async () => { - if (t.user.muting) { - await API.user.unmute(t.user.id_str); - t.user.muting = false; - tweetInteractMoreMenuMuteUser.innerText = - LOC.mute_user.message.replace("$SCREEN_NAME$", t.user.screen_name); - - toast.info( - LOC.unmuted_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ) - ); - } else { - await API.user.mute(t.user.id_str); - t.user.muting = true; - tweetInteractMoreMenuMuteUser.innerText = - LOC.unmute_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - - toast.info( - LOC.muted_user.message.replace("$SCREEN_NAME$", t.user.screen_name) - ); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - tweetInteractMoreMenuCopy.addEventListener("click", () => { - navigator.clipboard.writeText( - `https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}` - ); - }); - if (tweetInteractMoreMenuCopyTweetId) - tweetInteractMoreMenuCopyTweetId.addEventListener("click", () => { - navigator.clipboard.writeText(t.id_str); - }); - if (tweetInteractMoreMenuCopyUserId) - tweetInteractMoreMenuCopyUserId.addEventListener("click", () => { - navigator.clipboard.writeText(t.user.id_str); - }); - if (tweetInteractMoreMenuShare) - tweetInteractMoreMenuShare.addEventListener("click", () => { - navigator.share({ - url: `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`, - }); - }); - tweetInteractMoreMenuShareDMs.addEventListener("click", () => { - tweetUrlToShareInDMs = `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`; - document.getElementById("messages").click(); - setTimeout(() => { - let title = document.querySelector("div.inbox h1.nice-header.larger"); - title.innerText = LOC.share_tweet_to.message; - }); - }); - tweetInteractMoreMenuNewtwitter.addEventListener("click", () => { - openInNewTab(`/${t.user.screen_name}/status/${t.id_str}?newtwitter=true`); - }); - tweetInteractMoreMenuEmbed.addEventListener("click", () => { - openInNewTab( - `https://publish.${location.hostname}/?query=https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}&widget=tweet` - ); - }); - if (t.user.id_str === user.id_str) { - tweetInteractMoreMenuAnalytics.addEventListener("click", () => { - openInNewTab( - `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true` - ); - }); - tweetInteractMoreMenuDelete.addEventListener("click", async () => { - let sure = confirm(LOC.delete_sure.message); - if (!sure) return; - try { - await API.tweet.delete(t.id_str); - } catch (e) { - alert(e); - console.error(e); - return; - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - Array.from( - document.querySelectorAll(`div.tweet[data-tweet-id="${t.id_str}"]`) - ).forEach((tweet) => { - tweet.remove(); - }); - if (options.mainTweet) { - let tweets = Array.from(document.getElementsByClassName("tweet")); - if (tweets.length === 0) { - location.href = "/home"; - } else { - location.href = - tweets[0].getElementsByClassName("tweet-time")[0].href; - } - } - if (typeof timeline !== "undefined") { - timeline.data = timeline.data.filter( - (tweet) => tweet.id_str !== t.id_str - ); + timelineContainer.append(tweet); } - if (options.after && !options.disableAfterReplyCounter) { - if (options.after.getElementsByClassName("tweet-self-thread-div")[0]) - options.after.getElementsByClassName( - "tweet-self-thread-div" - )[0].hidden = true; - if (!options.after.classList.contains("tweet-main")) - options.after.getElementsByClassName( - "tweet-interact-reply" - )[0].innerText = ( - +options.after.getElementsByClassName("tweet-interact-reply")[0] - .innerText - 1 - ).toString(); - else - options.after.getElementsByClassName( - "tweet-footer-stat-replies" - )[0].innerText = ( - +options.after.getElementsByClassName( - "tweet-footer-stat-replies" - )[0].innerText - 1 - ).toString(); - } - if (typeof fixTweetThreadLine !== "undefined") - setTimeout(fixTweetThreadLine, 100); - }); - if (tweetInteractMoreMenuPin) - tweetInteractMoreMenuPin.addEventListener("click", async () => { - if (pinnedTweet && pinnedTweet.id_str === t.id_str) { - await API.tweet.unpin(t.id_str); - pinnedTweet = null; - tweet.remove(); - let tweetTime = new Date(t.created_at).getTime(); - let beforeTweet = Array.from( - document.getElementsByClassName("tweet") - ).find((i) => { - let timestamp = - +i.getElementsByClassName("tweet-time")[0].dataset.timestamp; - return timestamp < tweetTime; - }); - if (beforeTweet) { - appendTweet(t, timelineContainer, { - after: beforeTweet, - disableAfterReplyCounter: true, - }); - } - return; - } else { - await API.tweet.pin(t.id_str); - pinnedTweet = t; - let pinnedTweetElement = Array.from( - document.getElementsByClassName("tweet") - ).find((i) => { - let topText = i.getElementsByClassName("tweet-top-text")[0]; - return topText && topText.className.includes("pinned"); - }); - if (pinnedTweetElement) { - pinnedTweetElement.remove(); - } - tweet.remove(); - appendTweet(t, timelineContainer, { - prepend: true, - top: { - text: LOC.pinned_tweet.message, - icon: "\uf003", - color: "var(--link-color)", - class: "pinned", - }, - }); - return; - } - }); - } - tweetInteractMoreMenuRefresh.addEventListener("click", async () => { - let tweetData; - try { - tweetData = await API.tweet.getV2(t.id_str); - } catch (e) { + if (vars.enableTwemoji) twemoji.parse(tweet); + return tweet; + } catch (e) { console.error(e); - return; - } - if (!tweetData) { - return; - } - if (typeof timeline !== "undefined") { - let tweetIndex = timeline.data.findIndex( - (tweet) => tweet.id_str === t.id_str - ); - if (tweetIndex !== -1) { - timeline.data[tweetIndex] = tweetData; - } - } - if ( - tweetInteractFavorite.className.includes("tweet-interact-favorited") && - !tweetData.favorited - ) { - tweetInteractFavorite.classList.remove("tweet-interact-favorited"); - } - if ( - tweetInteractRetweet.className.includes("tweet-interact-retweeted") && - !tweetData.retweeted - ) { - tweetInteractRetweet.classList.remove("tweet-interact-retweeted"); - } - if ( - !tweetInteractFavorite.className.includes("tweet-interact-favorited") && - tweetData.favorited - ) { - tweetInteractFavorite.classList.add("tweet-interact-favorited"); - } - if ( - !tweetInteractRetweet.className.includes("tweet-interact-retweeted") && - tweetData.retweeted - ) { - tweetInteractRetweet.classList.add("tweet-interact-retweeted"); - } - if (!options.mainTweet) { - tweetInteractFavorite.innerText = tweetData.favorite_count; - tweetInteractRetweet.innerText = tweetData.retweet_count; - tweetInteractReply.innerText = tweetData.reply_count; - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - tweetInteractMoreMenuMute.addEventListener("click", async () => { - if (t.conversation_muted) { - await API.tweet.unmute(t.id_str); - toast.info(LOC.unmuted_convo.message); - t.conversation_muted = false; - tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; - } else { - await API.tweet.mute(t.id_str); - toast.info(LOC.muted_convo.message); - t.conversation_muted = true; - tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - let downloading = false; - if (t.extended_entities && t.extended_entities.media.length > 0) { - tweetInteractMoreMenuDownload.addEventListener("click", () => { - if (downloading) return; - downloading = true; - t.extended_entities.media.forEach((item, index) => { - let url = - item.type === "photo" - ? item.media_url_https - : item.video_info.variants[0].url; - url = new URL(url); - if (item.type === "photo") { - url.searchParams.set("name", "orig"); // force original resolution - } - _fetch(url) - .then((res) => res.blob()) - .then((blob) => { - downloading = false; - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - - let ts = new Date(t.created_at).toISOString().split("T")[0]; - let extension = url.pathname.split(".").pop(); - let _index = - t.extended_entities.media.length > 1 ? "_" + (index + 1) : ""; - let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; - let filename_template = vars.customDownloadTemplate; - - // use the filename from the user's custom download template, if any - if (filename_template && filename_template.length > 0) { - const filesave_map = { - user_screen_name: t.user.screen_name, - user_name: t.user.name, - extension: extension, - timestamp: ts, - id: t.id_str, - index: _index, - filename: url.pathname.substring( - url.pathname.lastIndexOf("/") + 1, - url.pathname.lastIndexOf(".") - ), - }; - filename = filename_template.replace( - /\{([\w]+)\}/g, - (_, key) => filesave_map[key] - ); - } - - a.download = filename; - a.click(); - a.remove(); - }) - .catch((e) => { - downloading = false; - console.error(e); - }); - }); - }); - } - if ( - t.extended_entities && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ) { - tweetInteractMoreMenuDownloadGifs.forEach((dgb) => - dgb.addEventListener("click", (e) => { - if (downloading) return; - downloading = true; - let n = parseInt(e.target.dataset.gifno) - 1; - let videos = Array.from( - tweet.getElementsByClassName("tweet-media-gif") - ); - let video = videos[n]; - let canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - let ctx = canvas.getContext("2d"); - if (video.duration > 10 && !confirm(LOC.long_vid.message)) { - return (downloading = false); - } - let mde = tweet.getElementsByClassName("tweet-media-data")[0]; - mde.innerText = LOC.initialization.message + "..."; - let gif = new GIF({ - workers: 4, - quality: 15, - debug: true, - workerScript: window.gifWorkerUrl, - }); - video.currentTime = 0; - video.loop = false; - let isFirst = true; - let step = 50; - let interval = setInterval(async () => { - if (isFirst) { - video.currentTime = 0; - isFirst = false; - await sleep(5); - } - mde.innerText = `${LOC.initialization.message}... (${Math.round( - ((video.currentTime / video.duration) * 100) | 0 - )}%)`; - if (video.currentTime + step / 1000 >= video.duration) { - clearInterval(interval); - gif.on("working", (frame, frames) => { - mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; - }); - gif.on("finished", (blob) => { - mde.innerText = ""; - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - - let ts = new Date(t.created_at).toISOString().split("T")[0]; - let extension = "gif"; - let _index = - t.extended_entities.media.length > 1 ? "_" + (index + 1) : ""; - let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; - let filename_template = vars.customDownloadTemplate; - - // use the filename from the user's custom download template, if any - if (filename_template && filename_template.length > 0) { - const filesave_map = { - user_screen_name: t.user.screen_name, - user_name: t.user.name, - extension: extension, - timestamp: ts, - id: t.id_str, - index: _index, - filename: url.pathname.substring( - url.pathname.lastIndexOf("/") + 1, - url.pathname.lastIndexOf(".") - ), - }; - filename = filename_template.replace( - /\{([\w]+)\}/g, - (_, key) => filesave_map[key] - ); - } - a.download = filename; - - document.body.append(a); - a.click(); - a.remove(); - downloading = false; - video.loop = true; - video.play(); - }); - gif.render(); - return; - } - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); - gif.addFrame(imgData, { delay: step }); - }, step); - }) - ); - } - if (tweetInteractMoreMenuFeedbacks) - tweetInteractMoreMenuFeedbacks.forEach((feedbackButton) => { - let feedback = t.feedback[feedbackButton.dataset.index]; - if (!feedback) return; - feedbackButton.addEventListener("click", () => { - chrome.storage.local.remove(["algoTimeline"], () => {}); - if ( - feedback.richBehavior && - feedback.richBehavior.markNotInterestedTopic - ) { - fetch(`/i/api/graphql/OiKldXdrDrSjh36WO9_3Xw/TopicNotInterested`, { - method: "post", - headers: { - "content-type": "application/json", - authorization: OLDTWITTER_CONFIG.public_token, - "x-twitter-active-user": "yes", - "x-csrf-token": OLDTWITTER_CONFIG.csrf, - "x-twitter-auth-type": "OAuth2Session", - }, - body: JSON.stringify({ - variables: { - topicId: feedback.richBehavior.markNotInterestedTopic.topicId, - undo: false, - }, - queryId: "OiKldXdrDrSjh36WO9_3Xw", - }), - credentials: "include", - }) - .then((i) => i.json()) - .then(() => {}); - } - fetch(`/i/api${feedback.feedbackUrl}`, { - method: "post", - headers: { - "content-type": "application/x-www-form-urlencoded", - authorization: OLDTWITTER_CONFIG.public_token, - "x-twitter-active-user": "yes", - "x-csrf-token": OLDTWITTER_CONFIG.csrf, - "x-twitter-auth-type": "OAuth2Session", - }, - body: `feedback_type=${feedback.feedbackType}&feedback_metadata=${t.feedbackMetadata}&undo=false`, - credentials: "include", - }) - .then((i) => i.json()) - .then((i) => { - alert( - feedback.confirmation - ? feedback.confirmation - : LOC.feedback_thanks.message - ); - tweet.remove(); - }); - }); - }); - - if (options.noInsert) { - return tweet; - } - - if (options.after) { - options.after.after(tweet); - } else if (options.before) { - options.before.before(tweet); - } else if (options.prepend) { - timelineContainer.prepend(tweet); - } else { - timelineContainer.append(tweet); - } - if (vars.enableTwemoji) twemoji.parse(tweet); - return tweet; - } catch (e) { - console.error(e); - if (Date.now() - lastTweetErrorDate > 1000) { - lastTweetErrorDate = Date.now(); - createModal(` + if (Date.now() - lastTweetErrorDate > 1000) { + lastTweetErrorDate = Date.now(); + createModal(`
`); + } + return null; } - return null; - } } let lastNotificationErrorDate = 0; const iconClasses = { - heart_icon: "ni-favorite", - person_icon: "ni-follow", - retweet_icon: "ni-retweet", - recommendation_icon: "ni-recommend", - lightning_bolt_icon: "ni-bolt", - bird_icon: "ni-twitter", - security_alert_icon: "ni-alert", - bell_icon: "ni-bell", - list_icon: "ni-list", - milestone_icon: "ni-milestone", + heart_icon: "ni-favorite", + person_icon: "ni-follow", + retweet_icon: "ni-retweet", + recommendation_icon: "ni-recommend", + lightning_bolt_icon: "ni-bolt", + bird_icon: "ni-twitter", + security_alert_icon: "ni-alert", + bell_icon: "ni-bell", + list_icon: "ni-list", + milestone_icon: "ni-milestone", }; let aRegex = /]*>([\s\S]*?)<\/a>/g; let replacerLocs; function renderNotification(n, options = {}) { - if (typeof n !== "object") { - console.error("Notification is undefined", t); - return; - } - - try { - let notification = document.createElement("div"); - notification.className = "notification"; - notification.dataset.notificationId = n.id; - - if (options.unread) { - notification.classList.add("notification-unread"); + if (typeof n !== "object") { + console.error("Notification is undefined", t); + return; } - let notificationHeader = n.message.text; - if (n.message.entities) { - let additionalLength = 0; - let matches = 0; - n.message.entities.forEach((e) => { - if (!e.ref || !e.ref.user) return; - let user = n.users[e.ref.user.id]; - notificationHeader = Array.from(notificationHeader); - notificationHeader = arrayInsert( - notificationHeader, - e.toIndex + additionalLength, - "" - ); - notificationHeader = arrayInsert( - notificationHeader, - e.fromIndex + additionalLength, - `` - ); - notificationHeader = notificationHeader.join(""); - additionalLength += ``.length; - let mi = 0; - let newText = notificationHeader.replace(aRegex, (_, m) => { - if (mi++ !== matches) return _; - return `${escapeHTML(m)}`; - }); - additionalLength += newText.length - notificationHeader.length; - notificationHeader = newText; - matches++; - }); - } + try { + let notification = document.createElement("div"); + notification.className = "notification"; + notification.dataset.notificationId = n.id; - notification.addEventListener("click", (e) => { - if ( - e.target.closest(".notification") && - e.target.tagName !== "IMG" && - e.target.tagName !== "A" && - e.target.className !== "notification-feedback" - ) { - if (n.icon.id === "bell_icon") { - let a = document.createElement("a"); - a.href = `/i/timeline?page=device_follow&nid=${n.id}`; - document.body.appendChild(a); - a.click(); - a.remove(); - } else if (n.icon.id === "heart_icon") { - if ( - n.entry.clientEventInfo.element === "user_liked_multiple_tweets" - ) { - let a = document.createElement("a"); - a.href = `/i/timeline?page=likes&nid=${n.id}`; - document.body.appendChild(a); - a.click(); - a.remove(); - } else { - new TweetViewer( - user, - n.tweet.retweeted_status ? n.tweet.retweeted_status : n.tweet - ); - } - } else if ( - n.entry.clientEventInfo.element === "users_added_you_to_lists" - ) { - let a = document.createElement("a"); - a.href = `/i/timeline?page=lists&nid=${n.id}`; - document.body.appendChild(a); - a.click(); - a.remove(); - } else if (n.icon.id === "list_icon") { - let a = document.createElement("a"); - a.href = n.entry.content.notification.url.url; - document.body.appendChild(a); - a.click(); - a.remove(); - } else if ( - n.entry.clientEventInfo.element === "users_followed_you" || - n.entry.clientEventInfo.element === "follow_from_recommended_user" - ) { - let a = document.createElement("a"); - a.href = `/${user.screen_name}/followers`; - document.body.appendChild(a); - a.click(); - a.remove(); - } else if (n.icon.id === "milestone_icon") { - //this is such a stupid way to implement it but it works - document.getElementById("navbar-tweet-button").click(); - setTimeout(async () => { - document.getElementsByClassName("navbar-new-tweet-text")[0].value = - LOC.anniversary_tweet.message; - - let userJoinYear = new Date(user.created_at).getFullYear(); - let currentYear = new Date().getFullYear(); - let yearsSinceJoin = currentYear - userJoinYear; - let anniversaryPicUrl = `https://ton.twimg.com/ntab_public/twitterversary/year${yearsSinceJoin}.jpg`; - let anniversaryPicBlob = await ( - await fetch(anniversaryPicUrl) - ).blob(); - let anniversaryPicFile = new File( - [anniversaryPicBlob], - `year${yearsSinceJoin}.jpg`, - { type: "image/jpeg" } - ); + if (options.unread) { + notification.classList.add("notification-unread"); + } - mediaToUpload = []; - handleFiles( - [anniversaryPicFile], - mediaToUpload, - document.getElementsByClassName("navbar-new-tweet-media-c")[0] - ); - }, 10); - } else if (n.tweet && n.tweet.user) { - new TweetViewer( - user, - n.tweet.retweeted_status ? n.tweet.retweeted_status : n.tweet - ); - } else if (n.entry.content.notification.url) { - //this should always be last because most (if not all) notifications have a url for good measure - let url = new URL(n.entry.content.notification.url.url); - url.searchParams.append("newtwitter", true); - - openInNewTab(url.href); + let notificationHeader = n.message.text; + if (n.message.entities) { + let additionalLength = 0; + let matches = 0; + n.message.entities.forEach((e) => { + if (!e.ref || !e.ref.user) return; + let user = n.users[e.ref.user.id]; + notificationHeader = Array.from(notificationHeader); + notificationHeader = arrayInsert( + notificationHeader, + e.toIndex + additionalLength, + "" + ); + notificationHeader = arrayInsert( + notificationHeader, + e.fromIndex + additionalLength, + `` + ); + notificationHeader = notificationHeader.join(""); + additionalLength += ``.length; + let mi = 0; + let newText = notificationHeader.replace(aRegex, (_, m) => { + if (mi++ !== matches) return _; + return `${escapeHTML(m)}`; + }); + additionalLength += newText.length - notificationHeader.length; + notificationHeader = newText; + matches++; + }); } - } - }); - notification.addEventListener("mousedown", (e) => { - if ( - e.target.tagName === "A" || - e.target.className === "notification-avatar-img" - ) { - let url = new URL(e.target.href); - if (isProfilePath(url.pathname)) { - return; + + notification.addEventListener("click", (e) => { + if ( + e.target.closest(".notification") && + e.target.tagName !== "IMG" && + e.target.tagName !== "A" && + e.target.className !== "notification-feedback" + ) { + if (n.icon.id === "bell_icon") { + let a = document.createElement("a"); + a.href = `/i/timeline?page=device_follow&nid=${n.id}`; + document.body.appendChild(a); + a.click(); + a.remove(); + } else if (n.icon.id === "heart_icon") { + if ( + n.entry.clientEventInfo.element === + "user_liked_multiple_tweets" + ) { + let a = document.createElement("a"); + a.href = `/i/timeline?page=likes&nid=${n.id}`; + document.body.appendChild(a); + a.click(); + a.remove(); + } else { + new TweetViewer( + user, + n.tweet.retweeted_status + ? n.tweet.retweeted_status + : n.tweet + ); + } + } else if ( + n.entry.clientEventInfo.element === + "users_added_you_to_lists" + ) { + let a = document.createElement("a"); + a.href = `/i/timeline?page=lists&nid=${n.id}`; + document.body.appendChild(a); + a.click(); + a.remove(); + } else if (n.icon.id === "list_icon") { + let a = document.createElement("a"); + a.href = n.entry.content.notification.url.url; + document.body.appendChild(a); + a.click(); + a.remove(); + } else if ( + n.entry.clientEventInfo.element === "users_followed_you" || + n.entry.clientEventInfo.element === + "follow_from_recommended_user" + ) { + let a = document.createElement("a"); + a.href = `/${user.screen_name}/followers`; + document.body.appendChild(a); + a.click(); + a.remove(); + } else if (n.icon.id === "milestone_icon") { + //this is such a stupid way to implement it but it works + document.getElementById("navbar-tweet-button").click(); + setTimeout(async () => { + document.getElementsByClassName( + "navbar-new-tweet-text" + )[0].value = LOC.anniversary_tweet.message; + + let userJoinYear = new Date( + user.created_at + ).getFullYear(); + let currentYear = new Date().getFullYear(); + let yearsSinceJoin = currentYear - userJoinYear; + let anniversaryPicUrl = `https://ton.twimg.com/ntab_public/twitterversary/year${yearsSinceJoin}.jpg`; + let anniversaryPicBlob = await ( + await fetch(anniversaryPicUrl) + ).blob(); + let anniversaryPicFile = new File( + [anniversaryPicBlob], + `year${yearsSinceJoin}.jpg`, + { type: "image/jpeg" } + ); + + mediaToUpload = []; + handleFiles( + [anniversaryPicFile], + mediaToUpload, + document.getElementsByClassName( + "navbar-new-tweet-media-c" + )[0] + ); + }, 10); + } else if (n.tweet && n.tweet.user) { + new TweetViewer( + user, + n.tweet.retweeted_status + ? n.tweet.retweeted_status + : n.tweet + ); + } else if (n.entry.content.notification.url) { + //this should always be last because most (if not all) notifications have a url for good measure + let url = new URL(n.entry.content.notification.url.url); + url.searchParams.append("newtwitter", true); + + openInNewTab(url.href); + } + } + }); + notification.addEventListener("mousedown", (e) => { + if ( + e.target.tagName === "A" || + e.target.className === "notification-avatar-img" + ) { + let url = new URL(e.target.href); + if (isProfilePath(url.pathname)) { + return; + } + } + if (e.button === 1) { + e.preventDefault(); + if (n.icon.id === "bell_icon") { + openInNewTab(`/i/timeline?page=device_follow&nid=${n.id}`); + } else if (n.icon.id === "heart_icon") { + openInNewTab(`/i/timeline?page=likes&nid=${n.id}`); + } else if ( + n.entry.clientEventInfo.element === + "users_added_you_to_lists" + ) { + openInNewTab(`/i/timeline?page=lists&nid=${n.id}`); + } else if (n.icon.id === "list_icon") { + openInNewTab(n.entry.content.notification.url.url); + } else if ( + n.entry.clientEventInfo.element === "users_followed_you" + ) { + openInNewTab(`/${user.screen_name}/followers`); + } else if ( + e.target.closest(".notification") && + e.target.tagName !== "IMG" + ) { + if (n.tweet.retweeted_status) { + openInNewTab( + `/${n.tweet.retweeted_status.user.screen_name}/status/${n.tweet.retweeted_status.id_str}` + ); + } else { + openInNewTab( + `/${n.tweet.user.screen_name}/status/${n.tweet.id_str}` + ); + } + } + } + }); + for (let i in n.users) { + if (!userStorage[i]) userStorage[i] = n.users[i]; } - } - if (e.button === 1) { - e.preventDefault(); - if (n.icon.id === "bell_icon") { - openInNewTab(`/i/timeline?page=device_follow&nid=${n.id}`); - } else if (n.icon.id === "heart_icon") { - openInNewTab(`/i/timeline?page=likes&nid=${n.id}`); - } else if ( - n.entry.clientEventInfo.element === "users_added_you_to_lists" - ) { - openInNewTab(`/i/timeline?page=lists&nid=${n.id}`); - } else if (n.icon.id === "list_icon") { - openInNewTab(n.entry.content.notification.url.url); - } else if (n.entry.clientEventInfo.element === "users_followed_you") { - openInNewTab(`/${user.screen_name}/followers`); - } else if ( - e.target.closest(".notification") && - e.target.tagName !== "IMG" - ) { - if (n.tweet.retweeted_status) { - openInNewTab( - `/${n.tweet.retweeted_status.user.screen_name}/status/${n.tweet.retweeted_status.id_str}` + + let users = n.template.aggregateUserActionsV1.fromUsers.map( + (u) => n.users[u.user.id] + ); + if (n.icon.id === "recommendation_icon") { + notificationHeader = `${escapeHTML(notificationHeader)}`; + } + if (!iconClasses[n.icon.id]) { + console.log( + `Unsupported icon: "${n.icon.id}". Report it to https://github.com/dimdenGD/OldTwitter/issues` ); - } else { - openInNewTab( - `/${n.tweet.user.screen_name}/status/${n.tweet.id_str}` + } + if (n.icon.id === "heart_icon" && !vars.heartsNotStars) { + LOC.replacer_liked_to_favorited.message.split("|").forEach((el) => { + notificationHeader = notificationHeader.replace( + new RegExp(el.split("->")[0], "g"), + el.split("->")[1] + ); + }); + } + if (n.icon.id === "milestone_icon") { + notificationHeader = notificationHeader.replace( + "X anniversary", + "Twitter anniversary" ); - } } - } - }); - for (let i in n.users) { - if (!userStorage[i]) userStorage[i] = n.users[i]; - } - - let users = n.template.aggregateUserActionsV1.fromUsers.map( - (u) => n.users[u.user.id] - ); - if (n.icon.id === "recommendation_icon") { - notificationHeader = `${escapeHTML(notificationHeader)}`; - } - if (!iconClasses[n.icon.id]) { - console.log( - `Unsupported icon: "${n.icon.id}". Report it to https://github.com/dimdenGD/OldTwitter/issues` - ); - } - if (n.icon.id === "heart_icon" && !vars.heartsNotStars) { - LOC.replacer_liked_to_favorited.message.split("|").forEach((el) => { - notificationHeader = notificationHeader.replace( - new RegExp(el.split("->")[0], "g"), - el.split("->")[1] - ); - }); - } - if (n.icon.id === "milestone_icon") { - notificationHeader = notificationHeader.replace( - "X anniversary", - "Twitter anniversary" - ); - } - LOC.replacer_repost_to_retweet.message.split("|").forEach((el) => { - notificationHeader = notificationHeader.replace( - new RegExp(el.split("->")[0], "g"), - el.split("->")[1] - ); - }); - LOC.replacer_post_to_tweet.message.split("|").forEach((el) => { - notificationHeader = notificationHeader.replace( - new RegExp(el.split("->")[0], "g"), - el.split("->")[1] - ); - }); - notification.innerHTML = html` -
-
- ${notificationHeader} - ${n.feedback - ? `[${n.feedback.prompt}]` - : ""} -
-
- ${escapeHTML( - n.tweet.full_text.replace( - /^(@[\w+]{1,15}\b\s)((@[\w+]{1,15}\b\s)+)/g, - "$1" - ) - )} -
- +
+ ${users + .map( + (u) => + `${escapeHTML(
+                                u.name
+                            )}` + ) + .join("")} +
+ `; + let notifText = notification.querySelector(".notification-text"); + if (n.tweet.entities && n.tweet.entities.urls) { + for (let url of n.tweet.entities.urls) { + notifText.innerText = notifText.innerText.replace( + new RegExp(url.url, "g"), + url.display_url + ); + } + } - if (n.feedback) { - let feedbackBtn = notification.querySelector(".notification-feedback"); - feedbackBtn.addEventListener("click", () => { - fetch( - "/i/api/2/notifications/feedback.json?" + - n.feedback.feedbackUrl.split("?").slice(1).join("?"), - { - headers: { - authorization: OLDTWITTER_CONFIG.public_token, - "x-csrf-token": OLDTWITTER_CONFIG.csrf, - "content-type": "application/x-www-form-urlencoded", - "x-twitter-auth-type": "OAuth2Session", - "x-twitter-client-language": LANGUAGE || navigator.language, - "x-twitter-active-user": "yes", - }, - method: "post", - credentials: "include", - body: `feedback_type=${n.feedback.feedbackType}&feedback_metadata=${n.feedback.metadata}&undo=false`, - } - ) - .then((i) => i.text()) - .then((i) => { - notification.remove(); - let confirmation = n.feedback.confirmation; - confirmation = confirmation.replace(/\bX\b/g, LOC.twitter.message); //replace X (by itself) in a string with Twitter (wont replace X in a word but that wouldnt happen anyways) - alert(confirmation); - }); - }); - } + if (n.feedback) { + let feedbackBtn = notification.querySelector( + ".notification-feedback" + ); + feedbackBtn.addEventListener("click", () => { + fetch( + "/i/api/2/notifications/feedback.json?" + + n.feedback.feedbackUrl.split("?").slice(1).join("?"), + { + headers: { + authorization: OLDTWITTER_CONFIG.public_token, + "x-csrf-token": OLDTWITTER_CONFIG.csrf, + "content-type": "application/x-www-form-urlencoded", + "x-twitter-auth-type": "OAuth2Session", + "x-twitter-client-language": + LANGUAGE || navigator.language, + "x-twitter-active-user": "yes", + }, + method: "post", + credentials: "include", + body: `feedback_type=${n.feedback.feedbackType}&feedback_metadata=${n.feedback.metadata}&undo=false`, + } + ) + .then((i) => i.text()) + .then((i) => { + notification.remove(); + let confirmation = n.feedback.confirmation; + confirmation = confirmation.replace( + /\bX\b/g, + LOC.twitter.message + ); //replace X (by itself) in a string with Twitter (wont replace X in a word but that wouldnt happen anyways) + alert(confirmation); + }); + }); + } - if (vars.enableTwemoji) twemoji.parse(notification); + if (vars.enableTwemoji) twemoji.parse(notification); - return notification; - } catch (e) { - console.error(e); - if (Date.now() - lastNotificationErrorDate > 1000) { - lastNotificationErrorDate = Date.now(); - createModal(` + return notification; + } catch (e) { + console.error(e); + if (Date.now() - lastNotificationErrorDate > 1000) { + lastNotificationErrorDate = Date.now(); + createModal(`

${ - LOC.something_went_wrong.message + LOC.something_went_wrong.message }

${LOC.notifications_error.message}
${LOC.error_instructions.message - .replace( - "$AT1$", - "" - ) - .replace(/\$AT2\$/g, "") - .replace( - "$AT3$", - "" - )} + .replace( + "$AT1$", + "" + ) + .replace(/\$AT2\$/g, "") + .replace( + "$AT3$", + "" + )}
${escapeHTML( - e.stack ? e.stack : String(e) + e.stack ? e.stack : String(e) )} (OldTwitter v${chrome.runtime.getManifest().version})
`); + } + return null; } - return null; - } } function replaceAll(str, find, replace) { - return str.split(find).join(replace); + return str.split(find).join(replace); } diff --git a/scripts/iframeNavigation.js b/scripts/iframeNavigation.js index f4fd1050..1623c150 100644 --- a/scripts/iframeNavigation.js +++ b/scripts/iframeNavigation.js @@ -1,185 +1,192 @@ // i don't understand how this file works anymore please send help function getFrameDepth(winToID) { - if (winToID === window.top) { - return 0; - } else if (winToID.parent === window.top) { - return 1; - } + if (winToID === window.top) { + return 0; + } else if (winToID.parent === window.top) { + return 1; + } - return 1 + getFrameDepth(winToID.parent); + return 1 + getFrameDepth(winToID.parent); } if (!window.top.windows && window.top === window) { - window.top.windows = []; - window.top._title = document.title; - setTimeout(() => { + window.top.windows = []; window.top._title = document.title; - }, 1000); - setInterval(() => { - let iframe = document.getElementsByClassName("iframe-navigation")[0]; - if ( - window.top.windows - .slice(1) - .some((w) => w.location.pathname === "/home") || - (iframe && iframe.src === "/home") - ) { - document.body.style.overflowY = - window.previousOverflow && window.previousOverflow !== "hidden" - ? window.previousOverflow - : "auto"; - window.top.windows = [window]; - iframe.remove(); - window.focus(); - - window.top.document.title = window.top._title; - } - if (!iframe) { - window.top._title = document.title; - } - }, 250); + setTimeout(() => { + window.top._title = document.title; + }, 1000); + setInterval(() => { + let iframe = document.getElementsByClassName("iframe-navigation")[0]; + if ( + window.top.windows + .slice(1) + .some((w) => w.location.pathname === "/home") || + (iframe && iframe.src === "/home") + ) { + document.body.style.overflowY = + window.previousOverflow && window.previousOverflow !== "hidden" + ? window.previousOverflow + : "auto"; + window.top.windows = [window]; + iframe.remove(); + window.focus(); + + window.top.document.title = window.top._title; + } + if (!iframe) { + window.top._title = document.title; + } + }, 250); } if (!window.top.windows.includes(window)) window.top.windows.push(window); if (!window._realPath) window._realPath = location.pathname; if (window.top !== window && location.protocol == "https:") { - setTimeout(() => { - window.top.document.title = document.title; - }, 1000); + setTimeout(() => { + window.top.document.title = document.title; + }, 1000); } window.addEventListener("unload", () => { - window.top.windows = window.top.windows.filter((w) => w !== window); + window.top.windows = window.top.windows.filter((w) => w !== window); }); let lastNavigation = 0; function useIframeNavigation(e) { - if (e.defaultPrevented) return; - if (typeof vars === "undefined") return; - if (!vars.enableIframeNavigation) return; - // because of dumb mozilla's policies need to turn off this feature - // (they don't allow changing security headers so cant modify x-frame-options) - if (navigator.userAgent.toLowerCase().includes("firefox")) return; - - let a = e.target.closest("a"); - if ( - !a || - !a.href || - a.href.startsWith("#") || - a.href.startsWith("javascript:") || - a.href.startsWith("blob:") - ) - return; - if (a.href.startsWith("http") && !a.href.startsWith(location.origin)) return; - - let depth = getFrameDepth(window); - if (depth > 3) return; - - let parsedURL = new URL(a.href); - let windowExists = window.top.windows.findIndex( - (w) => w._realPath === parsedURL.pathname - ); - - if (windowExists !== -1) { - let we = window.top.windows[windowExists]; - let iframe = we.document.getElementsByClassName("iframe-navigation")[0]; - window.top.windows = window.top.windows.slice(0, windowExists + 1); - we.document.body.style.overflowY = - we.previousOverflow && we.previousOverflow !== "hidden" - ? we.previousOverflow - : "auto"; - we.focus(); - - window.top.document.title = we.document.title; - if (window.top.windows.length === 1) - window.top.document.title = window.top._title; - - if (location.href !== a.href) - window.top.history.pushState(null, null, a.href); - if (iframe) iframe.remove(); - return; - } - - if (window.top._realPath === "/" || window.top._realPath === "/home") { - e.preventDefault(); - e.stopImmediatePropagation(); - - window.previousOverflow = document.body.style.overflowY; - document.body.style.overflowY = "hidden"; - - if (location.href !== a.href) - window.top.history.pushState(null, null, a.href); - - let iframe = document.createElement("iframe"); - iframe.classList.add("iframe-navigation"); - iframe.src = a.href; - iframe.style = ` - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - border: none; - z-index: 99999; - background-color: var(--background-color); - `; - document.body.appendChild(iframe); - iframe.focus(); - } - - window.addEventListener("popstate", () => { + if (e.defaultPrevented) return; + if (typeof vars === "undefined") return; + if (!vars.enableIframeNavigation) return; + // because of dumb mozilla's policies need to turn off this feature + // (they don't allow changing security headers so cant modify x-frame-options) + if (navigator.userAgent.toLowerCase().includes("firefox")) return; + + let a = e.target.closest("a"); + if ( + !a || + !a.href || + a.href.startsWith("#") || + a.href.startsWith("javascript:") || + a.href.startsWith("blob:") + ) + return; + if (a.href.startsWith("http") && !a.href.startsWith(location.origin)) + return; + + let depth = getFrameDepth(window); + if (depth > 3) return; + + let parsedURL = new URL(a.href); let windowExists = window.top.windows.findIndex( - (w) => w._realPath === location.pathname + (w) => w._realPath === parsedURL.pathname ); if (windowExists !== -1) { - let we = window.top.windows[windowExists]; - let iframe = we.document.getElementsByClassName("iframe-navigation")[0]; - window.top.windows = window.top.windows.slice(0, windowExists + 1); - we.document.body.style.overflowY = - we.previousOverflow && we.previousOverflow !== "hidden" - ? we.previousOverflow - : "auto"; - we.focus(); - - window.top.document.title = we.document.title; - if (window.top.windows.length === 1) - window.top.document.title = window.top._title; - - if (iframe) iframe.remove(); - } else { - if ( - location.pathname === "/notifications" || - location.pathname.includes("/status/") - ) { - let we = window.top.windows[Math.max(window.top.windows.length - 2, 0)]; + let we = window.top.windows[windowExists]; let iframe = we.document.getElementsByClassName("iframe-navigation")[0]; - window.top.windows = window.top.windows.slice( - 0, - window.top.windows.length - 1 - ); - if (window.top.windows.length === 0) window.top.windows = [window]; + window.top.windows = window.top.windows.slice(0, windowExists + 1); we.document.body.style.overflowY = - we.previousOverflow && we.previousOverflow !== "hidden" - ? we.previousOverflow - : "auto"; + we.previousOverflow && we.previousOverflow !== "hidden" + ? we.previousOverflow + : "auto"; we.focus(); window.top.document.title = we.document.title; if (window.top.windows.length === 1) - window.top.document.title = window.top._title; + window.top.document.title = window.top._title; + if (location.href !== a.href) + window.top.history.pushState(null, null, a.href); if (iframe) iframe.remove(); - } else { - if (Date.now() - lastNavigation < 20) return; - lastNavigation = Date.now(); - useIframeNavigation({ - target: { closest: () => ({ href: location.href }) }, - preventDefault: () => {}, - stopImmediatePropagation: () => {}, - }); - } + return; } - }); + + if (window.top._realPath === "/" || window.top._realPath === "/home") { + e.preventDefault(); + e.stopImmediatePropagation(); + + window.previousOverflow = document.body.style.overflowY; + document.body.style.overflowY = "hidden"; + + if (location.href !== a.href) + window.top.history.pushState(null, null, a.href); + + let iframe = document.createElement("iframe"); + iframe.classList.add("iframe-navigation"); + iframe.src = a.href; + iframe.style = ` + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + border: none; + z-index: 99999; + background-color: var(--background-color); + `; + document.body.appendChild(iframe); + iframe.focus(); + } + + window.addEventListener("popstate", () => { + let windowExists = window.top.windows.findIndex( + (w) => w._realPath === location.pathname + ); + + if (windowExists !== -1) { + let we = window.top.windows[windowExists]; + let iframe = + we.document.getElementsByClassName("iframe-navigation")[0]; + window.top.windows = window.top.windows.slice(0, windowExists + 1); + we.document.body.style.overflowY = + we.previousOverflow && we.previousOverflow !== "hidden" + ? we.previousOverflow + : "auto"; + we.focus(); + + window.top.document.title = we.document.title; + if (window.top.windows.length === 1) + window.top.document.title = window.top._title; + + if (iframe) iframe.remove(); + } else { + if ( + location.pathname === "/notifications" || + location.pathname.includes("/status/") + ) { + let we = + window.top.windows[ + Math.max(window.top.windows.length - 2, 0) + ]; + let iframe = + we.document.getElementsByClassName("iframe-navigation")[0]; + window.top.windows = window.top.windows.slice( + 0, + window.top.windows.length - 1 + ); + if (window.top.windows.length === 0) + window.top.windows = [window]; + we.document.body.style.overflowY = + we.previousOverflow && we.previousOverflow !== "hidden" + ? we.previousOverflow + : "auto"; + we.focus(); + + window.top.document.title = we.document.title; + if (window.top.windows.length === 1) + window.top.document.title = window.top._title; + + if (iframe) iframe.remove(); + } else { + if (Date.now() - lastNavigation < 20) return; + lastNavigation = Date.now(); + useIframeNavigation({ + target: { closest: () => ({ href: location.href }) }, + preventDefault: () => {}, + stopImmediatePropagation: () => {}, + }); + } + } + }); } document.addEventListener("click", useIframeNavigation); diff --git a/scripts/injection.js b/scripts/injection.js index e5660a12..378ebb3e 100644 --- a/scripts/injection.js +++ b/scripts/injection.js @@ -1,461 +1,466 @@ let pages = [ - { - name: "home", - paths: ["/", "/home"], - activeMenu: "home", - }, - { - name: "notifications", - paths: ["/notifications", "/notifications/mentions"], - activeMenu: "notifications", - }, - { - name: "settings", - paths: ["/old/settings"], - }, - { - name: "search", - paths: ["/search"], - }, - { - name: "bookmarks", - paths: ["/i/bookmarks"], - activeMenu: "pin-bookmarks", - }, - { - name: "lists", - paths: [/^\/i\/lists\/\d+(\/members|\/followers|)$/], - }, - { - name: "topics", - paths: [/^\/i\/topics\/\d+$/], - }, - { - name: "history", - paths: ["/old/history"], - }, - { - name: "itl", - paths: ["/i/timeline"], - }, - { - name: "tweet", - paths: [ - /^\/[A-z-0-9-_]{1,15}\/status\/\d{2,32}(|\/likes|\/retweets|\/retweets\/with_comments|)$/, - ], - }, - { - name: "profile", - paths: [ - /^\/[A-z-0-9-_]{1,15}(\/with_replies|\/media|\/likes|\/following|\/followers|\/followers_you_follow|\/lists|)$/g, - ], - exclude: [ - "/home", - "/notifications", - "/messages", - "/settings", - "/explore", - "/login", - "/register", - "/logout", - "/search", - "/search-advanced", - ], - }, - { - name: "unfollows", - paths: ["/old/unfollows/followers", "/old/unfollows/following"], - }, + { + name: "home", + paths: ["/", "/home"], + activeMenu: "home", + }, + { + name: "notifications", + paths: ["/notifications", "/notifications/mentions"], + activeMenu: "notifications", + }, + { + name: "settings", + paths: ["/old/settings"], + }, + { + name: "search", + paths: ["/search"], + }, + { + name: "bookmarks", + paths: ["/i/bookmarks"], + activeMenu: "pin-bookmarks", + }, + { + name: "lists", + paths: [/^\/i\/lists\/\d+(\/members|\/followers|)$/], + }, + { + name: "topics", + paths: [/^\/i\/topics\/\d+$/], + }, + { + name: "history", + paths: ["/old/history"], + }, + { + name: "itl", + paths: ["/i/timeline"], + }, + { + name: "tweet", + paths: [ + /^\/[A-z-0-9-_]{1,15}\/status\/\d{2,32}(|\/likes|\/retweets|\/retweets\/with_comments|)$/, + ], + }, + { + name: "profile", + paths: [ + /^\/[A-z-0-9-_]{1,15}(\/with_replies|\/media|\/likes|\/following|\/followers|\/followers_you_follow|\/lists|)$/g, + ], + exclude: [ + "/home", + "/notifications", + "/messages", + "/settings", + "/explore", + "/login", + "/register", + "/logout", + "/search", + "/search-advanced", + ], + }, + { + name: "unfollows", + paths: ["/old/unfollows/followers", "/old/unfollows/following"], + }, ]; let realPath = location.pathname; let hrefUrl = location.href; if (realPath.endsWith("/") && realPath !== "/") { - location.replace(realPath.slice(0, -1)); + location.replace(realPath.slice(0, -1)); } if (location.hash.startsWith("#!/")) { - location.replace(location.hash.slice(2)); + location.replace(location.hash.slice(2)); } if (realPath === "/") { - location.replace("/home"); + location.replace("/home"); } if (realPath.startsWith("/i/user/")) { - let id = realPath.split("/i/user/")[1]; - if (id.endsWith("/")) id = id.slice(0, -1); - API.user.get(id, true).then((user) => { - if (user.error) { - return; - } - location.replace("/" + user.screen_name); - }); + let id = realPath.split("/i/user/")[1]; + if (id.endsWith("/")) id = id.slice(0, -1); + API.user.get(id, true).then((user) => { + if (user.error) { + return; + } + location.replace("/" + user.screen_name); + }); } if (realPath === "/intent/user") { - let id = location.search.split("user_id=")[1]; - API.user.get(id, true).then((user) => { - if (user.error) { - return; - } - location.replace("/" + user.screen_name); - }); + let id = location.search.split("user_id=")[1]; + API.user.get(id, true).then((user) => { + if (user.error) { + return; + } + location.replace("/" + user.screen_name); + }); } if (realPath.startsWith("/i/web/status/")) { - let id = location.pathname.split("/i/web/status/")[1]; - API.tweet.getV2(id).then((tweet) => { - if (tweet.error) { - return; - } - location.replace("/" + tweet.user.screen_name + "/status/" + tweet.id_str); - }); + let id = location.pathname.split("/i/web/status/")[1]; + API.tweet.getV2(id).then((tweet) => { + if (tweet.error) { + return; + } + location.replace( + "/" + tweet.user.screen_name + "/status/" + tweet.id_str + ); + }); } if (realPath.startsWith("/i/redirect")) { - let search = new URLSearchParams(location.search); - let url = search.get("url"); - location.replace(url); + let search = new URLSearchParams(location.search); + let url = search.get("url"); + location.replace(url); } if (/^\/direct_messages\/create\/[A-z-0-9-_]{1,15}$/.test(realPath)) { - location.href = `/${realPath.split("/direct_messages/create/")[1]}#dm`; + location.href = `/${realPath.split("/direct_messages/create/")[1]}#dm`; } if (/^\/hashtag\/(.*?)/.test(realPath)) { - let hashtag = realPath.split("/hashtag/").slice(1).join("/hashtag/"); - location.replace(`/search?q=%23${hashtag}`); + let hashtag = realPath.split("/hashtag/").slice(1).join("/hashtag/"); + location.replace(`/search?q=%23${hashtag}`); } if ( - /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/(photo|video)\/\d+$/.test(realPath) + /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/(photo|video)\/\d+$/.test(realPath) ) { - let path = realPath.split("/photo/")[0].split("/video/")[0]; - location.replace(path); + let path = realPath.split("/photo/")[0].split("/video/")[0]; + location.replace(path); } if (realPath === "/messages") { - location.replace("/home#dm"); + location.replace("/home#dm"); } if (realPath === "/compose/tweet") { - location.replace("/home"); + location.replace("/home"); } if (realPath === "/intent/tweet" || realPath === "/share") { - location.replace("/home#" + location.search); + location.replace("/home#" + location.search); } if (realPath === "/intent/follow") { - let screen_name = location.search.split("screen_name=")[1].split("&")[0]; - location.replace(`/${screen_name}`); + let screen_name = location.search.split("screen_name=")[1].split("&")[0]; + location.replace(`/${screen_name}`); } if ( - !hrefUrl.includes("newtwitter=true") && - (realPath.startsWith("/i/flow/") || - realPath.startsWith("/i/premium_sign_up/") || - realPath.startsWith("/i/events/") || - realPath.startsWith("/i/spaces/") || - realPath.startsWith("/i/oauth2") || - realPath.startsWith("/account") || - realPath.startsWith("/settings") || - /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/analytics$/.test(realPath)) + !hrefUrl.includes("newtwitter=true") && + (realPath.startsWith("/i/flow/") || + realPath.startsWith("/i/premium_sign_up/") || + realPath.startsWith("/i/events/") || + realPath.startsWith("/i/spaces/") || + realPath.startsWith("/i/oauth2") || + realPath.startsWith("/account") || + realPath.startsWith("/settings") || + /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/analytics$/.test(realPath)) ) { - let url = new URL(hrefUrl); - url.searchParams.set("newtwitter", "true"); - location.replace(url); + let url = new URL(hrefUrl); + url.searchParams.set("newtwitter", "true"); + location.replace(url); } const LANGUAGES = [ - "en", - "ru", - "uk", - "fr", - "pt_BR", - "es", - "el", - "ro", - "tl", - "lv", - "he", - "ne", - "nl", - "ja", - "tr", - "it", - "ar", - "th", - "ko", - "pl", - "vi", - "zh_CN", - "zh_TW", - "cs", - "de", - "ca", - "sv", - "bg", - "nb", - "fi", - "id", - "is", - "gl", + "en", + "ru", + "uk", + "fr", + "pt_BR", + "es", + "el", + "ro", + "tl", + "lv", + "he", + "ne", + "nl", + "ja", + "tr", + "it", + "ar", + "th", + "ko", + "pl", + "vi", + "zh_CN", + "zh_TW", + "cs", + "de", + "ca", + "sv", + "bg", + "nb", + "fi", + "id", + "is", + "gl", ]; const TRANSLATORS = { - ru: ["dimden", "https://dimden.dev/"], - uk: ["dimden", "https://dimden.dev/"], - fr: [ - ["Aurore C.", "https://asuure.com/"], - ["zdimension", "/zdimension_"], - ["Pikatchoum", "/applitom45"], - ["adriend", "/_adriend_"], - ["celestial04_", "/celestial04_"], - ], - pt_BR: [ - ["kigi", "/kigidere"], - ["guko", "/gukodev"], - ["prophamoon", "/prophamoon"], - ], - es: [ - ["Ruchi", "/anbulansia"], - ["gaelcoral", "/gaelcoral"], - ["hue", "/huey1116"], - ["elderreka", "/elderreka"], - ], - el: ["VasilisTheChu", "https://pikachu.systems/"], - ro: [ - ["Skyrina", "https://skyrina.dev/"], - ["AlexSem", "/AlexSem5399"], - ], - tl: ["Eurasian", "/NotPROxV"], - lv: ["sophie", "https://sad.ovh/"], - he: [["ugh"], ["kriterin", "/kriterin"]], - ne: ["DimeDead", "https://dimedead.neocities.org/"], - nl: ["Puka1611"], - ja: [ - ["Chazuru", "/AIWMD"], - ["Nyankodasu", "https://github.com/Nyankodasu"], - ], - tr: [ - ["KayrabCebll", "https://steamcommunity.com/id/KayrabCebll"], - ["YordemEren", "/YordemEren"], - ], - it: [ - ["krek", "/CactusInc420"], - ["Francesco", "/FrancescoRosi27"], - ], - ar: ["Yours Truly,", "/schrotheneko"], - th: ["0.21%BloodAlcohol", "https://github.com/Silberweich"], - ko: [["Nyankodasu", "https://github.com/Nyankodasu"], ["HY"]], - pl: [["lele"], ["nomi", "/nomisigns"]], - vi: ["btmxh", "https://github.com/btmxh"], - zh_CN: [ - ["am1006", "https://github.com/am1006"], - ["CarimoWuling", "/carimowuling"], - ], - zh_TW: [ - ["olivertzeng", "/olivertzeng"], - ["cirx1e", "https://github.com/cirx1e"], - ], - cs: ["Menal"], - de: ["basti564", "/basti564"], - ca: [ - ["elmees21", "/elmees21"], - ["Luna", "/chica_botella"], - ], - sv: ["actuallyaridan", "/actuallyaridan"], - bg: ["Scarlett7447", "/Scarlett7447"], - nb: ["twistquest", "/twistquest"], - fi: ["scepixel.fla", "https://caenogo.nl/pages/pxww"], - id: [ - ["lorizade", "/lorizade"], - ["Feerse_", "/Feerse_"], - ["DaGamerFiles", "/DaGamerFiles"], - ["KuchingNeko", "/KuchingNeko"], - ], - is: ["VipelyRS", "/VipelyRS"], - gl: [ - ["ikergcalvino", "https://github.com/ikergcalvino"], - ["alvaroddiaz", "https://github.com/alvaroddiaz"], - ], + ru: ["dimden", "https://dimden.dev/"], + uk: ["dimden", "https://dimden.dev/"], + fr: [ + ["Aurore C.", "https://asuure.com/"], + ["zdimension", "/zdimension_"], + ["Pikatchoum", "/applitom45"], + ["adriend", "/_adriend_"], + ["celestial04_", "/celestial04_"], + ], + pt_BR: [ + ["kigi", "/kigidere"], + ["guko", "/gukodev"], + ["prophamoon", "/prophamoon"], + ], + es: [ + ["Ruchi", "/anbulansia"], + ["gaelcoral", "/gaelcoral"], + ["hue", "/huey1116"], + ["elderreka", "/elderreka"], + ], + el: ["VasilisTheChu", "https://pikachu.systems/"], + ro: [ + ["Skyrina", "https://skyrina.dev/"], + ["AlexSem", "/AlexSem5399"], + ], + tl: ["Eurasian", "/NotPROxV"], + lv: ["sophie", "https://sad.ovh/"], + he: [["ugh"], ["kriterin", "/kriterin"]], + ne: ["DimeDead", "https://dimedead.neocities.org/"], + nl: ["Puka1611"], + ja: [ + ["Chazuru", "/AIWMD"], + ["Nyankodasu", "https://github.com/Nyankodasu"], + ], + tr: [ + ["KayrabCebll", "https://steamcommunity.com/id/KayrabCebll"], + ["YordemEren", "/YordemEren"], + ], + it: [ + ["krek", "/CactusInc420"], + ["Francesco", "/FrancescoRosi27"], + ], + ar: ["Yours Truly,", "/schrotheneko"], + th: ["0.21%BloodAlcohol", "https://github.com/Silberweich"], + ko: [["Nyankodasu", "https://github.com/Nyankodasu"], ["HY"]], + pl: [["lele"], ["nomi", "/nomisigns"]], + vi: ["btmxh", "https://github.com/btmxh"], + zh_CN: [ + ["am1006", "https://github.com/am1006"], + ["CarimoWuling", "/carimowuling"], + ], + zh_TW: [ + ["olivertzeng", "/olivertzeng"], + ["cirx1e", "https://github.com/cirx1e"], + ], + cs: ["Menal"], + de: ["basti564", "/basti564"], + ca: [ + ["elmees21", "/elmees21"], + ["Luna", "/chica_botella"], + ], + sv: ["actuallyaridan", "/actuallyaridan"], + bg: ["Scarlett7447", "/Scarlett7447"], + nb: ["twistquest", "/twistquest"], + fi: ["scepixel.fla", "https://caenogo.nl/pages/pxww"], + id: [ + ["lorizade", "/lorizade"], + ["Feerse_", "/Feerse_"], + ["DaGamerFiles", "/DaGamerFiles"], + ["KuchingNeko", "/KuchingNeko"], + ], + is: ["VipelyRS", "/VipelyRS"], + gl: [ + ["ikergcalvino", "https://github.com/ikergcalvino"], + ["alvaroddiaz", "https://github.com/alvaroddiaz"], + ], }; let LOC = {}; let LOC_EN = {}; if (!LANGUAGES.includes(LANGUAGE)) { - LANGUAGE = LANGUAGE.split("_")[0]; - if (!LANGUAGES.includes(LANGUAGE)) { - LANGUAGE = "en"; - } + LANGUAGE = LANGUAGE.split("_")[0]; + if (!LANGUAGES.includes(LANGUAGE)) { + LANGUAGE = "en"; + } } function isDark() { - if (vars.systemDarkMode) { - return window.matchMedia("(prefers-color-scheme: dark)").matches; - } - let date = new Date(); - let hours = date.getHours(); - return hours < 9 || hours >= 19; + if (vars.systemDarkMode) { + return window.matchMedia("(prefers-color-scheme: dark)").matches; + } + let date = new Date(); + let hours = date.getHours(); + return hours < 9 || hours >= 19; } let customCSS, - profileCSS = false; + profileCSS = false; function uuidV4() { - const uuid = new Array(36); - for (let i = 0; i < 36; i++) { - uuid[i] = Math.floor(Math.random() * 16); - } - uuid[14] = 4; // set bits 12-15 of time-high-and-version to 0100 - uuid[19] = uuid[19] &= ~(1 << 2); // set bit 6 of clock-seq-and-reserved to zero - uuid[19] = uuid[19] |= 1 << 3; // set bit 7 of clock-seq-and-reserved to one - uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-"; - return uuid.map((x) => x.toString(16)).join(""); + const uuid = new Array(36); + for (let i = 0; i < 36; i++) { + uuid[i] = Math.floor(Math.random() * 16); + } + uuid[14] = 4; // set bits 12-15 of time-high-and-version to 0100 + uuid[19] = uuid[19] &= ~(1 << 2); // set bit 6 of clock-seq-and-reserved to zero + uuid[19] = uuid[19] |= 1 << 3; // set bit 7 of clock-seq-and-reserved to one + uuid[8] = uuid[13] = uuid[18] = uuid[23] = "-"; + return uuid.map((x) => x.toString(16)).join(""); } async function readCryptoKey() { - return new Promise((resolve, reject) => { - let request = indexedDB.open("localforage"); + return new Promise((resolve, reject) => { + let request = indexedDB.open("localforage"); - request.onerror = function (event) { - reject(event); - }; + request.onerror = function (event) { + reject(event); + }; - request.onsuccess = function (event) { - const db = event.target.result; - - // Open a transaction to access the keyvaluepairs object store - if (db.objectStoreNames.contains("keyvaluepairs")) { - const transaction = db.transaction(["keyvaluepairs"], "readonly"); - const objectStore = transaction.objectStore("keyvaluepairs"); - - objectStore.openCursor().onsuccess = function (event) { - const cursor = event.target.result; - if (cursor) { - // Check if the key matches the pattern - const key = cursor.key; - if (key.startsWith("device:rweb.dmCryptoKeys")) { - resolve(cursor.value); + request.onsuccess = function (event) { + const db = event.target.result; + + // Open a transaction to access the keyvaluepairs object store + if (db.objectStoreNames.contains("keyvaluepairs")) { + const transaction = db.transaction( + ["keyvaluepairs"], + "readonly" + ); + const objectStore = transaction.objectStore("keyvaluepairs"); + + objectStore.openCursor().onsuccess = function (event) { + const cursor = event.target.result; + if (cursor) { + // Check if the key matches the pattern + const key = cursor.key; + if (key.startsWith("device:rweb.dmCryptoKeys")) { + resolve(cursor.value); + } + + // Move to the next entry + cursor.continue(); + } else { + // No more entries, reject the promise + reject("No key found"); + } + }; + } else { + reject("No key found"); } - - // Move to the next entry - cursor.continue(); - } else { - // No more entries, reject the promise - reject("No key found"); - } }; - } else { - reject("No key found"); - } - }; - }); + }); } async function openDatabase() { - return new Promise((resolve, reject) => { - let request = indexedDB.open("CustomCSSDatabase", 1); + return new Promise((resolve, reject) => { + let request = indexedDB.open("CustomCSSDatabase", 1); - request.onerror = function (event) { - reject("Database error: " + event.target.errorCode); - }; + request.onerror = function (event) { + reject("Database error: " + event.target.errorCode); + }; - request.onsuccess = function (event) { - resolve(event.target.result); - }; + request.onsuccess = function (event) { + resolve(event.target.result); + }; - // Only runs if the database was just created (like on first run) - request.onupgradeneeded = function (event) { - let db = event.target.result; - db.createObjectStore("cssStore", { keyPath: "id" }); - }; - }); + // Only runs if the database was just created (like on first run) + request.onupgradeneeded = function (event) { + let db = event.target.result; + db.createObjectStore("cssStore", { keyPath: "id" }); + }; + }); } async function readCSSFromDB() { - let db = await openDatabase(); - return new Promise((resolve, reject) => { - let transaction = db.transaction(["cssStore"]); - let objectStore = transaction.objectStore("cssStore"); - let request = objectStore.get("customCSS"); - - request.onerror = function (event) { - reject("Error reading CSS"); - }; + let db = await openDatabase(); + return new Promise((resolve, reject) => { + let transaction = db.transaction(["cssStore"]); + let objectStore = transaction.objectStore("cssStore"); + let request = objectStore.get("customCSS"); + + request.onerror = function (event) { + reject("Error reading CSS"); + }; - request.onsuccess = function (event) { - if (request.result) { - resolve(request.result.css); - } else { - resolve(""); - } - }; - }); + request.onsuccess = function (event) { + if (request.result) { + resolve(request.result.css); + } else { + resolve(""); + } + }; + }); } async function writeCSSToDB(cssData) { - let db = await openDatabase(); - return new Promise((resolve, reject) => { - let transaction = db.transaction(["cssStore"], "readwrite"); - let store = transaction.objectStore("cssStore"); - let request = store.put({ id: "customCSS", css: cssData }); - - request.onerror = function (event) { - reject("Error writing CSS to DB"); - }; + let db = await openDatabase(); + return new Promise((resolve, reject) => { + let transaction = db.transaction(["cssStore"], "readwrite"); + let store = transaction.objectStore("cssStore"); + let request = store.put({ id: "customCSS", css: cssData }); + + request.onerror = function (event) { + reject("Error writing CSS to DB"); + }; - request.onsuccess = function (event) { - resolve(); - }; - }); + request.onsuccess = function (event) { + resolve(); + }; + }); } async function updateCustomCSS() { - let data = await new Promise((resolve) => { - chrome.storage.sync.get(["customCSS"], (data) => { - resolve(data); + let data = await new Promise((resolve) => { + chrome.storage.sync.get(["customCSS"], (data) => { + resolve(data); + }); }); - }); - if (data.customCSS) { - writeCSSToDB(data.customCSS); - chrome.storage.sync.remove("customCSS"); - } - data.customCSS = await readCSSFromDB(); + if (data.customCSS) { + writeCSSToDB(data.customCSS); + chrome.storage.sync.remove("customCSS"); + } + data.customCSS = await readCSSFromDB(); - if (profileCSS) return; + if (profileCSS) return; - if (customCSS) customCSS.remove(); + if (customCSS) customCSS.remove(); - customCSS = document.createElement("style"); - customCSS.id = "oldtwitter-custom-css"; - customCSS.innerHTML = data.customCSS; + customCSS = document.createElement("style"); + customCSS.id = "oldtwitter-custom-css"; + customCSS.innerHTML = data.customCSS; - if (document.head) { - document.head.appendChild(customCSS); - } else { - let int = setInterval(() => { - if (document.head) { - clearInterval(int); + if (document.head) { document.head.appendChild(customCSS); - } - }, 100); - } + } else { + let int = setInterval(() => { + if (document.head) { + clearInterval(int); + document.head.appendChild(customCSS); + } + }, 100); + } } async function updateCustomCSSVariables() { - let root = document.querySelector(":root"); - let data = await new Promise((resolve) => { - chrome.storage.sync.get(["customCSSVariables"], (data) => { - resolve(data); + let root = document.querySelector(":root"); + let data = await new Promise((resolve) => { + chrome.storage.sync.get(["customCSSVariables"], (data) => { + resolve(data); + }); }); - }); - root.style.setProperty("--font", vars.font); - root.style.setProperty("--tweet-font", vars.tweetFont); - if (vars.modernUI) { - root.style.setProperty("--icon-font", `"edgeicons", "RosettaIcons"`); - } - if (data.customCSSVariables) { - let csv = parseVariables(data.customCSSVariables); - for (let i in csv) { - root.style.setProperty(i, csv[i]); + root.style.setProperty("--font", vars.font); + root.style.setProperty("--tweet-font", vars.tweetFont); + if (vars.modernUI) { + root.style.setProperty("--icon-font", `"edgeicons", "RosettaIcons"`); + } + if (data.customCSSVariables) { + let csv = parseVariables(data.customCSSVariables); + for (let i in csv) { + root.style.setProperty(i, csv[i]); + } } - } } function getThemeVariables(enabled) { - let theme; - if (enabled) { - if (vars.pitchBlack) { - // Pitch black theme - theme = ` + let theme; + if (enabled) { + if (vars.pitchBlack) { + // Pitch black theme + theme = ` --background-color: #000000; --dark-background-color: #000000; --darker-background-color: #000000; @@ -473,22 +478,22 @@ function getThemeVariables(enabled) { --list-actions-bg: #19212b; --menu-bg: rgb(16 19 22 / 98%); `; - } else { - // Dark theme - theme = ` + } else { + // Dark theme + theme = ` --background-color: #1b2836; --dark-background-color: #171f2a; --darker-background-color: #141d26; --almost-black: #d4e3ed; `; - if (vars.modernUI) { - //2018 Style - theme += `--border: #141d26;`; - } else { - //2015 Style - theme += `--border: #2c3c52;`; - } - theme += ` + if (vars.modernUI) { + //2018 Style + theme += `--border: #141d26;`; + } else { + //2015 Style + theme += `--border: #2c3c52;`; + } + theme += ` --darker-gray: #c9c9c9; --lil-darker-gray: #8394a1; --light-gray: #8394a1; @@ -501,21 +506,21 @@ function getThemeVariables(enabled) { --list-actions-bg: #19212b; --menu-bg: rgba(34,46,60,0.98); `; - } - } else { - // Light theme - theme = ` + } + } else { + // Light theme + theme = ` --background-color: white; --dark-background-color: #f5f8fa; `; - if (vars.modernUI) { - //2018 Style - theme += `--darker-background-color: #e1e8ed;`; - } else { - //2015 Style - theme += `--darker-background-color: #f5f8fa;`; - } - theme += ` + if (vars.modernUI) { + //2018 Style + theme += `--darker-background-color: #e1e8ed;`; + } else { + //2015 Style + theme += `--darker-background-color: #f5f8fa;`; + } + theme += ` --almost-black: #292f33; --border: #e1e8ed; --darker-gray: #66757f; @@ -530,353 +535,358 @@ function getThemeVariables(enabled) { --list-actions-bg: #efefef; --menu-bg: rgba(255,255,255,0.98); `; - } + } - return theme; + return theme; } function parseVariables(vars) { - let obj = {}; - let styles = vars - .split("\n") - .map((i) => i.trim()) - .filter((i) => i) - .map((i) => i.split(":")) - .filter((i) => i.length === 2); - styles.forEach((style) => { - if (style[1].endsWith(";")) style[1] = style[1].slice(0, -1); - obj[style[0].trim()] = style[1].trim(); - }); - return obj; + let obj = {}; + let styles = vars + .split("\n") + .map((i) => i.trim()) + .filter((i) => i) + .map((i) => i.split(":")) + .filter((i) => i.length === 2); + styles.forEach((style) => { + if (style[1].endsWith(";")) style[1] = style[1].slice(0, -1); + obj[style[0].trim()] = style[1].trim(); + }); + return obj; } async function switchDarkMode(enabled) { - let root = document.querySelector(":root"); - let theme = getThemeVariables(enabled); - let themeVars = parseVariables(theme); - for (let i in themeVars) { - root.style.setProperty(i, themeVars[i]); - } - await updateCustomCSSVariables(); - - if (document.body) { - document.body.classList.toggle("body-dark", enabled); - document.body.classList.toggle( - "body-pitch-black", - enabled && vars.pitchBlack - ); - document.body.classList.toggle("body-light", !enabled); - } else { - let int = setInterval(() => { - if (document.body) { - clearInterval(int); + let root = document.querySelector(":root"); + let theme = getThemeVariables(enabled); + let themeVars = parseVariables(theme); + for (let i in themeVars) { + root.style.setProperty(i, themeVars[i]); + } + await updateCustomCSSVariables(); + + if (document.body) { document.body.classList.toggle("body-dark", enabled); document.body.classList.toggle( - "body-pitch-black", - enabled && vars.pitchBlack + "body-pitch-black", + enabled && vars.pitchBlack ); document.body.classList.toggle("body-light", !enabled); - } - }, 100); - } + } else { + let int = setInterval(() => { + if (document.body) { + clearInterval(int); + document.body.classList.toggle("body-dark", enabled); + document.body.classList.toggle( + "body-pitch-black", + enabled && vars.pitchBlack + ); + document.body.classList.toggle("body-light", !enabled); + } + }, 100); + } } let page = - realPath === "" - ? pages[0] - : pages.find( - (p) => - (!p.exclude || !p.exclude.includes(realPath)) && - (p.paths.includes(realPath) || - p.paths.find((r) => r instanceof RegExp && r.test(realPath))) - ); + realPath === "" + ? pages[0] + : pages.find( + (p) => + (!p.exclude || !p.exclude.includes(realPath)) && + (p.paths.includes(realPath) || + p.paths.find( + (r) => r instanceof RegExp && r.test(realPath) + )) + ); (async () => { - if (!page) return; + if (!page) return; + + // wait for variables + try { + if (!vars) { + await varsPromise; + } + } catch (e) { + await varsPromise; + } + console.log(2, vars); + if (vars.darkMode || (vars.timeMode && isDark())) { + isDarkModeEnabled = true; + switchDarkMode(true); + } + document + .querySelector(":root") + .style.setProperty( + "--transition-profile-banner", + vars.transitionProfileBanner ? ".1s" : "0s" + ); + if (vars.systemDarkMode) { + var matchMediaDark = window.matchMedia("(prefers-color-scheme: dark)"); + var matchMediaLight = window.matchMedia( + "(prefers-color-scheme: light)" + ); - // wait for variables - try { - if (!vars) { - await varsPromise; + matchMediaDark.addEventListener("change", (e) => { + if (e.matches && vars.systemDarkMode) { + switchDarkMode(true); + } + }); + matchMediaLight.addEventListener("change", (e) => { + if (e.matches && vars.systemDarkMode) { + switchDarkMode(false); + } + }); } - } catch (e) { - await varsPromise; - } - console.log(2, vars); - if (vars.darkMode || (vars.timeMode && isDark())) { - isDarkModeEnabled = true; - switchDarkMode(true); - } - document - .querySelector(":root") - .style.setProperty( - "--transition-profile-banner", - vars.transitionProfileBanner ? ".1s" : "0s" - ); - if (vars.systemDarkMode) { - var matchMediaDark = window.matchMedia("(prefers-color-scheme: dark)"); - var matchMediaLight = window.matchMedia("(prefers-color-scheme: light)"); - matchMediaDark.addEventListener("change", (e) => { - if (e.matches && vars.systemDarkMode) { - switchDarkMode(true); - } - }); - matchMediaLight.addEventListener("change", (e) => { - if (e.matches && vars.systemDarkMode) { - switchDarkMode(false); - } - }); - } - - // disable twitters service worker - if ("serviceWorker" in navigator) { - navigator.serviceWorker.getRegistrations().then((registrations) => { - for (const registration of registrations) { - registration.unregister(); - } - }); - // clear cache of service worker - if (window.caches) - window.caches.keys().then((keyList) => { - return Promise.all( - keyList.map((key) => { - return window.caches.delete(key); - }) + // disable twitters service worker + if ("serviceWorker" in navigator) { + navigator.serviceWorker.getRegistrations().then((registrations) => { + for (const registration of registrations) { + registration.unregister(); + } + }); + // clear cache of service worker + if (window.caches) + window.caches.keys().then((keyList) => { + return Promise.all( + keyList.map((key) => { + return window.caches.delete(key); + }) + ); + }); + } + + // default variables + if (typeof vars.darkMode !== "boolean" && document.body) { + let bg = document.body.style.backgroundColor; + let isDark = bg === "rgb(21, 32, 43)" || bg === "rgb(0, 0, 0)"; + vars.darkMode = isDark; + chrome.storage.sync.set( + { + darkMode: isDark, + }, + () => {} + ); + let pitchBlack = bg === "rgb(0, 0, 0)"; + vars.pitchBlack = pitchBlack; + chrome.storage.sync.set( + { + pitchBlack: pitchBlack, + }, + () => {} + ); + } + if (!vars.displaySensitiveContentMoved) { + API.account.getSettings().then((settings) => { + vars.displaySensitiveContent = settings.display_sensitive_media; + chrome.storage.sync.set( + { + displaySensitiveContentMoved: true, + displaySensitiveContent: settings.display_sensitive_media, + }, + () => {} + ); + }); + } + if (typeof vars.language !== "string") { + chrome.storage.sync.set( + { + language: LANGUAGE, + }, + () => {} ); - }); - } - - // default variables - if (typeof vars.darkMode !== "boolean" && document.body) { - let bg = document.body.style.backgroundColor; - let isDark = bg === "rgb(21, 32, 43)" || bg === "rgb(0, 0, 0)"; - vars.darkMode = isDark; - chrome.storage.sync.set( - { - darkMode: isDark, - }, - () => {} - ); - let pitchBlack = bg === "rgb(0, 0, 0)"; - vars.pitchBlack = pitchBlack; - chrome.storage.sync.set( - { - pitchBlack: pitchBlack, - }, - () => {} - ); - } - if (!vars.displaySensitiveContentMoved) { - API.account.getSettings().then((settings) => { - vars.displaySensitiveContent = settings.display_sensitive_media; - chrome.storage.sync.set( - { - displaySensitiveContentMoved: true, - displaySensitiveContent: settings.display_sensitive_media, - }, - () => {} - ); - }); - } - if (typeof vars.language !== "string") { - chrome.storage.sync.set( - { - language: LANGUAGE, - }, - () => {} - ); - } else { - LANGUAGE = LANGUAGES.includes(vars.language) ? vars.language : "en"; - } - - let [LOC_DATA, LOC_EN_DATA, html, css, header_html, header_css] = - await Promise.all([ - fetch(chrome.runtime.getURL(`_locales/${LANGUAGE}/messages.json`)).then( - (response) => response.json() - ), - fetch(chrome.runtime.getURL(`_locales/en/messages.json`)).then( - (response) => response.json() - ), - fetch(chrome.runtime.getURL(`layouts/${page.name}/index.html`)).then( - (response) => response.text() - ), - fetch(chrome.runtime.getURL(`layouts/${page.name}/style.css`)).then( - (response) => response.text() - ), - fetch(chrome.runtime.getURL(`layouts/header/index.html`)).then( - (response) => response.text() - ), - fetch(chrome.runtime.getURL(`layouts/header/style.css`)).then( - (response) => response.text() - ), - ]); - LOC = LOC_DATA; - LOC_EN = LOC_EN_DATA; - LOC_EN.extension_id = { message: chrome.runtime.id }; - - try { - let cryptoKey = await readCryptoKey(); - console.log("Crypto key", cryptoKey); - if (cryptoKey) { - OLDTWITTER_CONFIG.deviceId = cryptoKey.deviceId; } else { - OLDTWITTER_CONFIG.deviceId = localStorage.getItem("device_id"); - if (!OLDTWITTER_CONFIG.deviceId) { - OLDTWITTER_CONFIG.deviceId = uuidV4(); - localStorage.setItem("device_id", OLDTWITTER_CONFIG.deviceId); - } + LANGUAGE = LANGUAGES.includes(vars.language) ? vars.language : "en"; } - } catch (e) { - console.error("Error reading crypto key", e); - OLDTWITTER_CONFIG.deviceId = localStorage.getItem("device_id"); - if (!OLDTWITTER_CONFIG.deviceId) { - OLDTWITTER_CONFIG.deviceId = uuidV4(); - localStorage.setItem("device_id", OLDTWITTER_CONFIG.deviceId); + + let [LOC_DATA, LOC_EN_DATA, html, css, header_html, header_css] = + await Promise.all([ + fetch( + chrome.runtime.getURL(`_locales/${LANGUAGE}/messages.json`) + ).then((response) => response.json()), + fetch(chrome.runtime.getURL(`_locales/en/messages.json`)).then( + (response) => response.json() + ), + fetch( + chrome.runtime.getURL(`layouts/${page.name}/index.html`) + ).then((response) => response.text()), + fetch(chrome.runtime.getURL(`layouts/${page.name}/style.css`)).then( + (response) => response.text() + ), + fetch(chrome.runtime.getURL(`layouts/header/index.html`)).then( + (response) => response.text() + ), + fetch(chrome.runtime.getURL(`layouts/header/style.css`)).then( + (response) => response.text() + ), + ]); + LOC = LOC_DATA; + LOC_EN = LOC_EN_DATA; + LOC_EN.extension_id = { message: chrome.runtime.id }; + + try { + let cryptoKey = await readCryptoKey(); + console.log("Crypto key", cryptoKey); + if (cryptoKey) { + OLDTWITTER_CONFIG.deviceId = cryptoKey.deviceId; + } else { + OLDTWITTER_CONFIG.deviceId = localStorage.getItem("device_id"); + if (!OLDTWITTER_CONFIG.deviceId) { + OLDTWITTER_CONFIG.deviceId = uuidV4(); + localStorage.setItem("device_id", OLDTWITTER_CONFIG.deviceId); + } + } + } catch (e) { + console.error("Error reading crypto key", e); + OLDTWITTER_CONFIG.deviceId = localStorage.getItem("device_id"); + if (!OLDTWITTER_CONFIG.deviceId) { + OLDTWITTER_CONFIG.deviceId = uuidV4(); + localStorage.setItem("device_id", OLDTWITTER_CONFIG.deviceId); + } } - } - LOC_EN.twitter_site_verification = { - message: OLDTWITTER_CONFIG.verificationKey, - }; + LOC_EN.twitter_site_verification = { + message: OLDTWITTER_CONFIG.verificationKey, + }; - // internationalization - for (let i in LOC_EN) { - if (!LOC[i]) { - LOC[i] = LOC_EN[i]; + // internationalization + for (let i in LOC_EN) { + if (!LOC[i]) { + LOC[i] = LOC_EN[i]; + } } - } - let msgs = html.match(/__MSG_(\w+)__/g); - if (msgs) { - for (let i = 0; i < msgs.length; i++) { - let m = msgs[i].slice(6, -2); - if (LOC[m]) html = replaceAll(html, msgs[i], LOC[m].message); + let msgs = html.match(/__MSG_(\w+)__/g); + if (msgs) { + for (let i = 0; i < msgs.length; i++) { + let m = msgs[i].slice(6, -2); + if (LOC[m]) html = replaceAll(html, msgs[i], LOC[m].message); + } } - } - msgs = header_html.match(/__MSG_(\w+)__/g); - if (msgs) { - for (let i = 0; i < msgs.length; i++) { - let m = msgs[i].slice(6, -2); - if (LOC[m]) - header_html = replaceAll(header_html, msgs[i], LOC[m].message); + msgs = header_html.match(/__MSG_(\w+)__/g); + if (msgs) { + for (let i = 0; i < msgs.length; i++) { + let m = msgs[i].slice(6, -2); + if (LOC[m]) + header_html = replaceAll(header_html, msgs[i], LOC[m].message); + } } - } - - let body = document.querySelector('body[style^="background"]'); - if (body) { - body.remove(); - } else { - let cleared = false; - let removeInt = setInterval(() => { - let body = document.querySelector('body[style^="background"]'); - if (body) { - clearInterval(removeInt); - cleared = true; + + let body = document.querySelector('body[style^="background"]'); + if (body) { body.remove(); - } - }, 25); - setTimeout(() => { - if (!cleared) clearInterval(removeInt); - }, 5000); - } - - if (vars.extensionCompatibilityMode) { - html = html.replace("fake-react-root", "react-root"); - } - - document.documentElement.innerHTML = html; - let root = document.getElementById("fake-react-root"); - if (!root) root = document.getElementById("react-root"); - if (root && root.style) root.style.paddingLeft = "calc(100vw - 100%)"; - document.body.classList.add("body-old-ui"); - - blockingObserver.disconnect(); - - document.getElementsByTagName("header")[0].innerHTML = header_html; - if (page.activeMenu) { - let el = document.getElementById(page.activeMenu); - el.classList.add("menu-active"); - } - let version = document.getElementById("oldtwitter-version"); - if (version) { - version.innerText = chrome.runtime.getManifest().version; - } - let version2 = document.getElementById("oldtwitter-version-left"); - if (version2) { - version2.innerText = chrome.runtime.getManifest().version; - } - - let style = document.createElement("style"); - style.innerHTML = css; - document.head.appendChild(style); - let header_style = document.createElement("style"); - header_style.innerHTML = header_css; - document.head.appendChild(header_style); - - let icon = document.createElement("link"); - icon.href = chrome.runtime.getURL( - `images/logo32${vars.useNewIcon ? "_new" : ""}.png` - ); - icon.rel = "icon"; - icon.id = "site-icon"; - document.head.appendChild(icon); - - history.scrollRestoration = "manual"; - - // gif.js - fetch(chrome.runtime.getURL("libraries/gif.worker.js")) - .then((response) => response.text()) - .then((text) => { - window.gifWorkerUrl = window.URL.createObjectURL( - new Blob([text], { type: "text/javascript" }) - ); - }); + } else { + let cleared = false; + let removeInt = setInterval(() => { + let body = document.querySelector('body[style^="background"]'); + if (body) { + clearInterval(removeInt); + cleared = true; + body.remove(); + } + }, 25); + setTimeout(() => { + if (!cleared) clearInterval(removeInt); + }, 5000); + } - // tinytoast - if (document.querySelector(".t-wrap")) { - document.querySelector(".t-wrap").remove(); - } - - // viewer.js - let touchStartTime = 0; - document.addEventListener( - "touchstart", - (e) => { - if (e.target.className !== "viewer-canvas") return; - touchStartTime = Date.now(); - }, - { passive: true } - ); - document.addEventListener( - "touchend", - (e) => { - if (e.target.className !== "viewer-canvas") return; - if (Date.now() - touchStartTime < 150) { - let viewerContainer = e.target.closest(".viewer-container"); - setTimeout( - () => viewerContainer.querySelector(".viewer-close").click(), - 100 - ); - } - }, - { passive: false } - ); - - chrome.runtime.sendMessage({ - action: "inject", - files: [ - "libraries/purify.min.js", - "libraries/twemoji.min.js", - page.name === "settings" ? "libraries/parseCssColor.js" : "", - page.name === "settings" ? "libraries/coloris.min.js" : "", - "libraries/twitter-text.js", - "layouts/header/script.js", - `layouts/${page.name}/script.js`, - "scripts/tweetviewer.js", - "libraries/gif.js", - "libraries/viewer.min.js", - "libraries/custom-elements.min.js", - "libraries/emojipicker.js", - "libraries/tinytoast.js", - "scripts/iframeNavigation.js", - ].filter((i) => i), - }); + if (vars.extensionCompatibilityMode) { + html = html.replace("fake-react-root", "react-root"); + } + + document.documentElement.innerHTML = html; + let root = document.getElementById("fake-react-root"); + if (!root) root = document.getElementById("react-root"); + if (root && root.style) root.style.paddingLeft = "calc(100vw - 100%)"; + document.body.classList.add("body-old-ui"); + + blockingObserver.disconnect(); + + document.getElementsByTagName("header")[0].innerHTML = header_html; + if (page.activeMenu) { + let el = document.getElementById(page.activeMenu); + el.classList.add("menu-active"); + } + let version = document.getElementById("oldtwitter-version"); + if (version) { + version.innerText = chrome.runtime.getManifest().version; + } + let version2 = document.getElementById("oldtwitter-version-left"); + if (version2) { + version2.innerText = chrome.runtime.getManifest().version; + } + + let style = document.createElement("style"); + style.innerHTML = css; + document.head.appendChild(style); + let header_style = document.createElement("style"); + header_style.innerHTML = header_css; + document.head.appendChild(header_style); + + let icon = document.createElement("link"); + icon.href = chrome.runtime.getURL( + `images/logo32${vars.useNewIcon ? "_new" : ""}.png` + ); + icon.rel = "icon"; + icon.id = "site-icon"; + document.head.appendChild(icon); + + history.scrollRestoration = "manual"; + + // gif.js + fetch(chrome.runtime.getURL("libraries/gif.worker.js")) + .then((response) => response.text()) + .then((text) => { + window.gifWorkerUrl = window.URL.createObjectURL( + new Blob([text], { type: "text/javascript" }) + ); + }); + + // tinytoast + if (document.querySelector(".t-wrap")) { + document.querySelector(".t-wrap").remove(); + } + + // viewer.js + let touchStartTime = 0; + document.addEventListener( + "touchstart", + (e) => { + if (e.target.className !== "viewer-canvas") return; + touchStartTime = Date.now(); + }, + { passive: true } + ); + document.addEventListener( + "touchend", + (e) => { + if (e.target.className !== "viewer-canvas") return; + if (Date.now() - touchStartTime < 150) { + let viewerContainer = e.target.closest(".viewer-container"); + setTimeout( + () => + viewerContainer.querySelector(".viewer-close").click(), + 100 + ); + } + }, + { passive: false } + ); + + chrome.runtime.sendMessage({ + action: "inject", + files: [ + "libraries/purify.min.js", + "libraries/twemoji.min.js", + page.name === "settings" ? "libraries/parseCssColor.js" : "", + page.name === "settings" ? "libraries/coloris.min.js" : "", + "libraries/twitter-text.js", + "layouts/header/script.js", + `layouts/${page.name}/script.js`, + "scripts/tweetviewer.js", + "libraries/gif.js", + "libraries/viewer.min.js", + "libraries/custom-elements.min.js", + "libraries/emojipicker.js", + "libraries/tinytoast.js", + "scripts/iframeNavigation.js", + ].filter((i) => i), + }); })(); diff --git a/scripts/newtwitter.js b/scripts/newtwitter.js index 43a68c17..c1e8dba4 100644 --- a/scripts/newtwitter.js +++ b/scripts/newtwitter.js @@ -5,125 +5,125 @@ searchParams.delete("newtwitter"); hrefUrl.search = searchParams.toString(); r.href = hrefUrl.toString(); setInterval(() => { - let hrefUrl = new URL(location.href); - let searchParams = new URLSearchParams(hrefUrl.search); - searchParams.delete("newtwitter"); - hrefUrl.search = searchParams.toString(); - r.href = hrefUrl.toString(); + let hrefUrl = new URL(location.href); + let searchParams = new URLSearchParams(hrefUrl.search); + searchParams.delete("newtwitter"); + hrefUrl.search = searchParams.toString(); + r.href = hrefUrl.toString(); - let realPath = location.pathname.split("?")[0].split("#")[0]; - if (realPath.endsWith("/")) { - realPath = realPath.slice(0, -1); - } - if ( - /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/analytics$/.test(realPath) || - (realPath.startsWith("/i/") && - realPath !== "/i/bookmarks" && - !realPath.startsWith("/i/lists/")) || - realPath === "/explore" || - realPath === "/login" || - realPath === "/register" || - realPath === "/logout" || - realPath === "/messages" || - realPath.endsWith("/tos") || - realPath.endsWith("/privacy") || - realPath.startsWith("/account/") || - realPath.endsWith("/lists") || - realPath.endsWith("/topics") || - realPath.startsWith("/settings/") - ) { - r.hidden = true; - } else { - r.hidden = false; - } + let realPath = location.pathname.split("?")[0].split("#")[0]; + if (realPath.endsWith("/")) { + realPath = realPath.slice(0, -1); + } + if ( + /^\/[A-z-0-9-_]{1,15}\/status\/\d{5,32}\/analytics$/.test(realPath) || + (realPath.startsWith("/i/") && + realPath !== "/i/bookmarks" && + !realPath.startsWith("/i/lists/")) || + realPath === "/explore" || + realPath === "/login" || + realPath === "/register" || + realPath === "/logout" || + realPath === "/messages" || + realPath.endsWith("/tos") || + realPath.endsWith("/privacy") || + realPath.startsWith("/account/") || + realPath.endsWith("/lists") || + realPath.endsWith("/topics") || + realPath.startsWith("/settings/") + ) { + r.hidden = true; + } else { + r.hidden = false; + } - if (!location.search.includes("newtwitter=true")) { - let url = new URL(location.href); - url.searchParams.set("newtwitter", "true"); - history.replaceState(null, null, url.href); - } + if (!location.search.includes("newtwitter=true")) { + let url = new URL(location.href); + url.searchParams.set("newtwitter", "true"); + history.replaceState(null, null, url.href); + } }, 500); r.textContent = "Open this page in OldTwitter"; r.style.cssText = - "position: fixed; top: 0; right: 10px; padding: 0.5em; background: #fff; color: #000; font-family: Arial, sans-serif;border-radius:3px;"; + "position: fixed; top: 0; right: 10px; padding: 0.5em; background: #fff; color: #000; font-family: Arial, sans-serif;border-radius:3px;"; document.body.appendChild(r); setTimeout(() => { - let realPath = location.pathname.split("?")[0].split("#")[0]; - if (realPath.endsWith("/")) { - realPath = realPath.slice(0, -1); - } - if (realPath === "/i/flow/login") { - let i = setInterval(() => { - let head = document.getElementById("modal-header"); - if (head) { - clearInterval(i); - let span = document.createElement("span"); - span.innerHTML = html`OldTwitter relies on internal APIs that only work - when you're logged in.
Please log in on this page to see old - Twitter layout.`; - span.style.cssText = `display: block;margin: 0.5em 0px;color: #fbfeff;font-family: TwitterChirp;background: rgb(0 161 255 / 10%);padding: 8px;border-radius: 5px;`; - head.after(span); - } - }, 500); - } + let realPath = location.pathname.split("?")[0].split("#")[0]; + if (realPath.endsWith("/")) { + realPath = realPath.slice(0, -1); + } + if (realPath === "/i/flow/login") { + let i = setInterval(() => { + let head = document.getElementById("modal-header"); + if (head) { + clearInterval(i); + let span = document.createElement("span"); + span.innerHTML = html`OldTwitter relies on internal APIs that + only work when you're logged in.
Please log in on this + page to see old Twitter layout.`; + span.style.cssText = `display: block;margin: 0.5em 0px;color: #fbfeff;font-family: TwitterChirp;background: rgb(0 161 255 / 10%);padding: 8px;border-radius: 5px;`; + head.after(span); + } + }, 500); + } }, 1000); (() => { - let keysHeld = {}; - function processHotkeys() { - if (keysHeld["Alt"] && keysHeld["Control"] && keysHeld["KeyO"]) { - let url = new URL(location.href); - url.searchParams.delete("newtwitter"); - location.replace(url.href); + let keysHeld = {}; + function processHotkeys() { + if (keysHeld["Alt"] && keysHeld["Control"] && keysHeld["KeyO"]) { + let url = new URL(location.href); + url.searchParams.delete("newtwitter"); + location.replace(url.href); + } } - } - window.addEventListener("keydown", (ev) => { - let key = ev.code; - if (key === "AltLeft" || key === "AltRight") key = "Alt"; - if (key === "ControlLeft" || key === "ControlRight") key = "Control"; - if (key === "ShiftLeft" || key === "ShiftRight") key = "Shift"; - keysHeld[key] = true; + window.addEventListener("keydown", (ev) => { + let key = ev.code; + if (key === "AltLeft" || key === "AltRight") key = "Alt"; + if (key === "ControlLeft" || key === "ControlRight") key = "Control"; + if (key === "ShiftLeft" || key === "ShiftRight") key = "Shift"; + keysHeld[key] = true; - processHotkeys(); - }); + processHotkeys(); + }); - window.addEventListener("keyup", (ev) => { - let key = ev.code; - if (key === "AltLeft" || key === "AltRight") key = "Alt"; - if (key === "ControlLeft" || key === "ControlRight") key = "Control"; - if (key === "ShiftLeft" || key === "ShiftRight") key = "Shift"; - keysHeld[key] = true; - processHotkeys(); - keysHeld[key] = false; - }); + window.addEventListener("keyup", (ev) => { + let key = ev.code; + if (key === "AltLeft" || key === "AltRight") key = "Alt"; + if (key === "ControlLeft" || key === "ControlRight") key = "Control"; + if (key === "ShiftLeft" || key === "ShiftRight") key = "Shift"; + keysHeld[key] = true; + processHotkeys(); + keysHeld[key] = false; + }); })(); function modifyLink(a) { - if (a.href && !a.href.includes("newtwitter=true")) { - let url = new URL(a.href); - url.searchParams.set("newtwitter", "true"); - a.href = url.href; - } + if (a.href && !a.href.includes("newtwitter=true")) { + let url = new URL(a.href); + url.searchParams.set("newtwitter", "true"); + a.href = url.href; + } } const linkObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.tagName === "A") { - modifyLink(node); - } - node.querySelectorAll("a").forEach(modifyLink); + mutations.forEach((mutation) => { + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.tagName === "A") { + modifyLink(node); + } + node.querySelectorAll("a").forEach(modifyLink); + } + }); } - }); - } - }); + }); }); // Start observing the page for changes linkObserver.observe(document.documentElement, { - childList: true, - subtree: true, + childList: true, + subtree: true, }); diff --git a/scripts/tdeb.js b/scripts/tdeb.js index 0ab35c3c..5c1a5a4b 100644 --- a/scripts/tdeb.js +++ b/scripts/tdeb.js @@ -1,8 +1,8 @@ // Shim for isArray check. (cc. https://stackoverflow.com/a/20956445) if (typeof Array.isArray === "undefined") { - Array.isArray = function (obj) { - return Object.prototype.toString.call(obj) === "[object Array]"; - }; + Array.isArray = function (obj) { + return Object.prototype.toString.call(obj) === "[object Array]"; + }; } /** @@ -12,9 +12,9 @@ if (typeof Array.isArray === "undefined") { * @note This code does not sanitize **any** html and is inheriently unsafe. */ function htmlToNodes(string) { - const tmp = document.createElement("template"); - tmp.innerHTML = string; - return tmp; + const tmp = document.createElement("template"); + tmp.innerHTML = string; + return tmp; } /** @@ -24,7 +24,7 @@ function htmlToNodes(string) { * @returns */ function interleave(arr, x) { - return arr.flatMap((e) => [e, x]).slice(0, -1); + return arr.flatMap((e) => [e, x]).slice(0, -1); } // Tiny Dom Element builder. Adapted from stackoverflow. @@ -39,71 +39,77 @@ function interleave(arr, x) { * @returns A Node. */ const elNew = (tag, prop, children = []) => { - const _elCustomValue = ["dataset", "className", "classList", "class"]; - const element = document.createElement(tag); - if (prop) { - const filteredObject = Object.keys(prop).reduce(function (returnObj, key) { - if (!_elCustomValue.includes(key) && prop[key]) - returnObj[key] = prop[key]; - return returnObj; - }, {}); - const customProps = Object.keys(prop).reduce(function ( - returnObj, - elementKey - ) { - if (_elCustomValue.includes(elementKey) && prop[elementKey]) - returnObj[elementKey] = prop[elementKey]; - return returnObj; - }, - {}); - Object.assign(element, filteredObject); - if (Object.keys(customProps).length > 0) { - for (const key in customProps) { - const propValue = customProps[key]; - if (key === "dataset") { - for (const datasetKey in propValue) { - element.dataset[datasetKey] = propValue[datasetKey]; - } - } else if ( - key === "className" || - key === "classList" || - key === "class" + const _elCustomValue = ["dataset", "className", "classList", "class"]; + const element = document.createElement(tag); + if (prop) { + const filteredObject = Object.keys(prop).reduce(function ( + returnObj, + key ) { - if (typeof propValue === "string") { - element.className = propValue; - } else if ( - typeof propValue === "object" && - Array.isArray(propValue) - ) { - element.className = propValue.filter((m) => m).join(" "); - } else { - console.error( - `Passed in a non Array/String value to ${key}. will be ignored.` - ); - } + if (!_elCustomValue.includes(key) && prop[key]) + returnObj[key] = prop[key]; + return returnObj; + }, + {}); + const customProps = Object.keys(prop).reduce(function ( + returnObj, + elementKey + ) { + if (_elCustomValue.includes(elementKey) && prop[elementKey]) + returnObj[elementKey] = prop[elementKey]; + return returnObj; + }, + {}); + Object.assign(element, filteredObject); + if (Object.keys(customProps).length > 0) { + for (const key in customProps) { + const propValue = customProps[key]; + if (key === "dataset") { + for (const datasetKey in propValue) { + element.dataset[datasetKey] = propValue[datasetKey]; + } + } else if ( + key === "className" || + key === "classList" || + key === "class" + ) { + if (typeof propValue === "string") { + element.className = propValue; + } else if ( + typeof propValue === "object" && + Array.isArray(propValue) + ) { + element.className = propValue + .filter((m) => m) + .join(" "); + } else { + console.error( + `Passed in a non Array/String value to ${key}. will be ignored.` + ); + } + } + } } - } } - } - if (typeof children !== "string") { - // Cleanup children. - children = children.filter((m) => m); + if (typeof children !== "string") { + // Cleanup children. + children = children.filter((m) => m); - if (children.length > 0) - children.forEach((child) => { - if (child !== null) { - if (typeof child === "string") { - if (child !== "") { - element.appendChild(document.createTextNode(child)); - } - } else { - element.appendChild(child); - } - } - }); - } else { - element.appendChild(document.createTextNode(children)); - } + if (children.length > 0) + children.forEach((child) => { + if (child !== null) { + if (typeof child === "string") { + if (child !== "") { + element.appendChild(document.createTextNode(child)); + } + } else { + element.appendChild(child); + } + } + }); + } else { + element.appendChild(document.createTextNode(children)); + } - return element; + return element; }; diff --git a/scripts/twchallenge.js b/scripts/twchallenge.js index c133eedf..ce9811f0 100644 --- a/scripts/twchallenge.js +++ b/scripts/twchallenge.js @@ -7,226 +7,233 @@ let solverErrored = false; let sentData = false; let sandboxUrl = fetch(chrome.runtime.getURL(`sandbox.html`)) - .then((resp) => resp.blob()) - .then((blob) => URL.createObjectURL(blob)) - .catch(console.error); + .then((resp) => resp.blob()) + .then((blob) => URL.createObjectURL(blob)) + .catch(console.error); function createSolverFrame() { - if (solverIframe) solverIframe.remove(); - solverIframe = document.createElement("iframe"); - //display:none causes animations to not play which breaks the challenge solver, so have to hide it in different way - solverIframe.style.position = "absolute"; - solverIframe.width = "0px"; - solverIframe.height = "0px"; - solverIframe.style.border = "none"; - solverIframe.style.opacity = 0; - solverIframe.style.pointerEvents = "none"; - solverIframe.tabIndex = -1; - sandboxUrl.then((url) => (solverIframe.src = url)); - let injectedBody = document.getElementById("injected-body"); - if (injectedBody) { - injectedBody.appendChild(solverIframe); - } else { - let int = setInterval(() => { - let injectedBody = document.getElementById("injected-body"); - if (injectedBody) { + if (solverIframe) solverIframe.remove(); + solverIframe = document.createElement("iframe"); + //display:none causes animations to not play which breaks the challenge solver, so have to hide it in different way + solverIframe.style.position = "absolute"; + solverIframe.width = "0px"; + solverIframe.height = "0px"; + solverIframe.style.border = "none"; + solverIframe.style.opacity = 0; + solverIframe.style.pointerEvents = "none"; + solverIframe.tabIndex = -1; + sandboxUrl.then((url) => (solverIframe.src = url)); + let injectedBody = document.getElementById("injected-body"); + if (injectedBody) { injectedBody.appendChild(solverIframe); - clearInterval(int); - } - }, 10); - } + } else { + let int = setInterval(() => { + let injectedBody = document.getElementById("injected-body"); + if (injectedBody) { + injectedBody.appendChild(solverIframe); + clearInterval(int); + } + }, 10); + } } createSolverFrame(); function solveChallenge(path, method) { - return new Promise((resolve, reject) => { - if (solverErrored) { - reject("Solver errored during initialization"); - return; - } - let id = solveId++; - solveCallbacks[id] = { resolve, reject, time: Date.now() }; - if (!solverReady || !solverIframe || !solverIframe.contentWindow) { - solveQueue.push({ id, path, method }); - } else { - try { - solverIframe.contentWindow.postMessage( - { action: "solve", id, path, method }, - "*" - ); - } catch (e) { - console.error(`Error sending challenge to solver:`, e); - reject(e); - } - // setTimeout(() => { - // if(solveCallbacks[id]) { - // solveCallbacks[id].reject('Solver timed out'); - // delete solveCallbacks[id]; - // } - // }, 1750); - } - }); + return new Promise((resolve, reject) => { + if (solverErrored) { + reject("Solver errored during initialization"); + return; + } + let id = solveId++; + solveCallbacks[id] = { resolve, reject, time: Date.now() }; + if (!solverReady || !solverIframe || !solverIframe.contentWindow) { + solveQueue.push({ id, path, method }); + } else { + try { + solverIframe.contentWindow.postMessage( + { action: "solve", id, path, method }, + "*" + ); + } catch (e) { + console.error(`Error sending challenge to solver:`, e); + reject(e); + } + // setTimeout(() => { + // if(solveCallbacks[id]) { + // solveCallbacks[id].reject('Solver timed out'); + // delete solveCallbacks[id]; + // } + // }, 1750); + } + }); } setInterval(() => { - if ( - !document.getElementById("loading-box").hidden && - sentData && - solveQueue.length - ) { - console.log( - "Something's wrong with the challenge solver, reloading", - solveQueue - ); - createSolverFrame(); - initChallenge(); - } + if ( + !document.getElementById("loading-box").hidden && + sentData && + solveQueue.length + ) { + console.log( + "Something's wrong with the challenge solver, reloading", + solveQueue + ); + createSolverFrame(); + initChallenge(); + } }, 2000); window.addEventListener("message", (e) => { - if (e.source !== solverIframe.contentWindow) return; - let data = e.data; - if (data.action === "solved" && typeof data.id === "number") { - let { id, result } = data; - if (solveCallbacks[id]) { - solveCallbacks[id].resolve(result); - delete solveCallbacks[id]; + if (e.source !== solverIframe.contentWindow) return; + let data = e.data; + if (data.action === "solved" && typeof data.id === "number") { + let { id, result } = data; + if (solveCallbacks[id]) { + solveCallbacks[id].resolve(result); + delete solveCallbacks[id]; + } + } else if (data.action === "error" && typeof data.id === "number") { + let { id, error } = data; + if (solveCallbacks[id]) { + solveCallbacks[id].reject(error); + delete solveCallbacks[id]; + } + } else if (data.action === "initError") { + solverErrored = true; + for (let id in solveCallbacks) { + solveCallbacks[id].reject("Solver errored during initialization"); + delete solveCallbacks[id]; + } + alert( + `There was an error in initializing security header generator:\n${data.error}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security.` + ); + console.error("Error initializing solver:"); + console.error(data.error); + } else if (data.action === "ready") { + solverReady = true; + for (let task of solveQueue) { + solverIframe.contentWindow.postMessage( + { + action: "solve", + id: task.id, + path: task.path, + method: task.method, + }, + "*" + ); + } } - } else if (data.action === "error" && typeof data.id === "number") { - let { id, error } = data; - if (solveCallbacks[id]) { - solveCallbacks[id].reject(error); - delete solveCallbacks[id]; +}); + +window._fetch = window.fetch; +fetch = async function (url, options) { + if ( + !url.startsWith("/i/api") && + !url.startsWith("https://api.twitter.com") && + !url.startsWith("https://api.x.com") + ) + return _fetch(url, options); + if (!options) options = {}; + if (!options.headers) options.headers = {}; + if (!options.headers["x-twitter-auth-type"]) { + options.headers["x-twitter-auth-type"] = "OAuth2Session"; } - } else if (data.action === "initError") { - solverErrored = true; - for (let id in solveCallbacks) { - solveCallbacks[id].reject("Solver errored during initialization"); - delete solveCallbacks[id]; + if (!options.headers["x-twitter-active-user"]) { + options.headers["x-twitter-active-user"] = "yes"; } - alert( - `There was an error in initializing security header generator:\n${data.error}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security.` + if (!options.headers["X-Client-UUID"]) { + options.headers["X-Client-UUID"] = OLDTWITTER_CONFIG.deviceId; + } + if (!url.startsWith("http:") && !url.startsWith("https:")) { + let host = location.hostname; + if (!["x.com", "twitter.com"].includes(host)) host = "x.com"; + if (!url.startsWith("/")) url = "/" + url; + url = `https://${host}${url}`; + } + let parsedUrl = new URL(url); + // try { + let solved = await solveChallenge( + parsedUrl.pathname, + options.method ? options.method.toUpperCase() : "GET" ); - console.error("Error initializing solver:"); - console.error(data.error); - } else if (data.action === "ready") { - solverReady = true; - for (let task of solveQueue) { - solverIframe.contentWindow.postMessage( - { action: "solve", id: task.id, path: task.path, method: task.method }, - "*" - ); + options.headers["x-client-transaction-id"] = solved; + // } catch (e) { + // console.error(`Error solving challenge for ${url}:`); + // console.error(e); + // } + if ( + options.method && + options.method.toUpperCase() === "POST" && + typeof options.body === "string" + ) { + options.headers["Content-Length"] = options.body.length; } - } -}); -window._fetch = window.fetch; -fetch = async function (url, options) { - if ( - !url.startsWith("/i/api") && - !url.startsWith("https://api.twitter.com") && - !url.startsWith("https://api.x.com") - ) return _fetch(url, options); - if (!options) options = {}; - if (!options.headers) options.headers = {}; - if (!options.headers["x-twitter-auth-type"]) { - options.headers["x-twitter-auth-type"] = "OAuth2Session"; - } - if (!options.headers["x-twitter-active-user"]) { - options.headers["x-twitter-active-user"] = "yes"; - } - if (!options.headers["X-Client-UUID"]) { - options.headers["X-Client-UUID"] = OLDTWITTER_CONFIG.deviceId; - } - if (!url.startsWith("http:") && !url.startsWith("https:")) { - let host = location.hostname; - if (!["x.com", "twitter.com"].includes(host)) host = "x.com"; - if (!url.startsWith("/")) url = "/" + url; - url = `https://${host}${url}`; - } - let parsedUrl = new URL(url); - // try { - let solved = await solveChallenge( - parsedUrl.pathname, - options.method ? options.method.toUpperCase() : "GET" - ); - options.headers["x-client-transaction-id"] = solved; - // } catch (e) { - // console.error(`Error solving challenge for ${url}:`); - // console.error(e); - // } - if ( - options.method && - options.method.toUpperCase() === "POST" && - typeof options.body === "string" - ) { - options.headers["Content-Length"] = options.body.length; - } - - return _fetch(url, options); }; async function initChallenge() { - try { - let homepageData; - let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); - let host = location.hostname; - if (!["x.com", "twitter.com"].includes(host)) host = "x.com"; try { - homepageData = await _fetch(`https://${host}/`).then((res) => res.text()); - } catch (e) { - await sleep(500); - try { - homepageData = await _fetch(`https://${host}/`).then((res) => - res.text() - ); - } catch (e) { - throw new Error("Failed to fetch homepage: " + e); - } - } - let dom = new DOMParser().parseFromString(homepageData, "text/html"); - let verificationKey = dom.querySelector( - 'meta[name="twitter-site-verification"]' - ).content; - let anims = Array.from(dom.querySelectorAll('svg[id^="loading-x"]')).map( - (svg) => svg.outerHTML - ); + let homepageData; + let sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + let host = location.hostname; + if (!["x.com", "twitter.com"].includes(host)) host = "x.com"; + try { + homepageData = await _fetch(`https://${host}/`).then((res) => + res.text() + ); + } catch (e) { + await sleep(500); + try { + homepageData = await _fetch(`https://${host}/`).then((res) => + res.text() + ); + } catch (e) { + throw new Error("Failed to fetch homepage: " + e); + } + } + let dom = new DOMParser().parseFromString(homepageData, "text/html"); + let verificationKey = dom.querySelector( + 'meta[name="twitter-site-verification"]' + ).content; + let anims = Array.from( + dom.querySelectorAll('svg[id^="loading-x"]') + ).map((svg) => svg.outerHTML); - let challengeCode = homepageData.match(/"ondemand.s":"(\w+)"/)[1]; + let challengeCode = homepageData.match(/"ondemand.s":"(\w+)"/)[1]; - OLDTWITTER_CONFIG.verificationKey = verificationKey; + OLDTWITTER_CONFIG.verificationKey = verificationKey; - function sendInit() { - sentData = true; - if (!solverIframe || !solverIframe.contentWindow) - return setTimeout(sendInit, 50); - solverIframe.contentWindow.postMessage( - { - action: "init", - challengeCode, - anims, - verificationCode: OLDTWITTER_CONFIG.verificationKey, - }, - "*" - ); - } - setTimeout(sendInit, 50); - return true; - } catch (e) { - console.error(`Error during challenge init:`); - console.error(e); - if (location.hostname === "twitter.com") { - alert( - `There was an error in initializing security header generator: ${e}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security. Currently the main reason for this happening is social network tracker protection blocking the script. Try disabling such settings in your browser and extensions that do that and refresh the page. This also might be because you're either not logged in or using twitter.com instead of x.com.` - ); - } else { - alert( - `There was an error in initializing security header generator: ${e}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security. Currently the main reason for this happening is social network tracker protection blocking the script. Try disabling such settings in your browser and extensions that do that and refresh the page. This can also happen if you're not logged in.` - ); + function sendInit() { + sentData = true; + if (!solverIframe || !solverIframe.contentWindow) + return setTimeout(sendInit, 50); + solverIframe.contentWindow.postMessage( + { + action: "init", + challengeCode, + anims, + verificationCode: OLDTWITTER_CONFIG.verificationKey, + }, + "*" + ); + } + setTimeout(sendInit, 50); + return true; + } catch (e) { + console.error(`Error during challenge init:`); + console.error(e); + if (location.hostname === "twitter.com") { + alert( + `There was an error in initializing security header generator: ${e}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security. Currently the main reason for this happening is social network tracker protection blocking the script. Try disabling such settings in your browser and extensions that do that and refresh the page. This also might be because you're either not logged in or using twitter.com instead of x.com.` + ); + } else { + alert( + `There was an error in initializing security header generator: ${e}\nUser Agent: ${navigator.userAgent}\nOldTwitter doesn't allow unsigned requests anymore for your account security. Currently the main reason for this happening is social network tracker protection blocking the script. Try disabling such settings in your browser and extensions that do that and refresh the page. This can also happen if you're not logged in.` + ); + } + return false; } - return false; - } } initChallenge(); diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index dce1a29d..b7c20f37 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1,19 +1,19 @@ // svgPlayIcon. if (Intl && Intl.DateTimeFormat) { - var tweetTimeFormatter = new Intl.DateTimeFormat(undefined, { - hour: "numeric", - minute: "numeric", - }); - var tweetDateFormatter = new Intl.DateTimeFormat(undefined, { - day: "numeric", - month: "short", - year: "numeric", - }); - var tweetShortishTimeformatter = new Intl.DateTimeFormat(undefined, { - dateStyle: "short", - timeStyle: "medium", - }); + var tweetTimeFormatter = new Intl.DateTimeFormat(undefined, { + hour: "numeric", + minute: "numeric", + }); + var tweetDateFormatter = new Intl.DateTimeFormat(undefined, { + day: "numeric", + month: "short", + year: "numeric", + }); + var tweetShortishTimeformatter = new Intl.DateTimeFormat(undefined, { + dateStyle: "short", + timeStyle: "medium", + }); } const svgPlayIcon = ` @@ -34,148 +34,156 @@ const svgPlayIcon = ` { - var source = document.createElement("source"); - source.src = variant.url; - source.type = variant.content_type; - newClone.appendChild(source); - }); - newClone.appendChild( - document.createTextNode(LOC.unsupported_video.message) - ); - htmlNodes.push(newClone); - } else if (m.type === "video") { - if (m.mediaStats && m.mediaStats.viewCount) { - m.ext = { - mediaStats: { r: { ok: { viewCount: m.mediaStats.viewCount } } }, - }; - } - let [w, h] = sizeFunctions[tweetObject.extended_entities.media.length]( - m.original_info.width, - m.original_info.height - ); - const newClone = video_template.cloneNode(true); - const altText = m.ext_alt_text ? escapeHTML(m.ext_alt_text, true) : ""; - if (altText) { - newClone.alt = newClone.title = altText; - } - newClone.width = w; - newClone.height = h; - newClone.loop = true; - newClone.controls = true; - - newClone.disableRemotePlayback = true; - if (vars.muteVideos) { - newClone.defaultMuted = true; - newClone.muted = true; - } else { - newClone.defaultMuted = false; - newClone.muted = false; - } - var mediaClass = mediaClasses[tweetObject.extended_entities.media.length]; - if (mediaClass) newClone.classList.add(mediaClass); - if (toCensor) newClone.classList.add("tweet-media-element-censor"); - newClone.poster = m.media_url_https; - m.video_info.variants.forEach((variant) => { - var source = document.createElement("source"); - source.src = variant.url; - source.type = variant.content_type; - newClone.appendChild(source); - }); - newClone.appendChild( - document.createTextNode(LOC.unsupported_video.message) - ); - htmlNodes.push(newClone); + // let _html = ""; + let htmlNodes = []; + if (!tweetObject.extended_entities || !tweetObject.extended_entities.media) + return htmlNodes; + + let cws = []; + + for (let i = 0; i < tweetObject.extended_entities.media.length; i++) { + let m = tweetObject.extended_entities.media[i]; + let toCensor = + !vars.displaySensitiveContent && tweetObject.possibly_sensitive; + if (m.sensitive_media_warning) { + if (m.sensitive_media_warning.graphic_violence) { + cws.push(LOC.graphic_violence.message); + toCensor = !vars.uncensorGraphicViolenceAutomatically; + } + if (m.sensitive_media_warning.adult_content) { + cws.push(LOC.adult_content.message); + toCensor = !vars.uncensorAdultContentAutomatically; + } + if (m.sensitive_media_warning.other) { + cws.push(LOC.sensitive_content.message); + toCensor = !vars.uncensorSensitiveContentAutomatically; + } + } + if (m.type === "photo") { + let [w, h] = sizeFunctions[ + tweetObject.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + const newClone = img_template.cloneNode(true); + const altText = m.ext_alt_text + ? escapeHTML(m.ext_alt_text, true) + : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.src = + m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith(".jpg") || + m.media_url_https.endsWith(".png")) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ? "?name=small" + : ""); + var mediaClass = + mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + htmlNodes.push(newClone); + } else if (m.type === "animated_gif") { + let [w, h] = sizeFunctions[ + tweetObject.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + let rid = m.id_str + m.media_key; + + const newClone = animated_gif_template.cloneNode(true); + const altText = m.ext_alt_text + ? escapeHTML(m.ext_alt_text, true) + : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + + newClone.defaultMuted = true; + newClone.muted = true; + newClone.disableRemotePlayback = true; + if (!vars.disableGifAutoplay) newClone.autoplay = true; + else newClone.autoplay = false; + var mediaClass = + mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + m.video_info.variants.forEach((variant) => { + var source = document.createElement("source"); + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }); + newClone.appendChild( + document.createTextNode(LOC.unsupported_video.message) + ); + htmlNodes.push(newClone); + } else if (m.type === "video") { + if (m.mediaStats && m.mediaStats.viewCount) { + m.ext = { + mediaStats: { + r: { ok: { viewCount: m.mediaStats.viewCount } }, + }, + }; + } + let [w, h] = sizeFunctions[ + tweetObject.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + const newClone = video_template.cloneNode(true); + const altText = m.ext_alt_text + ? escapeHTML(m.ext_alt_text, true) + : ""; + if (altText) { + newClone.alt = newClone.title = altText; + } + newClone.width = w; + newClone.height = h; + newClone.loop = true; + newClone.controls = true; + + newClone.disableRemotePlayback = true; + if (vars.muteVideos) { + newClone.defaultMuted = true; + newClone.muted = true; + } else { + newClone.defaultMuted = false; + newClone.muted = false; + } + var mediaClass = + mediaClasses[tweetObject.extended_entities.media.length]; + if (mediaClass) newClone.classList.add(mediaClass); + if (toCensor) newClone.classList.add("tweet-media-element-censor"); + newClone.poster = m.media_url_https; + m.video_info.variants.forEach((variant) => { + var source = document.createElement("source"); + source.src = variant.url; + source.type = variant.content_type; + newClone.appendChild(source); + }); + newClone.appendChild( + document.createTextNode(LOC.unsupported_video.message) + ); + htmlNodes.push(newClone); + } + if (i === 1 && tweetObject.extended_entities.media.length > 3) { + htmlNodes.push(elNew("br")); + } } - if (i === 1 && tweetObject.extended_entities.media.length > 3) { - htmlNodes.push(elNew("br")); + + if (cws.length > 0) { + cws = [...new Set(cws)]; + cws = LOC.content_warning.message.replace("$WARNINGS$", cws.join(", ")); + htmlNodes.push(elNew("br")); + htmlNodes.push(elNew("div", { className: "tweet-media-cws" }, [cws])); } - } - - if (cws.length > 0) { - cws = [...new Set(cws)]; - cws = LOC.content_warning.message.replace("$WARNINGS$", cws.join(", ")); - htmlNodes.push(elNew("br")); - htmlNodes.push(elNew("div", { className: "tweet-media-cws" }, [cws])); - } - return htmlNodes; + return htmlNodes; } /** @@ -186,224 +194,240 @@ function renderMultiMediaNodes(tweetObject) { * @returns */ async function constructQuotedTweet( - t, - isQuoteMatchingLanguage, - quoteMentionedUserText, - newQuoteMentionedUserText, - asNode + t, + isQuoteMatchingLanguage, + quoteMentionedUserText, + newQuoteMentionedUserText, + asNode ) { - asNode = asNode ? asNode : false; - // === Profile Element === - const profileElement = elNew("img", { - src: - t.quoted_status.user.default_profile_image && - vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(t.quoted_status.user.id_str) % 7 - }_normal.png` - ) - : t.quoted_status.user.profile_image_url_https, - alt: escapeHTML(t.quoted_status.user.name), - className: "tweet-avatar-quote", - width: "24", - height: "24", - }); - - // === Name Element === - var userNameClass = ["tweet-header-name-quote"]; - - if (t.quoted_status.user.verified) { - userNameClass.push("user-verified"); - } else if (t.quoted_status.user.id_str === "1708130407663759360") { - // Special case for dimden. - userNameClass.push("user-verified", "user-verified-dimden"); // Push multiple classes - } else if (t.quoted_status.user.protected) { - userNameClass.push("user-protected"); - } else if (t.quoted_status.user.verified_type === "Government") { - userNameClass.push("user-verified-gray"); - } else if (t.quoted_status.user.verified_type === "Business") { - userNameClass.push("user-verified-yellow"); - } else if (t.quoted_status.user.verified_type === "Blue") { - userNameClass.push("user-verified-blue"); - } - - // The name element + @ handle - const nameElement = elNew("div", { className: "tweet-header-quote" }, [ - elNew("span", { className: "tweet-header-info-quote" }, [ - // User name - elNew( - "b", + asNode = asNode ? asNode : false; + // === Profile Element === + const profileElement = elNew("img", { + src: + t.quoted_status.user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(t.quoted_status.user.id_str) % 7 + }_normal.png` + ) + : t.quoted_status.user.profile_image_url_https, + alt: escapeHTML(t.quoted_status.user.name), + className: "tweet-avatar-quote", + width: "24", + height: "24", + }); + + // === Name Element === + var userNameClass = ["tweet-header-name-quote"]; + + if (t.quoted_status.user.verified) { + userNameClass.push("user-verified"); + } else if (t.quoted_status.user.id_str === "1708130407663759360") { + // Special case for dimden. + userNameClass.push("user-verified", "user-verified-dimden"); // Push multiple classes + } else if (t.quoted_status.user.protected) { + userNameClass.push("user-protected"); + } else if (t.quoted_status.user.verified_type === "Government") { + userNameClass.push("user-verified-gray"); + } else if (t.quoted_status.user.verified_type === "Business") { + userNameClass.push("user-verified-yellow"); + } else if (t.quoted_status.user.verified_type === "Blue") { + userNameClass.push("user-verified-blue"); + } + + // The name element + @ handle + const nameElement = elNew("div", { className: "tweet-header-quote" }, [ + elNew("span", { className: "tweet-header-info-quote" }, [ + // User name + elNew( + "b", + { + className: userNameClass, + }, + [escapeHTML(t.quoted_status.user.name)] + ), + " ", + // At handle + elNew("span", { className: "tweet-header-handle-quote" }, [ + "@" + escapeHTML(t.quoted_status.user.screen_name), + ]), + ]), + ]); + // Time span + const quotedDateObject = new Date(t.quoted_status.created_at); + var titleTime = null; + if (tweetShortishTimeformatter) { + titleTime = tweetShortishTimeformatter.format(quotedDateObject); + } else { + titleTime = quotedDateObject.toLocaleString(); + } + const tweetTimeElement = elNew( + "span", { - className: userNameClass, + className: "tweet-time-quote", + dataset: { + timestamp: quotedDateObject.getTime(), + }, + title: titleTime, }, - [escapeHTML(t.quoted_status.user.name)] - ), - " ", - // At handle - elNew("span", { className: "tweet-header-handle-quote" }, [ - "@" + escapeHTML(t.quoted_status.user.screen_name), - ]), - ]), - ]); - // Time span - const quotedDateObject = new Date(t.quoted_status.created_at); - var titleTime = null; - if (tweetShortishTimeformatter) { - titleTime = tweetShortishTimeformatter.format(quotedDateObject); - } else { - titleTime = quotedDateObject.toLocaleString(); - } - const tweetTimeElement = elNew( - "span", - { - className: "tweet-time-quote", - dataset: { - timestamp: quotedDateObject.getTime(), - }, - title: titleTime, - }, - [timeElapsed(quotedDateObject)] - ); - - var oldStyleReplyTo = null; - if (newQuoteMentionedUserText.length > 0 && !vars.useOldStyleReply) { - newQuoteMentionedUserText = interleave( - newQuoteMentionedUserText, - LOC.replying_to_comma.message + [timeElapsed(quotedDateObject)] ); - if (newQuoteMentionedUserText.length >= 5) { - const arrLength = newQuoteMentionedUserText.length; - newQuoteMentionedUserText[arrLength - 2] = LOC.replying_to_and.message; + var oldStyleReplyTo = null; + if (newQuoteMentionedUserText.length > 0 && !vars.useOldStyleReply) { + newQuoteMentionedUserText = interleave( + newQuoteMentionedUserText, + LOC.replying_to_comma.message + ); + + if (newQuoteMentionedUserText.length >= 5) { + const arrLength = newQuoteMentionedUserText.length; + newQuoteMentionedUserText[arrLength - 2] = + LOC.replying_to_and.message; + } + + var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); + textFragments = interleave( + textFragments, + newQuoteMentionedUserText + ).flat(Infinity); + + oldStyleReplyTo = elNew( + "span", + { className: "tweet-reply-to tweet-quote-reply-to" }, + textFragments + ); } - var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); - textFragments = interleave(textFragments, newQuoteMentionedUserText).flat( - Infinity + var textBodySpan = [vars.useOldStyleReply ? oldStyleReplyTo : ""]; + if (t.quoted_status.full_text) { + // XXX: renderTweetBodyHTML returns html. To refactor later. + textBodySpan.push( + htmlToNodes(await renderTweetBodyHTML(t, true)).content + ); + } + const bodyElement = elNew( + "span", + { + className: + "tweet-body-text tweet-body-text-quote tweet-body-text-long", + style: "color:var(--default-text-color)!important", + }, + textBodySpan ); - oldStyleReplyTo = elNew( - "span", - { className: "tweet-reply-to tweet-quote-reply-to" }, - textFragments + // Solve for media contents + var mediaElement = null; + if ( + t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ) { + const mediaMap = t.quoted_status.extended_entities.media.map((m) => { + const [w, h] = quoteSizeFunctions[ + t.quoted_status.extended_entities.media.length + ](m.original_info.width, m.original_info.height); + const mediaElement = elNew(m.type === "photo" ? "img" : "video", { + alt: escapeHTML(m.ext_alt_text, true), + title: escapeHTML(m.ext_alt_text, true), + crossorigin: "anonymous", + width: w, + height: h, + loading: "lazy", + }); + if (m.type == "video") { + mediaElement.disableRemotePlayback = true; + mediaElement.controls = true; + } + if (m.type == "animated_gif") { + mediaElement.disableRemotePlayback = true; + mediaElement.loop = true; + mediaElement.defaultMuted = true; + mediaElement.muted = true; + // XXX: To check if this works. + mediaElement.onclick = + "if(this.paused) this.play(); else this.pause()"; + if (!vars.disableGifAutoplay) mediaElement.autoplay = true; + } + if (m.type === "photo") { + var base_url = m.media_url_https; + if ( + vars.showOriginalImages && + (m.media_url_https.endsWith(".jpg") || + m.media_url_https.endsWith(".png")) + ) { + base_url += "?name=orig"; + } else if ( + window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ) { + base_url += "?name=small"; + } + mediaElement.src = base_url; + } else { + mediaElement.src = m.video_info.variants.find( + (v) => v.content_type === "video/mp4" + ).url; + } + mediaElement.classList.add( + "tweet-media-element", + "tweet-media-element-quote" + ); + if (m.type === "animated_gif") { + mediaElement.classList.add("tweet-media-element-quote-gif"); + } + mediaElement.classList.add( + mediaClasses[t.quoted_status.extended_entities.media.length] + ); + if ( + !vars.displaySensitiveContent && + t.quoted_status.possibly_sensitive + ) { + mediaElement.classList.add("tweet-media-element-censor"); + } + return mediaElement; + }); + mediaElement = elNew( + "div", + { className: "tweet-media-quote" }, + mediaMap + ); + } + + const rootAHref = elNew( + "a", + { + className: "tweet-body-quote", + target: "_blank", + href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, + }, + [ + profileElement, + // Header + nameElement, + // Tweet Time Quote + tweetTimeElement, + oldStyleReplyTo, + bodyElement, + mediaElement, + !isQuoteMatchingLanguage + ? elNew( + "span", + { + className: + "tweet-button tweet-quote-translate tweet-button", + }, + [LOC.view_translation.message] + ) + : ``, + ] ); - } - - var textBodySpan = [vars.useOldStyleReply ? oldStyleReplyTo : ""]; - if (t.quoted_status.full_text) { - // XXX: renderTweetBodyHTML returns html. To refactor later. - textBodySpan.push(htmlToNodes(await renderTweetBodyHTML(t, true)).content); - } - const bodyElement = elNew( - "span", - { - className: "tweet-body-text tweet-body-text-quote tweet-body-text-long", - style: "color:var(--default-text-color)!important", - }, - textBodySpan - ); - - // Solve for media contents - var mediaElement = null; - if ( - t.quoted_status.extended_entities && - t.quoted_status.extended_entities.media - ) { - const mediaMap = t.quoted_status.extended_entities.media.map((m) => { - const [w, h] = quoteSizeFunctions[ - t.quoted_status.extended_entities.media.length - ](m.original_info.width, m.original_info.height); - const mediaElement = elNew(m.type === "photo" ? "img" : "video", { - alt: escapeHTML(m.ext_alt_text, true), - title: escapeHTML(m.ext_alt_text, true), - crossorigin: "anonymous", - width: w, - height: h, - loading: "lazy", - }); - if (m.type == "video") { - mediaElement.disableRemotePlayback = true; - mediaElement.controls = true; - } - if (m.type == "animated_gif") { - mediaElement.disableRemotePlayback = true; - mediaElement.loop = true; - mediaElement.defaultMuted = true; - mediaElement.muted = true; - // XXX: To check if this works. - mediaElement.onclick = "if(this.paused) this.play(); else this.pause()"; - if (!vars.disableGifAutoplay) mediaElement.autoplay = true; - } - if (m.type === "photo") { - var base_url = m.media_url_https; - if ( - vars.showOriginalImages && - (m.media_url_https.endsWith(".jpg") || - m.media_url_https.endsWith(".png")) - ) { - base_url += "?name=orig"; - } else if ( - window.navigator && - navigator.connection && - navigator.connection.type === "cellular" && - !vars.disableDataSaver - ) { - base_url += "?name=small"; - } - mediaElement.src = base_url; - } else { - mediaElement.src = m.video_info.variants.find( - (v) => v.content_type === "video/mp4" - ).url; - } - mediaElement.classList.add( - "tweet-media-element", - "tweet-media-element-quote" - ); - if (m.type === "animated_gif") { - mediaElement.classList.add("tweet-media-element-quote-gif"); - } - mediaElement.classList.add( - mediaClasses[t.quoted_status.extended_entities.media.length] - ); - if (!vars.displaySensitiveContent && t.quoted_status.possibly_sensitive) { - mediaElement.classList.add("tweet-media-element-censor"); - } - return mediaElement; - }); - mediaElement = elNew("div", { className: "tweet-media-quote" }, mediaMap); - } - - const rootAHref = elNew( - "a", - { - className: "tweet-body-quote", - target: "_blank", - href: `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`, - }, - [ - profileElement, - // Header - nameElement, - // Tweet Time Quote - tweetTimeElement, - oldStyleReplyTo, - bodyElement, - mediaElement, - !isQuoteMatchingLanguage - ? elNew( - "span", - { className: "tweet-button tweet-quote-translate tweet-button" }, - [LOC.view_translation.message] - ) - : ``, - ] - ); - if (!asNode) return rootAHref.outerHTML; - else { - return rootAHref; - } + if (!asNode) return rootAHref.outerHTML; + else { + return rootAHref; + } } /** @@ -416,1306 +440,1394 @@ async function constructQuotedTweet( * @returns */ async function constructTweet(t, tweetConstructorArgs, options = {}) { - const _tweetTopConst = elNew("div", { className: "tweet-top", hidden: true }); - - const defaultProfilePicture = vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_bigger.png` - ) - : "https://abs.twimg.com/sticky/default_profile_images/default_profile_bigger.png"; - - const profilePicture = ( - t.user.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(t.user.id_str) % 7 - }_normal.png` - ) - : t.user.profile_image_url_https - ).replace("_normal.", "_bigger."); - - const tweetDateObject = new Date(t.created_at); - const tweetTimestamp = tweetDateObject.getTime(); - - const avatarImage = elNew( - "a", - { className: "tweet-avatar-link", href: `/${t.user.screen_name}` }, - [ - elNew("img", { - onerror: `this.src = '${defaultProfilePicture}'`, - src: profilePicture, - alt: t.user.name, - className: "tweet-avatar", - width: 48, - height: 48, - }), - ] - ); - - // tweetHeaderName - let tweetHeaderClass = ["tweet-header-name"]; - - if (options.mainTweet) { - tweetHeaderClass.push("tweet-header-name-main"); - } - - if (t.user.verified || t.user.verified_type) { - tweetHeaderClass.push("user-verified"); - } else if (t.user.id_str === "1708130407663759360") { - tweetHeaderClass.push("user-verified", "user-verified-dimden"); // Add both classes - } - - if (t.user.protected) { - tweetHeaderClass.push("user-protected"); - } - - if (t.user.verified_type === "Government") { - tweetHeaderClass.push("user-verified-gray"); - } else if (t.user.verified_type === "Business") { - tweetHeaderClass.push("user-verified-yellow"); - } else if (t.user.verified_type === "Blue") { - tweetHeaderClass.push("user-verified-blue"); - } - - const screenUsername = elNew( - "a", - { - className: [ - "tweet-header-info", - options.mainTweet ? "tweet-header-info-main" : "", - ], - href: `/${t.user.screen_name}`, - }, - // The Screen / Display name - [ - elNew( - "b", - { - title: - t.user.id_str === "1708130407663759360" - ? "Old Twitter Layout extension developer" - : "", - className: tweetHeaderClass, - }, - [escapeHTML(t.user.name)] - ), - " ", - // @Handle - elNew("span", { className: "tweet-header-handle" }, [ - "@" + escapeHTML(t.user.screen_name), - ]), - ] - ); - - var titleTime = null; - if (tweetShortishTimeformatter) { - titleTime = tweetShortishTimeformatter.format(tweetDateObject); - } else { - titleTime = tweetDateObject.toLocaleString(); - } - - let tweetHeaderBlock = [ - // The Screen & username block. - screenUsername, - " ", - // Tweet Time Element - elNew( - "a", - { - className: "tweet-time", - hidden: options.mainTweet ? true : false, - dataset: { - timestamp: tweetTimestamp, - }, - title: titleTime, - href: `/${t.user.screen_name}/status/${t.id_str}`, - }, - [timeElapsed(tweetTimestamp)] - ), - ]; - // If it's the main tweet, add a follow button. - if (options.mainTweet && t.user.id_str !== user.id_str) { - tweetHeaderBlock.push( - elNew( - "button", - { - className: [ - "tweet-button", - "nice-button", - "tweet-header-follow", - t.user.following ? "following" : "follow", - ], - }, - [t.user.following ? LOC.following_btn.message : LOC.follow.message] - ) - ); - } - // translate icon for timelines. - if (!options.mainTweet && !tweetConstructorArgs.isMatchingLanguage) { - tweetHeaderBlock.push( - elNew("span", { className: ["tweet-translate-after", "tweet-button"] }, [ - `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && - innerWidth > 650 - ? LOC.view_translation.message - : null, - ]) - ); - } - - const tweetTopConst = elNew("template", {}, [ - _tweetTopConst, - avatarImage, - elNew( - "div", - { - className: [ - "tweet-header", - options.mainTweet ? "tweet-header-main" : "", - ], - }, - tweetHeaderBlock - ), - ]); - - // mentionText - const doMentionText = - // tweetConstructorArgs.mentionedUserText !== `` && - tweetConstructorArgs.mentionedUserTextArray.length > 0 && - !options.threadContinuation && - !options.noTop && - !location.pathname.includes("/status/") && - !vars.useOldStyleReply; - var mentioned_node = null; - if (doMentionText) { - // Taken from StackOverflow once again. - tweetConstructorArgs.mentionedUserTextArray = interleave( - tweetConstructorArgs.mentionedUserTextArray, - LOC.replying_to_comma.message - ); + const _tweetTopConst = elNew("div", { + className: "tweet-top", + hidden: true, + }); - // XXX: In theory, this should be the right order. - // Previously it was [` and ` | `, ` | `, ` | ...] - // but it should be: [`, ` | `, ` ... ` and `] - // But I cannot be 100% sure about this. - Ristellise. - if (tweetConstructorArgs.mentionedUserTextArray.length >= 5) { - const arrLength = tweetConstructorArgs.mentionedUserTextArray.length; - tweetConstructorArgs.mentionedUserTextArray[arrLength - 2] = - LOC.replying_to_and.message; - } - // XXX: This is actually missing a `LOC.replying_to_user.message`. - // Will need to figure out how to replace it. + const defaultProfilePicture = vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_bigger.png` + ) + : "https://abs.twimg.com/sticky/default_profile_images/default_profile_bigger.png"; + + const profilePicture = ( + t.user.default_profile_image && vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(t.user.id_str) % 7 + }_normal.png` + ) + : t.user.profile_image_url_https + ).replace("_normal.", "_bigger."); - var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); - textFragments = interleave( - textFragments, - tweetConstructorArgs.mentionedUserTextArray - ).flat(Infinity); + const tweetDateObject = new Date(t.created_at); + const tweetTimestamp = tweetDateObject.getTime(); - mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ - elNew("span", {}, textFragments), - ]); - } - - // Main text content - const longShortClass = - vars.noBigFont || - t.full_text.length > 280 || - !options.bigFont || - (!options.mainTweet && location.pathname.includes("/status/")) - ? "tweet-body-text-long" - : "tweet-body-text-short"; - - var bodyTextChildren = []; - if (vars.useOldStyleReply) { - bodyTextChildren.push( - ...interleave(tweetConstructorArgs.mentionedUserTextArray, " ") + const avatarImage = elNew( + "a", + { className: "tweet-avatar-link", href: `/${t.user.screen_name}` }, + [ + elNew("img", { + onerror: `this.src = '${defaultProfilePicture}'`, + src: profilePicture, + alt: t.user.name, + className: "tweet-avatar", + width: 48, + height: 48, + }), + ] ); - } - if (tweetConstructorArgs.full_text) { - if (bodyTextChildren.length > 0) { - bodyTextChildren.push(" "); + + // tweetHeaderName + let tweetHeaderClass = ["tweet-header-name"]; + + if (options.mainTweet) { + tweetHeaderClass.push("tweet-header-name-main"); } - // XXX: renderTweetBodyHTML returns html. To refactor later. - bodyTextChildren.push(htmlToNodes(await renderTweetBodyHTML(t)).content); - } - - const body_node = elNew( - "div", - { - lang: t.lang, - classList: ["tweet-body-text", longShortClass], - }, - [elNew("span", { class: ["tweet-body-text-span"] }, bodyTextChildren)] - ); - - // render media content elements - var extended_media_nodes = null; - if (t.extended_entities && t.extended_entities.media) { - // videoOverlay if it's only a single video. - const isSingleVideo = - t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "video" - ? true - : false; - var videoOverlay = null; - if (isSingleVideo) { - videoOverlay = elNew( - "div", - { class: ["tweet-media-video-overlay", "tweet-button"] }, - // XXX: htmlToNodes needs to always be called, else .content will be null/undefined. - [htmlToNodes(svgPlayIcon).content] - ); + + if (t.user.verified || t.user.verified_type) { + tweetHeaderClass.push("user-verified"); + } else if (t.user.id_str === "1708130407663759360") { + tweetHeaderClass.push("user-verified", "user-verified-dimden"); // Add both classes } - // Render media nodes - var mediaNodes = renderMultiMediaNodes(t); - - //
GIF
- var gifControl = - t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ? elNew("div", { class: "tweet-media-controls" }, ["GIF"]) - : null; - var videoControls = null; - if (tweetConstructorArgs.videos) { - var viewNode = null; - if ( - tweetConstructorArgs.videos[0].ext && - tweetConstructorArgs.videos[0].ext.mediaStats && - tweetConstructorArgs.videos[0].ext.mediaStats.r && - tweetConstructorArgs.videos[0].ext.mediaStats.r.ok - ) { - viewNode = Number( - tweetConstructorArgs.videos[0].ext.mediaStats.r.ok.viewCount - ); - viewNode = elNew( - "span", - { class: ["tweet-video-views", "tweet-button"] }, - [ - `${viewNode.toLocaleString().replace(/\s/g, ",")} ${ - LOC.views.message - } • `, - ] - ); - } - const reloadNode = elNew( - "span", - { class: ["tweet-video-reload", "tweet-button"] }, - [LOC.reload.message] - ); - var resolutionNodes = tweetConstructorArgs.videos[0].video_info.variants - .filter((v) => v.bitrate) - .map((v) => - elNew( - "span", - { - class: ["tweet-video-quality", "tweet-button"], - dataset: { url: v.url }, - }, - [v.url.match(/\/(\d+)x/)[1] + "p"] - ) - ); - videoControls = elNew("div", { class: "tweet-media-controls" }, [ - viewNode, - reloadNode, - " • ", - ...interleave(resolutionNodes, " / "), - ]); + if (t.user.protected) { + tweetHeaderClass.push("user-protected"); } - // console.log([videoOverlay, ...mediaNodes]); - extended_media_nodes = [ - elNew("div", { class: ["tweet-media"] }, [videoOverlay, ...mediaNodes]), - gifControl, - videoControls, - elNew("span", { class: "tweet-media-data" }), - ]; - } - - // var extended_media = ""; - // if (extended_node) { - // [].forEach.call(extended_node.children, function (el) { - // extended_media += el.outerHTML; - // }); - // } - - // card placeholder if there is a card to placeholder. - // const card = t.card ? `
` : ""; - const card_node = t.card ? elNew("div", { class: ["tweet-card"] }) : null; - // quoted tweet status. - let quoted_node = ""; - if (t.quoted_status) { - quoted_node = await constructQuotedTweet( - t, - tweetConstructorArgs.isQuoteMatchingLanguage, - tweetConstructorArgs.quoteMentionedUserText, - tweetConstructorArgs.newQuoteMentionedUserText, - true - ); - } - - // limited text - var limited_node = null; - if ( - t.limited_actions === "limit_trusted_friends_tweet" && - (options.mainTweet || !location.pathname.includes("/status/")) - ) { - let screen_name = null; - if (tweet.trusted_circle_owner) { - screen_name = tweet.trusted_circle_owner; - } else if (tweetStorage[t.conversation_id_str]) { - screen_name = tweetStorage[t.conversation_id_str].user.screen_name; - } else if (t.in_reply_to_screen_name) { - screen_name = t.in_reply_to_screen_name; - } else { - screen_name = t.user.screen_name; + + if (t.user.verified_type === "Government") { + tweetHeaderClass.push("user-verified-gray"); + } else if (t.user.verified_type === "Business") { + tweetHeaderClass.push("user-verified-yellow"); + } else if (t.user.verified_type === "Blue") { + tweetHeaderClass.push("user-verified-blue"); } - limited_node = elNew("div", { class: ["tweet-limited"] }, [ - LOC.circle_limited_tweet.message.replace("$SCREEN_NAME$", screen_name), - elNew( + + const screenUsername = elNew( "a", { - href: "https://help.twitter.com/en/using-twitter/twitter-circle", - target: "blank", + className: [ + "tweet-header-info", + options.mainTweet ? "tweet-header-info-main" : "", + ], + href: `/${t.user.screen_name}`, }, - [LOC.learn_more.message] - ), - ]); + // The Screen / Display name + [ + elNew( + "b", + { + title: + t.user.id_str === "1708130407663759360" + ? "Old Twitter Layout extension developer" + : "", + className: tweetHeaderClass, + }, + [escapeHTML(t.user.name)] + ), + " ", + // @Handle + elNew("span", { className: "tweet-header-handle" }, [ + "@" + escapeHTML(t.user.screen_name), + ]), + ] + ); - // limited = ` - //
- // `.replace( - // "$SCREEN_NAME$", - // screen_name - // ); - } - // tombstoned - const tombstone_node = t.tombstone - ? elNew("div", { class: ["tweet-warning"] }, [t.tombstone]) - : null; - // country restricted text. - var country_restrictions = null; - if ( - (t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY"))) || - t.withheld_scope - ) { - country_restrictions = country_restriction_node; - } - // const country_restrictions = - // (t.withheld_in_countries && - // (t.withheld_in_countries.includes("XX") || - // t.withheld_in_countries.includes("XY"))) || - // t.withheld_scope - // ? `
- // This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` - // : ""; - - var conversation_control_node = null; - if (t.conversation_control) { - const limitedActions = t.limited_actions_text - ? t.limited_actions_text - : LOC.limited_tweet.message; - var replyText = - t.conversation_control.policy && - (t.user.id_str === user.id_str || - (t.conversation_control.policy.toLowerCase() === "community" && - (t.user.followed_by || - (tweetConstructorArgs.full_text && - tweetConstructorArgs.full_text.includes( - `@${user.screen_name}` - )))) || - (t.conversation_control.policy.toLowerCase() === "by_invitation" && - tweetConstructorArgs.full_text && - tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))); - if (replyText) { - replyText = " " + LOC.you_can_reply.message; + var titleTime = null; + if (tweetShortishTimeformatter) { + titleTime = tweetShortishTimeformatter.format(tweetDateObject); } else { - replyText = ""; + titleTime = tweetDateObject.toLocaleString(); } - conversation_control_node = elNew("div", { class: "tweet-warning" }, [ - limitedActions, - replyText, - ]); - } - - // const conversation_control = t.conversation_control - // ? `
${ - // t.limited_actions_text - // ? t.limited_actions_text - // : LOC.limited_tweet.message - // }${ - // t.conversation_control.policy && - // (t.user.id_str === user.id_str || - // (t.conversation_control.policy.toLowerCase() === "community" && - // (t.user.followed_by || - // (tweetConstructorArgs.full_text && - // tweetConstructorArgs.full_text.includes( - // `@${user.screen_name}` - // )))) || - // (t.conversation_control.policy.toLowerCase() === "by_invitation" && - // tweetConstructorArgs.full_text && - // tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))) - // ? " " + LOC.you_can_reply.message - // : "" - // }
` - // : ""; - - var tweet_footer_node = ""; - if (options.mainTweet) { - var statsArray = []; - // Replies - statsArray.push( - elNew( - "a", - { - href: `${t.user.screen_name}/status/${t.id_str}`, - class: ["tweet-footer-stat", "tweet-footer-stat-o"], - }, - [ - elNew("span", { class: "tweet-footer-stat-text" }, [ - LOC.replies.message, - ]), - elNew( - "b", + + let tweetHeaderBlock = [ + // The Screen & username block. + screenUsername, + " ", + // Tweet Time Element + elNew( + "a", { - class: ["tweet-footer-stat-count", "tweet-footer-stat-replies"], + className: "tweet-time", + hidden: options.mainTweet ? true : false, + dataset: { + timestamp: tweetTimestamp, + }, + title: titleTime, + href: `/${t.user.screen_name}/status/${t.id_str}`, }, - [formatLargeNumber(t.reply_count).replace(/\s/g, ",")] - ), - ] - ) - ); - // Retweets - statsArray.push( - elNew( - "a", - { - href: `${t.user.screen_name}/status/${t.id_str}/retweets`, - class: ["tweet-footer-stat", "tweet-footer-stat-o"], - }, - [ - elNew("span", { class: "tweet-footer-stat-text" }, [ - LOC.retweets.message, - ]), - elNew( - "b", + [timeElapsed(tweetTimestamp)] + ), + ]; + // If it's the main tweet, add a follow button. + if (options.mainTweet && t.user.id_str !== user.id_str) { + tweetHeaderBlock.push( + elNew( + "button", + { + className: [ + "tweet-button", + "nice-button", + "tweet-header-follow", + t.user.following ? "following" : "follow", + ], + }, + [ + t.user.following + ? LOC.following_btn.message + : LOC.follow.message, + ] + ) + ); + } + // translate icon for timelines. + if (!options.mainTweet && !tweetConstructorArgs.isMatchingLanguage) { + tweetHeaderBlock.push( + elNew( + "span", + { className: ["tweet-translate-after", "tweet-button"] }, + [ + `${t.user.name} ${t.user.screen_name} 1 Sept`.length < 40 && + innerWidth > 650 + ? LOC.view_translation.message + : null, + ] + ) + ); + } + + const tweetTopConst = elNew("template", {}, [ + _tweetTopConst, + avatarImage, + elNew( + "div", { - class: ["tweet-footer-stat-count", "tweet-footer-stat-r"], + className: [ + "tweet-header", + options.mainTweet ? "tweet-header-main" : "", + ], }, - [formatLargeNumber(t.retweet_count).replace(/\s/g, ",")] - ), - ] - ) + tweetHeaderBlock + ), + ]); + + // mentionText + const doMentionText = + // tweetConstructorArgs.mentionedUserText !== `` && + tweetConstructorArgs.mentionedUserTextArray.length > 0 && + !options.threadContinuation && + !options.noTop && + !location.pathname.includes("/status/") && + !vars.useOldStyleReply; + var mentioned_node = null; + if (doMentionText) { + // Taken from StackOverflow once again. + tweetConstructorArgs.mentionedUserTextArray = interleave( + tweetConstructorArgs.mentionedUserTextArray, + LOC.replying_to_comma.message + ); + + // XXX: In theory, this should be the right order. + // Previously it was [` and ` | `, ` | `, ` | ...] + // but it should be: [`, ` | `, ` ... ` and `] + // But I cannot be 100% sure about this. - Ristellise. + if (tweetConstructorArgs.mentionedUserTextArray.length >= 5) { + const arrLength = + tweetConstructorArgs.mentionedUserTextArray.length; + tweetConstructorArgs.mentionedUserTextArray[arrLength - 2] = + LOC.replying_to_and.message; + } + // XXX: This is actually missing a `LOC.replying_to_user.message`. + // Will need to figure out how to replace it. + + var textFragments = LOC.replying_to_user.message.split("$SCREEN_NAME$"); + textFragments = interleave( + textFragments, + tweetConstructorArgs.mentionedUserTextArray + ).flat(Infinity); + + mentioned_node = elNew("div", { className: "tweet-reply-to" }, [ + elNew("span", {}, textFragments), + ]); + } + + // Main text content + const longShortClass = + vars.noBigFont || + t.full_text.length > 280 || + !options.bigFont || + (!options.mainTweet && location.pathname.includes("/status/")) + ? "tweet-body-text-long" + : "tweet-body-text-short"; + + var bodyTextChildren = []; + if (vars.useOldStyleReply) { + bodyTextChildren.push( + ...interleave(tweetConstructorArgs.mentionedUserTextArray, " ") + ); + } + if (tweetConstructorArgs.full_text) { + if (bodyTextChildren.length > 0) { + bodyTextChildren.push(" "); + } + // XXX: renderTweetBodyHTML returns html. To refactor later. + bodyTextChildren.push( + htmlToNodes(await renderTweetBodyHTML(t)).content + ); + } + + const body_node = elNew( + "div", + { + lang: t.lang, + classList: ["tweet-body-text", longShortClass], + }, + [elNew("span", { class: ["tweet-body-text-span"] }, bodyTextChildren)] ); + + // render media content elements + var extended_media_nodes = null; + if (t.extended_entities && t.extended_entities.media) { + // videoOverlay if it's only a single video. + const isSingleVideo = + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? true + : false; + var videoOverlay = null; + if (isSingleVideo) { + videoOverlay = elNew( + "div", + { class: ["tweet-media-video-overlay", "tweet-button"] }, + // XXX: htmlToNodes needs to always be called, else .content will be null/undefined. + [htmlToNodes(svgPlayIcon).content] + ); + } + // Render media nodes + var mediaNodes = renderMultiMediaNodes(t); + + //
GIF
+ var gifControl = + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ? elNew("div", { class: "tweet-media-controls" }, ["GIF"]) + : null; + var videoControls = null; + if (tweetConstructorArgs.videos) { + var viewNode = null; + if ( + tweetConstructorArgs.videos[0].ext && + tweetConstructorArgs.videos[0].ext.mediaStats && + tweetConstructorArgs.videos[0].ext.mediaStats.r && + tweetConstructorArgs.videos[0].ext.mediaStats.r.ok + ) { + viewNode = Number( + tweetConstructorArgs.videos[0].ext.mediaStats.r.ok.viewCount + ); + viewNode = elNew( + "span", + { class: ["tweet-video-views", "tweet-button"] }, + [ + `${viewNode.toLocaleString().replace(/\s/g, ",")} ${ + LOC.views.message + } • `, + ] + ); + } + + const reloadNode = elNew( + "span", + { class: ["tweet-video-reload", "tweet-button"] }, + [LOC.reload.message] + ); + var resolutionNodes = + tweetConstructorArgs.videos[0].video_info.variants + .filter((v) => v.bitrate) + .map((v) => + elNew( + "span", + { + class: ["tweet-video-quality", "tweet-button"], + dataset: { url: v.url }, + }, + [v.url.match(/\/(\d+)x/)[1] + "p"] + ) + ); + videoControls = elNew("div", { class: "tweet-media-controls" }, [ + viewNode, + reloadNode, + " • ", + ...interleave(resolutionNodes, " / "), + ]); + } + // console.log([videoOverlay, ...mediaNodes]); + extended_media_nodes = [ + elNew("div", { class: ["tweet-media"] }, [ + videoOverlay, + ...mediaNodes, + ]), + gifControl, + videoControls, + elNew("span", { class: "tweet-media-data" }), + ]; + } + + // var extended_media = ""; + // if (extended_node) { + // [].forEach.call(extended_node.children, function (el) { + // extended_media += el.outerHTML; + // }); + // } + + // card placeholder if there is a card to placeholder. + // const card = t.card ? `
` : ""; + const card_node = t.card ? elNew("div", { class: ["tweet-card"] }) : null; + // quoted tweet status. + let quoted_node = ""; + if (t.quoted_status) { + quoted_node = await constructQuotedTweet( + t, + tweetConstructorArgs.isQuoteMatchingLanguage, + tweetConstructorArgs.quoteMentionedUserText, + tweetConstructorArgs.newQuoteMentionedUserText, + true + ); + } + + // limited text + var limited_node = null; if ( - vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 + t.limited_actions === "limit_trusted_friends_tweet" && + (options.mainTweet || !location.pathname.includes("/status/")) ) { - statsArray.push( - elNew( - "a", - { - href: `${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`, - class: ["tweet-footer-stat", "tweet-footer-stat-q"], - }, - [ - elNew("span", { class: "tweet-footer-stat-text" }, [ - LOC.quotes.message, - ]), + let screen_name = null; + if (tweet.trusted_circle_owner) { + screen_name = tweet.trusted_circle_owner; + } else if (tweetStorage[t.conversation_id_str]) { + screen_name = tweetStorage[t.conversation_id_str].user.screen_name; + } else if (t.in_reply_to_screen_name) { + screen_name = t.in_reply_to_screen_name; + } else { + screen_name = t.user.screen_name; + } + limited_node = elNew("div", { class: ["tweet-limited"] }, [ + LOC.circle_limited_tweet.message.replace( + "$SCREEN_NAME$", + screen_name + ), elNew( - "b", - { - class: ["tweet-footer-stat-count", "tweet-footer-stat-quotes"], - }, - [formatLargeNumber(t.quote_count).replace(/\s/g, ",")] + "a", + { + href: "https://help.twitter.com/en/using-twitter/twitter-circle", + target: "blank", + }, + [LOC.learn_more.message] ), - ] - ) - ); + ]); + + // limited = ` + //
+ // ${LOC.circle_limited_tweet.message} + // ${LOC.learn_more.message} + //
+ // `.replace( + // "$SCREEN_NAME$", + // screen_name + // ); + } + // tombstoned + const tombstone_node = t.tombstone + ? elNew("div", { class: ["tweet-warning"] }, [t.tombstone]) + : null; + // country restricted text. + var country_restrictions = null; + if ( + (t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY"))) || + t.withheld_scope + ) { + country_restrictions = country_restriction_node; + } + // const country_restrictions = + // (t.withheld_in_countries && + // (t.withheld_in_countries.includes("XX") || + // t.withheld_in_countries.includes("XY"))) || + // t.withheld_scope + // ? `
+ // This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` + // : ""; + + var conversation_control_node = null; + if (t.conversation_control) { + const limitedActions = t.limited_actions_text + ? t.limited_actions_text + : LOC.limited_tweet.message; + var replyText = + t.conversation_control.policy && + (t.user.id_str === user.id_str || + (t.conversation_control.policy.toLowerCase() === "community" && + (t.user.followed_by || + (tweetConstructorArgs.full_text && + tweetConstructorArgs.full_text.includes( + `@${user.screen_name}` + )))) || + (t.conversation_control.policy.toLowerCase() === + "by_invitation" && + tweetConstructorArgs.full_text && + tweetConstructorArgs.full_text.includes( + `@${user.screen_name}` + ))); + if (replyText) { + replyText = " " + LOC.you_can_reply.message; + } else { + replyText = ""; + } + conversation_control_node = elNew("div", { class: "tweet-warning" }, [ + limitedActions, + replyText, + ]); + } + + // const conversation_control = t.conversation_control + // ? `
${ + // t.limited_actions_text + // ? t.limited_actions_text + // : LOC.limited_tweet.message + // }${ + // t.conversation_control.policy && + // (t.user.id_str === user.id_str || + // (t.conversation_control.policy.toLowerCase() === "community" && + // (t.user.followed_by || + // (tweetConstructorArgs.full_text && + // tweetConstructorArgs.full_text.includes( + // `@${user.screen_name}` + // )))) || + // (t.conversation_control.policy.toLowerCase() === "by_invitation" && + // tweetConstructorArgs.full_text && + // tweetConstructorArgs.full_text.includes(`@${user.screen_name}`))) + // ? " " + LOC.you_can_reply.message + // : "" + // }
` + // : ""; + + var tweet_footer_node = ""; + if (options.mainTweet) { + var statsArray = []; + // Replies + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}`, + class: ["tweet-footer-stat", "tweet-footer-stat-o"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.replies.message, + ]), + elNew( + "b", + { + class: [ + "tweet-footer-stat-count", + "tweet-footer-stat-replies", + ], + }, + [formatLargeNumber(t.reply_count).replace(/\s/g, ",")] + ), + ] + ) + ); + // Retweets + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/retweets`, + class: ["tweet-footer-stat", "tweet-footer-stat-o"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.retweets.message, + ]), + elNew( + "b", + { + class: [ + "tweet-footer-stat-count", + "tweet-footer-stat-r", + ], + }, + [formatLargeNumber(t.retweet_count).replace(/\s/g, ",")] + ), + ] + ) + ); + if ( + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ) { + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/retweets/with_comments`, + class: ["tweet-footer-stat", "tweet-footer-stat-q"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + LOC.quotes.message, + ]), + elNew( + "b", + { + class: [ + "tweet-footer-stat-count", + "tweet-footer-stat-quotes", + ], + }, + [ + formatLargeNumber(t.quote_count).replace( + /\s/g, + "," + ), + ] + ), + ] + ) + ); + } + // Likes + statsArray.push( + elNew( + "a", + { + href: `${t.user.screen_name}/status/${t.id_str}/likes`, + class: ["tweet-footer-stat", "tweet-footer-stat-f"], + }, + [ + elNew("span", { class: "tweet-footer-stat-text" }, [ + vars.heartsNotStars + ? LOC.likes.message + : LOC.favorites.message, + ]), + elNew( + "b", + { + class: [ + "tweet-footer-stat-count", + "tweet-footer-stat-favorites", + ], + }, + [ + formatLargeNumber(t.favorite_count).replace( + /\s/g, + "," + ), + ] + ), + ] + ) + ); + + tweet_footer_node = elNew("div", { class: ["tweet-footer"] }, [ + elNew("div", { class: ["tweet-footer-stats"] }, statsArray), + ]); + } + var dt = []; + if (tweetTimeFormatter && tweetDateFormatter) { + dt = [ + tweetTimeFormatter.format(tweetDateObject), + " - ", + tweetDateFormatter.format(tweetDateObject), + ]; + } else { + dt = [ + tweetDateObject + .toLocaleTimeString(undefined, { + hour: "numeric", + minute: "numeric", + }) + .toLowerCase(), + " - ", + tweetDateObject.toLocaleDateString(undefined, { + day: "numeric", + month: "short", + year: "numeric", + }), + ]; } - // Likes - statsArray.push( - elNew( + + const tweet_date_node = elNew( "a", { - href: `${t.user.screen_name}/status/${t.id_str}/likes`, - class: ["tweet-footer-stat", "tweet-footer-stat-f"], + hidden: !options.mainTweet ? true : false, + class: ["tweet-date"], + title: tweetDateObject.toLocaleString(), + href: `/${t.user.screen_name}/status/${t.id_str}`, }, [ - elNew("span", { class: "tweet-footer-stat-text" }, [ - vars.heartsNotStars ? LOC.likes.message : LOC.favorites.message, - ]), - elNew( - "b", - { - class: ["tweet-footer-stat-count", "tweet-footer-stat-favorites"], - }, - [formatLargeNumber(t.favorite_count).replace(/\s/g, ",")] - ), + elNew("br"), + ...dt, + "  ・ ", + t.source ? t.source.split(">")[1].split("<")[0] : "Unknown", ] - ) ); - tweet_footer_node = elNew("div", { class: ["tweet-footer"] }, [ - elNew("div", { class: ["tweet-footer-stats"] }, statsArray), - ]); - } - var dt = []; - if (tweetTimeFormatter && tweetDateFormatter) { - dt = [ - tweetTimeFormatter.format(tweetDateObject), - " - ", - tweetDateFormatter.format(tweetDateObject), - ]; - } else { - dt = [ - tweetDateObject - .toLocaleTimeString(undefined, { hour: "numeric", minute: "numeric" }) - .toLowerCase(), - " - ", - tweetDateObject.toLocaleDateString(undefined, { - day: "numeric", - month: "short", - year: "numeric", - }), - ]; - } - - const tweet_date_node = elNew( - "a", - { - hidden: !options.mainTweet ? true : false, - class: ["tweet-date"], - title: tweetDateObject.toLocaleString(), - href: `/${t.user.screen_name}/status/${t.id_str}`, - }, - [ - elNew("br"), - ...dt, - "  ・ ", - t.source ? t.source.split(">")[1].split("<")[0] : "Unknown", - ] - ); - - // solve additional classes - var retweetClasses = ["tweet-button", "tweet-interact-retweet"]; - if (t.retweeted) { - retweetClasses.push("tweet-interact-retweeted"); - } - if ( - (t.user.protected || t.limited_actions === "limit_trusted_friends_tweet") && - t.user.id_str !== user.id_str - ) { - retweetClasses.push("tweet-interact-retweet-disabled"); - } - - var likeClasses = ["tweet-button", "tweet-interact-favorite"]; - if (t.favorited) { - likeClasses.push("tweet-interact-favorited"); - } - - var bookmarkClasses = ["tweet-button", "tweet-interact-bookmark"]; - if (t.favorited) { - bookmarkClasses.push("tweet-interact-bookmarked"); - } - - const viewsClasses = ["tweet-button","tweet-interact-views"] - // dropdown for retweet actions - var retweetDropdownArray = [ - elNew( - "span", - { - class: ["tweet-interact-retweet-menu-retweet"], - }, - [ - t.retweeted ? LOC.unretweet.message : LOC.retweet.message, - !vars.disableHotkeys ? " (T)" : "", - ] - ), - elNew( - "span", - { - class: ["tweet-interact-retweet-menu-quote"], - }, - [LOC.quote_tweet.message, !vars.disableHotkeys ? " (Q)" : ""] - ), - ]; - if (options.mainTweet) { - // If it's the main tweet, add quotes and retweeters - retweetDropdownArray.push( - elNew( - "span", - { - class: ["tweet-interact-retweet-menu-quotes"], - }, - [LOC.see_quotes_big.message] - ) - ); - retweetDropdownArray.push( - elNew( - "span", - { - class: ["tweet-interact-retweet-menu-retweeters"], - }, - [LOC.see_retweeters.message] - ) - ); - } - - var interactionArray = [ - // reply - elNew( - "span", - { - class: ["tweet-button", "tweet-interact-reply"], - title: `${LOC.reply_btn.message}${!vars.disableHotkeys ? " (R)" : ""}`, - dataset: { val: t.reply_count }, - }, - [ - options.mainTweet - ? null - : formatLargeNumber(t.reply_count).replace(/\s/g, ","), - ] - ), - // retweet - elNew( - "span", - { - class: retweetClasses, - title: `${LOC.retweet_btn.message}${ - !vars.disableHotkeys ? " (L)" : "" - }`, - dataset: { val: t.retweet_count }, - }, - [ - options.mainTweet - ? null - : formatLargeNumber(t.retweet_count).replace(/\s/g, ","), - ] - ), - // Retweet dropdown box - elNew( - "div", - { - class: ["tweet-interact-retweet-menu", "dropdown-menu"], - hidden: true, - }, - retweetDropdownArray - ), - // Like actions - elNew( - "span", - { - class: likeClasses, - title: `${ - vars.heartsNotStars ? LOC.like_btn.message : LOC.favorite_btn.message - }${!vars.disableHotkeys ? " (L)" : ""}`, - dataset: { val: t.favorite_count }, - }, - [ - options.mainTweet - ? null - : formatLargeNumber(t.favorite_count).replace(/\s/g, ","), - ] - ), - ]; - if ( - (vars.showBookmarkCount || options.mainTweet) && - typeof t.bookmark_count !== "undefined" - ) { - interactionArray.push( - // bookmark actions - elNew( - "span", - { - class: bookmarkClasses, - title: `${LOC.bookmarks_count.message}${ - !vars.disableHotkeys ? " (B)" : "" - }`, - dataset: { val: t.bookmark_count }, - }, - [formatLargeNumber(t.bookmark_count).replace(/\s/g, ",")] - ) - ); - } - if ( - vars.seeTweetViews && - t.ext && - t.ext.views && - t.ext.views.r && - t.ext.views.r.ok && - t.ext.views.r.ok.count - ) { - interactionArray.push( - // views - elNew( - "span", - { - class: viewsClasses, - title: LOC.views_count.message, - dataset: { val: t.ext.views.r.ok.count }, - }, - [formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ",")] - ) - ); - } - interactionArray.push( - elNew("span", { class: ["tweet-button", "tweet-interact-more"] }) - ); - // XXX: Figure out if we can/should use structuredClone - var dropDownMoreInteractionsArray = [ - elNew( - "span", - { class: ["tweet-interact-more-menu-separate"] }, - LOC.separate_text.message - ), - elNew( - "span", - { class: ["tweet-interact-more-menu-copy"] }, - LOC.copy_link.message - ), - elNew( - "span", - { class: ["tweet-interact-more-menu-embed"] }, - LOC.embed_tweet.message - ), - navigator.canShare - ? elNew( - "span", - { class: ["tweet-interact-more-menu-share"] }, - LOC.share_tweet.message - ) - : null, - elNew( - "span", - { class: ["tweet-interact-more-menu-share-dms"] }, - LOC.share_tweet_in_dms.message - ), - elNew( - "span", - { class: ["tweet-interact-more-menu-newtwitter"] }, - LOC.open_tweet_newtwitter.message - ), - ]; - if (t.user.id_str === user.id_str) { - dropDownMoreInteractionsArray.push("hr"); - dropDownMoreInteractionsArray.push( - elNew( - "span", - { class: ["tweet-interact-more-menu-analytics"] }, - LOC.tweet_analytics.message - ) - ); - dropDownMoreInteractionsArray.push( - elNew( - "span", - { class: ["tweet-interact-more-menu-delete"] }, - LOC.delete_tweet.message - ) - ); - if (typeof pageUser !== "undefined" && pageUser.id_str === user.id_str) { - dropDownMoreInteractionsArray.push( - elNew( - "span", - { class: ["tweet-interact-more-menu-pin"] }, - pinnedTweet && pinnedTweet.id_str === t.id_str - ? LOC.unpin_tweet.message - : LOC.pin_tweet.message - ) - ); + // solve additional classes + var retweetClasses = ["tweet-button", "tweet-interact-retweet"]; + if (t.retweeted) { + retweetClasses.push("tweet-interact-retweeted"); } - } - if ( - t.conversation_id_str && - tweetStorage[t.conversation_id_str] && - tweetStorage[t.conversation_id_str].user.id_str === user.id_str && - t.user.id_str !== user.id_str - ) { - dropDownMoreInteractionsArray.push( - elNew( - "span", - { class: ["tweet-interact-more-menu-hide"] }, - t.moderated ? LOC.unhide_tweet.message : LOC.hide_tweet.message - ) - ); - } - if (t.hasModeratedReplies) { - dropDownMoreInteractionsArray.push( - elNew("span", { class: ["tweet-interact-more-menu-hidden"] }, [ + if ( + (t.user.protected || + t.limited_actions === "limit_trusted_friends_tweet") && + t.user.id_str !== user.id_str + ) { + retweetClasses.push("tweet-interact-retweet-disabled"); + } + + var likeClasses = ["tweet-button", "tweet-interact-favorite"]; + if (t.favorited) { + likeClasses.push("tweet-interact-favorited"); + } + + var bookmarkClasses = ["tweet-button", "tweet-interact-bookmark"]; + if (t.favorited) { + bookmarkClasses.push("tweet-interact-bookmarked"); + } + + const viewsClasses = ["tweet-button", "tweet-interact-views"]; + // dropdown for retweet actions + var retweetDropdownArray = [ elNew( - "a", - { - target: "_blank", - href: `/${t.user.screen_name}/status/${t.id_str}/hidden?newtwitter=true`, - }, - LOC.see_hidden_replies.message + "span", + { + class: ["tweet-interact-retweet-menu-retweet"], + }, + [ + t.retweeted ? LOC.unretweet.message : LOC.retweet.message, + !vars.disableHotkeys ? " (T)" : "", + ] ), - ]) - ); - } - dropDownMoreInteractionsArray.push(elNew("hr")); - if (t.user.id_str !== user.id_str) { - if (!options.mainTweet) { - dropDownMoreInteractionsArray.push( elNew( - "span", - { - class: ["tweet-interact-more-menu-follow"], - hidden: t.user.blocking ? true : false, - }, - [ - t.user.following - ? tweetConstructorArgs.unfollowUserText - : tweetConstructorArgs.followUserText, - ] - ) - ); + "span", + { + class: ["tweet-interact-retweet-menu-quote"], + }, + [LOC.quote_tweet.message, !vars.disableHotkeys ? " (Q)" : ""] + ), + ]; + if (options.mainTweet) { + // If it's the main tweet, add quotes and retweeters + retweetDropdownArray.push( + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-quotes"], + }, + [LOC.see_quotes_big.message] + ) + ); + retweetDropdownArray.push( + elNew( + "span", + { + class: ["tweet-interact-retweet-menu-retweeters"], + }, + [LOC.see_retweeters.message] + ) + ); } - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-block"], - }, - [ - t.user.blocking - ? tweetConstructorArgs.unblockUserText - : tweetConstructorArgs.blockUserText, - ] - ) - ); - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-mute-user"], - }, - [ - t.user.muting - ? LOC.unmute_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ) - : LOC.mute_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ), - ] - ) - ); - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-lists-action"], - }, - LOC.from_list.message - ) - ); - } - if (!location.pathname.startsWith("/i/bookmarks")) { - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-bookmark"], - }, - t.bookmarked ? LOC.remove_bookmark.message : LOC.bookmark_tweet.message - ) - ); - } - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-mute"], - }, - t.conversation_muted ? LOC.unmute_convo.message : LOC.mute_convo.message - ) - ); - dropDownMoreInteractionsArray.push(elNew("hr")); - if (t.feedback) { - dropDownMoreInteractionsArray.push( - ...t.feedback.map((f, i) => { + + var interactionArray = [ + // reply elNew( - "span", - { - class: ["tweet-interact-more-menu-feedback"], - dataset: { index: i }, - }, - [f.prompt ? f.prompt : LOC.topic_not_interested.message] - ); - }) - ); - } - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-refresh"], - }, - LOC.refresh_tweet.message - ) - ); - if ( - t.extended_entities && - t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "animated_gif" - ) { - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-download-gif"], - dataset: { gifno: "1" }, - }, - LOC.download_gif.message - ) - ); - } - if (t.extended_entities && t.extended_entities.media.length > 1) { - dropDownMoreInteractionsArray.push( - ...t.extended_entities.media - .filter((m) => m.type === "animated_gif") - .map((m, i) => { - elNew( "span", { - class: ["tweet-interact-more-menu-download-gif"], - dataset: { gifno: i + 1 }, + class: ["tweet-button", "tweet-interact-reply"], + title: `${LOC.reply_btn.message}${ + !vars.disableHotkeys ? " (R)" : "" + }`, + dataset: { val: t.reply_count }, }, - `${LOC.download_gif.message} (#${i + 1})` - ); - }) - ); - } - if (t.extended_entities && t.extended_entities.media.length > 0) { - dropDownMoreInteractionsArray.push( - elNew( - "span", - { - class: ["tweet-interact-more-menu-download"], - }, - `${LOC.download_media.message}` - ) - ); - } - if (vars.developerMode) { - dropDownMoreInteractionsArray.push( - ...[ + [ + options.mainTweet + ? null + : formatLargeNumber(t.reply_count).replace(/\s/g, ","), + ] + ), + // retweet elNew( - "span", - { - class: ["tweet-interact-more-menu-copy-user-id"], - }, - LOC.copy_user_id.message + "span", + { + class: retweetClasses, + title: `${LOC.retweet_btn.message}${ + !vars.disableHotkeys ? " (L)" : "" + }`, + dataset: { val: t.retweet_count }, + }, + [ + options.mainTweet + ? null + : formatLargeNumber(t.retweet_count).replace(/\s/g, ","), + ] ), + // Retweet dropdown box elNew( - "span", - { - class: ["tweet-interact-more-menu-copy-tweet-id"], - }, - LOC.copy_tweet_id.message + "div", + { + class: ["tweet-interact-retweet-menu", "dropdown-menu"], + hidden: true, + }, + retweetDropdownArray ), + // Like actions elNew( - "span", - { - class: ["tweet-interact-more-menu-log"], - }, - "Log tweet object" + "span", + { + class: likeClasses, + title: `${ + vars.heartsNotStars + ? LOC.like_btn.message + : LOC.favorite_btn.message + }${!vars.disableHotkeys ? " (L)" : ""}`, + dataset: { val: t.favorite_count }, + }, + [ + options.mainTweet + ? null + : formatLargeNumber(t.favorite_count).replace(/\s/g, ","), + ] ), - ] - ); - } - - interactionArray.push( - elNew( - "div", - { class: ["tweet-interact-more-menu", "dropdown-menu"], hidden: true }, - dropDownMoreInteractionsArray - ) - ); - if ( - options.selfThreadButton && - t.self_thread && - t.self_thread.id_str && - !options.threadContinuation && - !location.pathname.includes("/status/") - ) { + ]; + if ( + (vars.showBookmarkCount || options.mainTweet) && + typeof t.bookmark_count !== "undefined" + ) { + interactionArray.push( + // bookmark actions + elNew( + "span", + { + class: bookmarkClasses, + title: `${LOC.bookmarks_count.message}${ + !vars.disableHotkeys ? " (B)" : "" + }`, + dataset: { val: t.bookmark_count }, + }, + [formatLargeNumber(t.bookmark_count).replace(/\s/g, ",")] + ) + ); + } + if ( + vars.seeTweetViews && + t.ext && + t.ext.views && + t.ext.views.r && + t.ext.views.r.ok && + t.ext.views.r.ok.count + ) { + interactionArray.push( + // views + elNew( + "span", + { + class: viewsClasses, + title: LOC.views_count.message, + dataset: { val: t.ext.views.r.ok.count }, + }, + [formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ",")] + ) + ); + } interactionArray.push( - elNew( - "a", - { - class: ["tweet-self-thread-button", "tweet-thread-right"], - target: "_blank", - href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, - }, - LOC.show_this_thread.message - ) + elNew("span", { class: ["tweet-button", "tweet-interact-more"] }) ); - } - if ( - !options.noTop && - !options.selfThreadButton && - t.in_reply_to_status_id_str && - !( - options.threadContinuation || - (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str) - ) && - !location.pathname.includes("/status/") - ) { - elNew( - "a", - { - class: ["tweet-self-thread-button", "tweet-thread-right"], - target: "_blank", - href: `/${t.in_reply_to_screen_name}/status/${t.in_reply_to_status_id_str}`, - }, - LOC.show_this_thread.message - ); - } - - // Tweet Interactions buttons - const interactions_node = elNew( - "div", - { class: ["tweet-interact"] }, - interactionArray - ); - - // tweet_interact = ; - - const tweet_reply_node = elNew( - "div", - { class: ["tweet-edit-section", "tweet-reply"], hidden: true }, - [ - elNew("br"), - elNew( - "b", - { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, - [ - `${LOC.replying_to_tweet.message} `, - elNew( + // XXX: Figure out if we can/should use structuredClone + var dropDownMoreInteractionsArray = [ + elNew( + "span", + { class: ["tweet-interact-more-menu-separate"] }, + LOC.separate_text.message + ), + elNew( + "span", + { class: ["tweet-interact-more-menu-copy"] }, + LOC.copy_link.message + ), + elNew( + "span", + { class: ["tweet-interact-more-menu-embed"] }, + LOC.embed_tweet.message + ), + navigator.canShare + ? elNew( + "span", + { class: ["tweet-interact-more-menu-share"] }, + LOC.share_tweet.message + ) + : null, + elNew( + "span", + { class: ["tweet-interact-more-menu-share-dms"] }, + LOC.share_tweet_in_dms.message + ), + elNew( + "span", + { class: ["tweet-interact-more-menu-newtwitter"] }, + LOC.open_tweet_newtwitter.message + ), + ]; + if (t.user.id_str === user.id_str) { + dropDownMoreInteractionsArray.push("hr"); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { class: ["tweet-interact-more-menu-analytics"] }, + LOC.tweet_analytics.message + ) + ); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { class: ["tweet-interact-more-menu-delete"] }, + LOC.delete_tweet.message + ) + ); + if ( + typeof pageUser !== "undefined" && + pageUser.id_str === user.id_str + ) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { class: ["tweet-interact-more-menu-pin"] }, + pinnedTweet && pinnedTweet.id_str === t.id_str + ? LOC.unpin_tweet.message + : LOC.pin_tweet.message + ) + ); + } + } + if ( + t.conversation_id_str && + tweetStorage[t.conversation_id_str] && + tweetStorage[t.conversation_id_str].user.id_str === user.id_str && + t.user.id_str !== user.id_str + ) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { class: ["tweet-interact-more-menu-hide"] }, + t.moderated ? LOC.unhide_tweet.message : LOC.hide_tweet.message + ) + ); + } + if (t.hasModeratedReplies) { + dropDownMoreInteractionsArray.push( + elNew("span", { class: ["tweet-interact-more-menu-hidden"] }, [ + elNew( + "a", + { + target: "_blank", + href: `/${t.user.screen_name}/status/${t.id_str}/hidden?newtwitter=true`, + }, + LOC.see_hidden_replies.message + ), + ]) + ); + } + dropDownMoreInteractionsArray.push(elNew("hr")); + if (t.user.id_str !== user.id_str) { + if (!options.mainTweet) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-follow"], + hidden: t.user.blocking ? true : false, + }, + [ + t.user.following + ? tweetConstructorArgs.unfollowUserText + : tweetConstructorArgs.followUserText, + ] + ) + ); + } + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-block"], + }, + [ + t.user.blocking + ? tweetConstructorArgs.unblockUserText + : tweetConstructorArgs.blockUserText, + ] + ) + ); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-mute-user"], + }, + [ + t.user.muting + ? LOC.unmute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + : LOC.mute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ), + ] + ) + ); + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-lists-action"], + }, + LOC.from_list.message + ) + ); + } + if (!location.pathname.startsWith("/i/bookmarks")) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-bookmark"], + }, + t.bookmarked + ? LOC.remove_bookmark.message + : LOC.bookmark_tweet.message + ) + ); + } + dropDownMoreInteractionsArray.push( + elNew( "span", { - title: !vars.disableHotkeys ? "ALT+M" : "", - class: ["tweet-reply-upload"], + class: ["tweet-interact-more-menu-mute"], }, - [LOC.upload_media_btn.message] - ), - " ", - elNew("span", { class: ["tweet-reply-add-emoji"] }, [ - LOC.emoji_btn.message, - ]), - " ", - elNew( + t.conversation_muted + ? LOC.unmute_convo.message + : LOC.mute_convo.message + ) + ); + dropDownMoreInteractionsArray.push(elNew("hr")); + if (t.feedback) { + dropDownMoreInteractionsArray.push( + ...t.feedback.map((f, i) => { + elNew( + "span", + { + class: ["tweet-interact-more-menu-feedback"], + dataset: { index: i }, + }, + [f.prompt ? f.prompt : LOC.topic_not_interested.message] + ); + }) + ); + } + dropDownMoreInteractionsArray.push( + elNew( "span", { - title: !vars.disableHotkeys ? "ALT+R" : "", - class: ["tweet-reply-cancel"], + class: ["tweet-interact-more-menu-refresh"], }, - [LOC.cancel_btn.message] - ), - ] - ), - elNew("span", { style: "color:red", class: ["tweet-reply-error"] }, []), - elNew("textarea", { - maxlength: 25000, - class: ["tweet-reply-text"], - placeholder: LOC.reply_example.message, - }), - elNew( - "button", - { title: "CTRL+ENTER", class: ["tweet-reply-button", "nice-button"] }, - [LOC.reply.message] - ), - elNew("br"), - elNew("span", { class: ["tweet-reply-char"] }, [ - localStorage.OTisBlueVerified ? "0/25000" : "0/280", - ]), - elNew("br"), - elNew("div", { - style: "padding-bottom: 10px;", - class: ["tweet-reply-media"], - }), - ] - ); - - const tweet_quote_node = elNew( - "div", - { class: ["tweet-edit-section", "tweet-quote"], hidden: true }, - [ - elNew("br"), - elNew( - "b", - { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, - [ - `${LOC.quote_tweet.message} `, - elNew( - "span", + LOC.refresh_tweet.message + ) + ); + if ( + t.extended_entities && + t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "animated_gif" + ) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-download-gif"], + dataset: { gifno: "1" }, + }, + LOC.download_gif.message + ) + ); + } + if (t.extended_entities && t.extended_entities.media.length > 1) { + dropDownMoreInteractionsArray.push( + ...t.extended_entities.media + .filter((m) => m.type === "animated_gif") + .map((m, i) => { + elNew( + "span", + { + class: ["tweet-interact-more-menu-download-gif"], + dataset: { gifno: i + 1 }, + }, + `${LOC.download_gif.message} (#${i + 1})` + ); + }) + ); + } + if (t.extended_entities && t.extended_entities.media.length > 0) { + dropDownMoreInteractionsArray.push( + elNew( + "span", + { + class: ["tweet-interact-more-menu-download"], + }, + `${LOC.download_media.message}` + ) + ); + } + if (vars.developerMode) { + dropDownMoreInteractionsArray.push( + ...[ + elNew( + "span", + { + class: ["tweet-interact-more-menu-copy-user-id"], + }, + LOC.copy_user_id.message + ), + elNew( + "span", + { + class: ["tweet-interact-more-menu-copy-tweet-id"], + }, + LOC.copy_tweet_id.message + ), + elNew( + "span", + { + class: ["tweet-interact-more-menu-log"], + }, + "Log tweet object" + ), + ] + ); + } + + interactionArray.push( + elNew( + "div", { - title: !vars.disableHotkeys ? "ALT+M" : "", - class: ["tweet-quote-upload"], + class: ["tweet-interact-more-menu", "dropdown-menu"], + hidden: true, }, - [LOC.upload_media_btn.message] - ), - " ", - elNew("span", { class: ["tweet-quote-add-emoji"] }, [ - LOC.emoji_btn.message, - ]), - " ", - elNew( - "span", + dropDownMoreInteractionsArray + ) + ); + if ( + options.selfThreadButton && + t.self_thread && + t.self_thread.id_str && + !options.threadContinuation && + !location.pathname.includes("/status/") + ) { + interactionArray.push( + elNew( + "a", + { + class: ["tweet-self-thread-button", "tweet-thread-right"], + target: "_blank", + href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, + }, + LOC.show_this_thread.message + ) + ); + } + if ( + !options.noTop && + !options.selfThreadButton && + t.in_reply_to_status_id_str && + !( + options.threadContinuation || + (options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str) + ) && + !location.pathname.includes("/status/") + ) { + elNew( + "a", { - title: !vars.disableHotkeys ? "ALT+Q" : "", - class: ["tweet-quote-cancel"], + class: ["tweet-self-thread-button", "tweet-thread-right"], + target: "_blank", + href: `/${t.in_reply_to_screen_name}/status/${t.in_reply_to_status_id_str}`, }, - [LOC.cancel_btn.message] - ), - ] - ), - elNew("span", { style: "color:red", class: ["tweet-quote-error"] }, []), - elNew("textarea", { - maxlength: 25000, - class: ["tweet-quote-text"], - placeholder: LOC.quote_example.message, - }), - elNew( - "button", - { title: "CTRL+ENTER", class: ["tweet-quote-button", "nice-button"] }, - [LOC.quote.message] - ), - elNew("br"), - elNew("span", { class: ["tweet-quote-char"] }, [ - localStorage.OTisBlueVerified ? "0/25000" : "0/280", - ]), - elNew("br"), - elNew("div", { - style: "padding-bottom: 10px;", - class: ["tweet-quote-media"], - }), - ] - ); - - const hideThreadContinuation = - options.threadContinuation || - (options.selfThreadContinuation && t.self_thread && t.self_thread.id_str) - ? false - : true; - const uriHasStatus = location.pathname.includes("/status/"); - const isStandaloneTweet = - options.selfThreadContinuation && - t.self_thread && - t.self_thread.id_str && - !uriHasStatus; - var replyChildren = []; - if (isStandaloneTweet) { - replyChildren.push(elNew("br")); - replyChildren.push( - elNew( - "a", - { - class: ["tweet-self-thread-button"], - target: "_blank", - href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, - }, - LOC.show_this_thread.message - ) + LOC.show_this_thread.message + ); + } + + // Tweet Interactions buttons + const interactions_node = elNew( + "div", + { class: ["tweet-interact"] }, + interactionArray ); - replyChildren.push( - elNew("span", { - class: ["tweet-self-thread-line"], - style: `margin-left: -108px;margin-top: -5px;`, - }) + + // tweet_interact = ; + + const tweet_reply_node = elNew( + "div", + { class: ["tweet-edit-section", "tweet-reply"], hidden: true }, + [ + elNew("br"), + elNew( + "b", + { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, + [ + `${LOC.replying_to_tweet.message} `, + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+M" : "", + class: ["tweet-reply-upload"], + }, + [LOC.upload_media_btn.message] + ), + " ", + elNew("span", { class: ["tweet-reply-add-emoji"] }, [ + LOC.emoji_btn.message, + ]), + " ", + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+R" : "", + class: ["tweet-reply-cancel"], + }, + [LOC.cancel_btn.message] + ), + ] + ), + elNew( + "span", + { style: "color:red", class: ["tweet-reply-error"] }, + [] + ), + elNew("textarea", { + maxlength: 25000, + class: ["tweet-reply-text"], + placeholder: LOC.reply_example.message, + }), + elNew( + "button", + { + title: "CTRL+ENTER", + class: ["tweet-reply-button", "nice-button"], + }, + [LOC.reply.message] + ), + elNew("br"), + elNew("span", { class: ["tweet-reply-char"] }, [ + localStorage.OTisBlueVerified ? "0/25000" : "0/280", + ]), + elNew("br"), + elNew("div", { + style: "padding-bottom: 10px;", + class: ["tweet-reply-media"], + }), + ] ); - replyChildren.push( - elNew("div", { - class: ["tweet-self-thread-line-dots"], - style: `margin-left: -120px;margin-top: -3px;`, - }) + + const tweet_quote_node = elNew( + "div", + { class: ["tweet-edit-section", "tweet-quote"], hidden: true }, + [ + elNew("br"), + elNew( + "b", + { style: "font-size: 12px;display: block;margin-bottom: 5px;" }, + [ + `${LOC.quote_tweet.message} `, + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+M" : "", + class: ["tweet-quote-upload"], + }, + [LOC.upload_media_btn.message] + ), + " ", + elNew("span", { class: ["tweet-quote-add-emoji"] }, [ + LOC.emoji_btn.message, + ]), + " ", + elNew( + "span", + { + title: !vars.disableHotkeys ? "ALT+Q" : "", + class: ["tweet-quote-cancel"], + }, + [LOC.cancel_btn.message] + ), + ] + ), + elNew( + "span", + { style: "color:red", class: ["tweet-quote-error"] }, + [] + ), + elNew("textarea", { + maxlength: 25000, + class: ["tweet-quote-text"], + placeholder: LOC.quote_example.message, + }), + elNew( + "button", + { + title: "CTRL+ENTER", + class: ["tweet-quote-button", "nice-button"], + }, + [LOC.quote.message] + ), + elNew("br"), + elNew("span", { class: ["tweet-quote-char"] }, [ + localStorage.OTisBlueVerified ? "0/25000" : "0/280", + ]), + elNew("br"), + elNew("div", { + style: "padding-bottom: 10px;", + class: ["tweet-quote-media"], + }), + ] ); - } else { - if (uriHasStatus) { - replyChildren.push(elNew("br")); - replyChildren.push(elNew("br")); + + const hideThreadContinuation = + options.threadContinuation || + (options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str) + ? false + : true; + const uriHasStatus = location.pathname.includes("/status/"); + const isStandaloneTweet = + options.selfThreadContinuation && + t.self_thread && + t.self_thread.id_str && + !uriHasStatus; + var replyChildren = []; + if (isStandaloneTweet) { + replyChildren.push(elNew("br")); + replyChildren.push( + elNew( + "a", + { + class: ["tweet-self-thread-button"], + target: "_blank", + href: `/${t.user.screen_name}/status/${t.self_thread.id_str}`, + }, + LOC.show_this_thread.message + ) + ); + replyChildren.push( + elNew("span", { + class: ["tweet-self-thread-line"], + style: `margin-left: -108px;margin-top: -5px;`, + }) + ); + replyChildren.push( + elNew("div", { + class: ["tweet-self-thread-line-dots"], + style: `margin-left: -120px;margin-top: -3px;`, + }) + ); + } else { + if (uriHasStatus) { + replyChildren.push(elNew("br")); + replyChildren.push(elNew("br")); + } + replyChildren.push( + elNew("span", { + class: ["tweet-self-thread-line"], + style: uriHasStatus ? "margin-top:-10px;" : "", + }) + ); + replyChildren.push( + elNew("div", { + style: uriHasStatus ? "margin-top:-8px;" : "", + class: ["tweet-self-thread-line-dots"], + }) + ); } - replyChildren.push( - elNew("span", { - class: ["tweet-self-thread-line"], - style: uriHasStatus ? "margin-top:-10px;" : "", - }) + const reply_nodes = elNew( + "div", + { + class: ["tweet-self-thread-div"], + hidden: hideThreadContinuation, + }, + replyChildren ); - replyChildren.push( - elNew("div", { - style: uriHasStatus ? "margin-top:-8px;" : "", - class: ["tweet-self-thread-line-dots"], - }) + + const translate_node = elNew("div", {}, [ + elNew("br"), + elNew("span", { class: ["tweet-button", "tweet-translate"] }, [ + LOC.view_translation.message, + ]), + ]); + + const country_restriction_node = elNew( + "div", + { class: ["tweet-warning"] }, + [ + "This Tweet has been withheld in response to a report from the copyright holder.", + elNew( + "a", + { + href: "https://help.twitter.com/en/rules-and-policies/copyright-policy", + target: "_blank", + }, + ["Learn more."] + ), + ] ); - } - const reply_nodes = elNew( - "div", - { - class: ["tweet-self-thread-div"], - hidden: hideThreadContinuation, - }, - replyChildren - ); - - const translate_node = elNew("div", {}, [ - elNew("br"), - elNew("span", { class: ["tweet-button", "tweet-translate"] }, [ - LOC.view_translation.message, - ]), - ]); - - const country_restriction_node = elNew("div", { class: ["tweet-warning"] }, [ - "This Tweet has been withheld in response to a report from the copyright holder.", - elNew( - "a", - { - href: "https://help.twitter.com/en/rules-and-policies/copyright-policy", - target: "_blank", - }, - ["Learn more."] - ), - ]); - - return [ - tweetTopConst.children, - [ - mentioned_node, - body_node, - !tweetConstructorArgs.isMatchingLanguage && options.mainTweet - ? translate_node - : null, - extended_media_nodes, - card_node, - quoted_node, - limited_node, - tombstone_node, - (t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY"))) || - t.withheld_scope - ? country_restriction_node - : null, - conversation_control_node, - tweet_footer_node, - tweet_date_node, - interactions_node, - // tweet_edit - tweet_reply_node, - tweet_quote_node, - reply_nodes, - ].flat(1), // flat required due to `extended_media_nodes` being an array. - ]; + + return [ + tweetTopConst.children, + [ + mentioned_node, + body_node, + !tweetConstructorArgs.isMatchingLanguage && options.mainTweet + ? translate_node + : null, + extended_media_nodes, + card_node, + quoted_node, + limited_node, + tombstone_node, + (t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY"))) || + t.withheld_scope + ? country_restriction_node + : null, + conversation_control_node, + tweet_footer_node, + tweet_date_node, + interactions_node, + // tweet_edit + tweet_reply_node, + tweet_quote_node, + reply_nodes, + ].flat(1), // flat required due to `extended_media_nodes` being an array. + ]; } diff --git a/scripts/tweetviewer.js b/scripts/tweetviewer.js index 9238706a..ddb9f25f 100644 --- a/scripts/tweetviewer.js +++ b/scripts/tweetviewer.js @@ -1,4317 +1,4795 @@ class TweetViewer { - constructor(user, tweetData) { - let previousLocation = location.pathname + location.search; + constructor(user, tweetData) { + let previousLocation = location.pathname + location.search; - this.container = createModal( - html` -
- -
- - - - - - - - - `, - "tweet-viewer", - () => { - this.close(); - if (location.pathname + location.search !== previousLocation) - history.pushState({}, null, previousLocation); - } - ); - this.tweetData = tweetData; - this.id = tweetData.id_str; - history.pushState( - {}, - null, - `/${tweetData.user.screen_name}/status/${this.id}` - ); + this.container = createModal( + html` +
+ +
+ + + + + + + + + `, + "tweet-viewer", + () => { + this.close(); + if (location.pathname + location.search !== previousLocation) + history.pushState({}, null, previousLocation); + } + ); + this.tweetData = tweetData; + this.id = tweetData.id_str; + history.pushState( + {}, + null, + `/${tweetData.user.screen_name}/status/${this.id}` + ); - this.user = user; - this.loadingNewTweets = false; - this.lastTweetDate = 0; - this.activeTweet; - this.pageData = {}; - this.tweets = []; - this.cursor = undefined; - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.users = {}; - this.linkColors = {}; - this.likeCursor = undefined; - this.retweetCursor = undefined; - this.retweetCommentsCursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - this.insertedMores = []; - this.currentLocation = location.pathname; - this.subpage = undefined; - this.popstateHelper = undefined; - this.scrollHelper = undefined; - this.timelineElement = this.container.getElementsByClassName("timeline")[0]; - this.moreBtn = this.container.getElementsByClassName("timeline-more")[0]; + this.user = user; + this.loadingNewTweets = false; + this.lastTweetDate = 0; + this.activeTweet; + this.pageData = {}; + this.tweets = []; + this.cursor = undefined; + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.users = {}; + this.linkColors = {}; + this.likeCursor = undefined; + this.retweetCursor = undefined; + this.retweetCommentsCursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + this.insertedMores = []; + this.currentLocation = location.pathname; + this.subpage = undefined; + this.popstateHelper = undefined; + this.scrollHelper = undefined; + this.timelineElement = + this.container.getElementsByClassName("timeline")[0]; + this.moreBtn = + this.container.getElementsByClassName("timeline-more")[0]; - let event = new CustomEvent("clearActiveTweet"); - document.dispatchEvent(event); + let event = new CustomEvent("clearActiveTweet"); + document.dispatchEvent(event); - chrome.storage.sync.get(["viewedtweets"], (result) => { - if (!result.viewedtweets) result.viewedtweets = []; - result.viewedtweets.unshift(this.id); - result.viewedtweets = [...new Set(result.viewedtweets)]; - while (result.viewedtweets.length >= 100) { - result.viewedtweets.pop(); - } - chrome.storage.sync.set({ viewedtweets: result.viewedtweets }); - }); + chrome.storage.sync.get(["viewedtweets"], (result) => { + if (!result.viewedtweets) result.viewedtweets = []; + result.viewedtweets.unshift(this.id); + result.viewedtweets = [...new Set(result.viewedtweets)]; + while (result.viewedtweets.length >= 100) { + result.viewedtweets.pop(); + } + chrome.storage.sync.set({ viewedtweets: result.viewedtweets }); + }); - this.init(); - } - async savePageData(path) { - if (!path) { - path = location.pathname.split("?")[0].split("#")[0]; - if (path.endsWith("/")) path = path.slice(0, -1); + this.init(); } - this.pageData[path] = { - linkColors: this.linkColors, - cursor: this.cursor, - likeCursor: this.likeCursor, - retweetCursor: this.retweetCursor, - retweetCommentsCursor: this.retweetCommentsCursor, - mainTweetLikers: this.mainTweetLikers, - seenReplies: this.seenReplies, - tweets: this.tweets, - scrollY: this.container.scrollTop, - }; - console.log(`Saving page: ${path}`, this.pageData[path]); - } - async restorePageData() { - let path = location.pathname.split("?")[0].split("#")[0]; - if (path.endsWith("/")) path = path.slice(0, -1); - if (this.pageData[path]) { - console.log(`Restoring page: ${path}`, this.pageData[path]); - this.linkColors = this.pageData[path].linkColors; - this.cursor = this.pageData[path].cursor; - this.likeCursor = this.pageData[path].likeCursor; - this.retweetCursor = this.pageData[path].retweetCursor; - this.retweetCommentsCursor = this.pageData[path].retweetCommentsCursor; - this.mainTweetLikers = this.pageData[path].mainTweetLikers; - this.seenReplies = []; - this.tweets = []; - this.container.getElementsByClassName("timeline-more")[0].hidden = - !this.cursor; - let tl = document.getElementsByClassName("timeline")[0]; - tl.innerHTML = ""; - for (let i in this.pageData[path].tweets) { - let t = this.pageData[path].tweets[i]; - if (t[0] === "tweet") { - await this.appendTweet(t[1], tl, t[2]); - } else if (t[0] === "compose") { - await this.appendComposeComponent(tl, t[1]); - } else if (t[0] === "tombstone") { - await this.appendTombstone(tl, t[1]); - } else if (t[0] === "showmore") { - await this.appendShowMore(tl, t[1], t[2]); + async savePageData(path) { + if (!path) { + path = location.pathname.split("?")[0].split("#")[0]; + if (path.endsWith("/")) path = path.slice(0, -1); } - } - let id = this.currentLocation.match(/status\/(\d{1,32})/)[1]; - if (id) { - setTimeout(() => { - let tweet = this.container.querySelector( - `div.tweet[data-tweet-id="${id}"]` - ); - if (tweet) { - tweet.scrollIntoView({ block: "center" }); - } - }, 100); - } - if ( - this.subpage === "retweets_with_comments" && - this.retweetCommentsCursor - ) { - this.container.getElementsByClassName( - "retweets_with_comments-more" - )[0].hidden = false; - } - this.loadingNewTweets = false; - return this.pageData[path]; - } else { - this.tweets = []; - this.seenReplies = []; + this.pageData[path] = { + linkColors: this.linkColors, + cursor: this.cursor, + likeCursor: this.likeCursor, + retweetCursor: this.retweetCursor, + retweetCommentsCursor: this.retweetCommentsCursor, + mainTweetLikers: this.mainTweetLikers, + seenReplies: this.seenReplies, + tweets: this.tweets, + scrollY: this.container.scrollTop, + }; + console.log(`Saving page: ${path}`, this.pageData[path]); + } + async restorePageData() { + let path = location.pathname.split("?")[0].split("#")[0]; + if (path.endsWith("/")) path = path.slice(0, -1); + if (this.pageData[path]) { + console.log(`Restoring page: ${path}`, this.pageData[path]); + this.linkColors = this.pageData[path].linkColors; + this.cursor = this.pageData[path].cursor; + this.likeCursor = this.pageData[path].likeCursor; + this.retweetCursor = this.pageData[path].retweetCursor; + this.retweetCommentsCursor = + this.pageData[path].retweetCommentsCursor; + this.mainTweetLikers = this.pageData[path].mainTweetLikers; + this.seenReplies = []; + this.tweets = []; + this.container.getElementsByClassName("timeline-more")[0].hidden = + !this.cursor; + let tl = document.getElementsByClassName("timeline")[0]; + tl.innerHTML = ""; + for (let i in this.pageData[path].tweets) { + let t = this.pageData[path].tweets[i]; + if (t[0] === "tweet") { + await this.appendTweet(t[1], tl, t[2]); + } else if (t[0] === "compose") { + await this.appendComposeComponent(tl, t[1]); + } else if (t[0] === "tombstone") { + await this.appendTombstone(tl, t[1]); + } else if (t[0] === "showmore") { + await this.appendShowMore(tl, t[1], t[2]); + } + } + let id = this.currentLocation.match(/status\/(\d{1,32})/)[1]; + if (id) { + setTimeout(() => { + let tweet = this.container.querySelector( + `div.tweet[data-tweet-id="${id}"]` + ); + if (tweet) { + tweet.scrollIntoView({ block: "center" }); + } + }, 100); + } + if ( + this.subpage === "retweets_with_comments" && + this.retweetCommentsCursor + ) { + this.container.getElementsByClassName( + "retweets_with_comments-more" + )[0].hidden = false; + } + this.loadingNewTweets = false; + return this.pageData[path]; + } else { + this.tweets = []; + this.seenReplies = []; + } + this.loadingNewTweets = false; + return false; } - this.loadingNewTweets = false; - return false; - } - updateSubpage() { - let path = location.pathname.slice(1); - if (path.endsWith("/")) path = path.slice(0, -1); + updateSubpage() { + let path = location.pathname.slice(1); + if (path.endsWith("/")) path = path.slice(0, -1); - let tlDiv = document.getElementsByClassName("timeline")[0]; - let rtDiv = document.getElementsByClassName("retweets")[0]; - let rtwDiv = document.getElementsByClassName("retweets_with_comments")[0]; - let likesDiv = document.getElementsByClassName("likes")[0]; - let tlMore = document.getElementsByClassName("timeline-more")[0]; - let rtMore = document.getElementsByClassName("retweets-more")[0]; - let rtwMore = document.getElementsByClassName( - "retweets_with_comments-more" - )[0]; - let likesMore = document.getElementsByClassName("likes-more")[0]; - tlDiv.hidden = true; - rtDiv.hidden = true; - rtwDiv.hidden = true; - likesDiv.hidden = true; - tlMore.hidden = true; - rtMore.hidden = true; - rtwMore.hidden = true; - likesMore.hidden = true; + let tlDiv = document.getElementsByClassName("timeline")[0]; + let rtDiv = document.getElementsByClassName("retweets")[0]; + let rtwDiv = document.getElementsByClassName( + "retweets_with_comments" + )[0]; + let likesDiv = document.getElementsByClassName("likes")[0]; + let tlMore = document.getElementsByClassName("timeline-more")[0]; + let rtMore = document.getElementsByClassName("retweets-more")[0]; + let rtwMore = document.getElementsByClassName( + "retweets_with_comments-more" + )[0]; + let likesMore = document.getElementsByClassName("likes-more")[0]; + tlDiv.hidden = true; + rtDiv.hidden = true; + rtwDiv.hidden = true; + likesDiv.hidden = true; + tlMore.hidden = true; + rtMore.hidden = true; + rtwMore.hidden = true; + likesMore.hidden = true; - if (path.split("/").length === 3) { - this.subpage = "tweet"; - tlDiv.hidden = false; - } else { - if (path.endsWith("/retweets")) { - this.subpage = "retweets"; - rtDiv.hidden = false; - } else if (path.endsWith("/likes")) { - this.subpage = "likes"; - likesDiv.hidden = false; - } else if (path.endsWith("/retweets/with_comments")) { - this.subpage = "retweets_with_comments"; - rtwDiv.hidden = false; - } - } - } - async updateReplies(id, c) { - let tvl = this.container.getElementsByClassName("tweet-viewer-loading")[0]; - if (!c) { - tvl.hidden = false; - document.getElementsByClassName("timeline")[0].innerHTML = ""; + if (path.split("/").length === 3) { + this.subpage = "tweet"; + tlDiv.hidden = false; + } else { + if (path.endsWith("/retweets")) { + this.subpage = "retweets"; + rtDiv.hidden = false; + } else if (path.endsWith("/likes")) { + this.subpage = "likes"; + likesDiv.hidden = false; + } else if (path.endsWith("/retweets/with_comments")) { + this.subpage = "retweets_with_comments"; + rtwDiv.hidden = false; + } + } } - let tl, tweetLikers; - try { - let [tlData, tweetLikersData] = await Promise.allSettled([ - API.tweet.getRepliesV2(id, c), - API.tweet.getLikers(id, undefined, 40), - ]); - if (!tlData.value) { + async updateReplies(id, c) { + let tvl = this.container.getElementsByClassName( + "tweet-viewer-loading" + )[0]; if (!c) { - tvl.hidden = true; - document.getElementsByClassName("timeline")[0].innerHTML = html`${tlData.reason}`; + tvl.hidden = false; + document.getElementsByClassName("timeline")[0].innerHTML = ""; } - this.cursor = undefined; - return console.error(tlData.reason); - } - if (!tweetLikersData.value) { - console.error(tweetLikersData.reason); - } - tl = tlData.value; - for (let u in tl.users) { - this.users[u] = tl.users[u]; - } - if (tweetLikersData.value) tweetLikers = tweetLikersData.value; - else tweetLikers = { list: [], cursor: undefined }; - this.loadingNewTweets = false; - document.getElementsByClassName("timeline-more")[0].innerText = - LOC.load_more.message; - } catch (e) { - document.getElementsByClassName("timeline-more")[0].innerText = - LOC.load_more.message; - this.loadingNewTweets = false; - tvl.hidden = true; - return (this.cursor = undefined); - } - - if (vars.slowLinkColorsInTL) { - let tlUsers = []; - for (let i in tl.list) { - let t = tl.list[i]; - if (t.type === "tweet" || t.type === "mainTweet") { - if (!tlUsers.includes(t.data.user.id_str)) - tlUsers.push(t.data.user.id_str); - } else if (t.type === "conversation") { - for (let j in t.data) { - tlUsers.push(t.data[j].user.id_str); - } + let tl, tweetLikers; + try { + let [tlData, tweetLikersData] = await Promise.allSettled([ + API.tweet.getRepliesV2(id, c), + API.tweet.getLikers(id, undefined, 40), + ]); + if (!tlData.value) { + if (!c) { + tvl.hidden = true; + document.getElementsByClassName( + "timeline" + )[0].innerHTML = html`${tlData.reason}`; + } + this.cursor = undefined; + return console.error(tlData.reason); + } + if (!tweetLikersData.value) { + console.error(tweetLikersData.reason); + } + tl = tlData.value; + for (let u in tl.users) { + this.users[u] = tl.users[u]; + } + if (tweetLikersData.value) tweetLikers = tweetLikersData.value; + else tweetLikers = { list: [], cursor: undefined }; + this.loadingNewTweets = false; + document.getElementsByClassName("timeline-more")[0].innerText = + LOC.load_more.message; + } catch (e) { + document.getElementsByClassName("timeline-more")[0].innerText = + LOC.load_more.message; + this.loadingNewTweets = false; + tvl.hidden = true; + return (this.cursor = undefined); } - } - tlUsers = tlUsers.filter((i) => !this.linkColors[i]); - let linkData = await getLinkColors(tlUsers); - if (linkData) - for (let i in linkData) { - this.linkColors[linkData[i].id] = linkData[i].color; + + if (vars.slowLinkColorsInTL) { + let tlUsers = []; + for (let i in tl.list) { + let t = tl.list[i]; + if (t.type === "tweet" || t.type === "mainTweet") { + if (!tlUsers.includes(t.data.user.id_str)) + tlUsers.push(t.data.user.id_str); + } else if (t.type === "conversation") { + for (let j in t.data) { + tlUsers.push(t.data[j].user.id_str); + } + } + } + tlUsers = tlUsers.filter((i) => !this.linkColors[i]); + let linkData = await getLinkColors(tlUsers); + if (linkData) + for (let i in linkData) { + this.linkColors[linkData[i].id] = linkData[i].color; + } } - } - this.cursor = tl.cursor; - if (!this.cursor) { - this.container.getElementsByClassName("timeline-more")[0].hidden = true; - } else { - this.container.getElementsByClassName("timeline-more")[0].hidden = false; - } - let mainTweet; - let mainTweetIndex = tl.list.findIndex((t) => t.type === "mainTweet"); - let tlContainer = this.container.getElementsByClassName("timeline")[0]; - if (!tlContainer) return; - for (let i in tl.list) { - let t = tl.list[i]; - if (t.type === "mainTweet") { - this.mainTweetLikers = tweetLikers.list; - this.likeCursor = tweetLikers.cursor; - if (i === 0) { - mainTweet = await this.appendTweet(t.data, tlContainer, { - mainTweet: true, - }); + this.cursor = tl.cursor; + if (!this.cursor) { + this.container.getElementsByClassName( + "timeline-more" + )[0].hidden = true; } else { - mainTweet = await this.appendTweet(t.data, tlContainer, { - noTop: true, - mainTweet: true, - }); + this.container.getElementsByClassName( + "timeline-more" + )[0].hidden = false; } - if (t.data.limited_actions !== "non_compliant") - this.appendComposeComponent(tlContainer, t.data); - } - if (t.type === "tweet") { - await this.appendTweet(t.data, tlContainer, { - noTop: i !== 0 && i < mainTweetIndex, - threadContinuation: i < mainTweetIndex, - }); - } else if (t.type === "conversation") { - for (let i2 in t.data) { - let t2 = t.data[i2]; - await this.appendTweet(t2, tlContainer, { - noTop: +i2 !== 0, - threadContinuation: +i2 !== t.data.length - 1, - threadButton: +i2 === t.data.length - 1, - threadId: t2.conversation_id_str, - }); + let mainTweet; + let mainTweetIndex = tl.list.findIndex((t) => t.type === "mainTweet"); + let tlContainer = this.container.getElementsByClassName("timeline")[0]; + if (!tlContainer) return; + for (let i in tl.list) { + let t = tl.list[i]; + if (t.type === "mainTweet") { + this.mainTweetLikers = tweetLikers.list; + this.likeCursor = tweetLikers.cursor; + if (i === 0) { + mainTweet = await this.appendTweet(t.data, tlContainer, { + mainTweet: true, + }); + } else { + mainTweet = await this.appendTweet(t.data, tlContainer, { + noTop: true, + mainTweet: true, + }); + } + if (t.data.limited_actions !== "non_compliant") + this.appendComposeComponent(tlContainer, t.data); + } + if (t.type === "tweet") { + await this.appendTweet(t.data, tlContainer, { + noTop: i !== 0 && i < mainTweetIndex, + threadContinuation: i < mainTweetIndex, + }); + } else if (t.type === "conversation") { + for (let i2 in t.data) { + let t2 = t.data[i2]; + await this.appendTweet(t2, tlContainer, { + noTop: +i2 !== 0, + threadContinuation: +i2 !== t.data.length - 1, + threadButton: +i2 === t.data.length - 1, + threadId: t2.conversation_id_str, + }); + } + } else if (t.type === "tombstone") { + this.appendTombstone(tlContainer, t.data); + } else if (t.type === "showMore") { + this.appendShowMore(tlContainer, t.data, id); + } } - } else if (t.type === "tombstone") { - this.appendTombstone(tlContainer, t.data); - } else if (t.type === "showMore") { - this.appendShowMore(tlContainer, t.data, id); - } - } - if (mainTweet) mainTweet.scrollIntoView(); - if (tvl) tvl.hidden = true; - return true; - } - async updateLikes(id, c) { - let tvl = this.container.getElementsByClassName("tweet-viewer-loading")[0]; - if (tvl) tvl.hidden = false; - let tweetLikers; - if (!c && this.mainTweetLikers.length > 0) { - tweetLikers = this.mainTweetLikers; - } else { - try { - tweetLikers = await API.tweet.getLikers(id, c, 40); - this.likeCursor = tweetLikers.cursor; - tweetLikers = tweetLikers.list; - if (!c) this.mainTweetLikers = tweetLikers; - } catch (e) { - console.error(e); + if (mainTweet) mainTweet.scrollIntoView(); if (tvl) tvl.hidden = true; - return (this.likeCursor = undefined); - } - } - let likeDiv = document.getElementsByClassName("likes")[0]; - - if (!c) { - likeDiv.innerHTML = ""; - let tweetData = await API.tweet.getV2(id); - let tweet = await this.appendTweet(tweetData, likeDiv, { - mainTweet: true, - }); - tweet.style.borderBottom = "1px solid var(--border)"; - tweet.style.marginBottom = "10px"; - tweet.style.borderRadius = "5px"; - let h1 = document.createElement("h1"); - h1.innerText = vars.heartsNotStars - ? LOC.liked_by.message - : LOC.favorited_by.message; - h1.className = "cool-header"; - likeDiv.appendChild(h1); - } - if (!this.likeCursor || tweetLikers.length === 0) { - this.container.getElementsByClassName("likes-more")[0].hidden = true; - } else { - this.container.getElementsByClassName("likes-more")[0].hidden = false; + return true; } + async updateLikes(id, c) { + let tvl = this.container.getElementsByClassName( + "tweet-viewer-loading" + )[0]; + if (tvl) tvl.hidden = false; + let tweetLikers; + if (!c && this.mainTweetLikers.length > 0) { + tweetLikers = this.mainTweetLikers; + } else { + try { + tweetLikers = await API.tweet.getLikers(id, c, 40); + this.likeCursor = tweetLikers.cursor; + tweetLikers = tweetLikers.list; + if (!c) this.mainTweetLikers = tweetLikers; + } catch (e) { + console.error(e); + if (tvl) tvl.hidden = true; + return (this.likeCursor = undefined); + } + } + let likeDiv = document.getElementsByClassName("likes")[0]; - for (let i in tweetLikers) { - appendUser(tweetLikers[i], likeDiv); - } + if (!c) { + likeDiv.innerHTML = ""; + let tweetData = await API.tweet.getV2(id); + let tweet = await this.appendTweet(tweetData, likeDiv, { + mainTweet: true, + }); + tweet.style.borderBottom = "1px solid var(--border)"; + tweet.style.marginBottom = "10px"; + tweet.style.borderRadius = "5px"; + let h1 = document.createElement("h1"); + h1.innerText = vars.heartsNotStars + ? LOC.liked_by.message + : LOC.favorited_by.message; + h1.className = "cool-header"; + likeDiv.appendChild(h1); + } + if (!this.likeCursor || tweetLikers.length === 0) { + this.container.getElementsByClassName( + "likes-more" + )[0].hidden = true; + } else { + this.container.getElementsByClassName( + "likes-more" + )[0].hidden = false; + } - if (tvl) tvl.hidden = true; - } - async updateRetweets(id, c) { - let tvl = this.container.getElementsByClassName("tweet-viewer-loading")[0]; - tvl.hidden = false; - let tweetRetweeters; - try { - tweetRetweeters = await API.tweet.getRetweeters(id, c); - this.retweetCursor = tweetRetweeters.cursor; - tweetRetweeters = tweetRetweeters.list; - } catch (e) { - console.error(e); - return (this.retweetCursor = undefined); - } - let retweetDiv = document.getElementsByClassName("retweets")[0]; + for (let i in tweetLikers) { + appendUser(tweetLikers[i], likeDiv); + } - if (!c) { - retweetDiv.innerHTML = ""; - let tweetData = await API.tweet.getV2(id); - let tweet = await this.appendTweet(tweetData, retweetDiv, { - mainTweet: true, - }); - tweet.style.borderBottom = "1px solid var(--border)"; - tweet.style.marginBottom = "10px"; - tweet.style.borderRadius = "5px"; - let h1 = document.createElement("h1"); - h1.innerHTML = html`${LOC.retweeted_by.message} (${LOC.see_quotes.message})`; - h1.className = "cool-header"; - retweetDiv.appendChild(h1); - h1.getElementsByTagName("a")[0].addEventListener("click", async (e) => { - e.preventDefault(); - history.pushState( - {}, - null, - `/${tweetData.user.screen_name}/status/${id}/retweets/with_comments` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweetsWithComments(tid); - this.currentLocation = location.pathname; - }); - } - if (!this.retweetCursor) { - this.container.getElementsByClassName("retweets-more")[0].hidden = true; - } else { - this.container.getElementsByClassName("retweets-more")[0].hidden = false; + if (tvl) tvl.hidden = true; } + async updateRetweets(id, c) { + let tvl = this.container.getElementsByClassName( + "tweet-viewer-loading" + )[0]; + tvl.hidden = false; + let tweetRetweeters; + try { + tweetRetweeters = await API.tweet.getRetweeters(id, c); + this.retweetCursor = tweetRetweeters.cursor; + tweetRetweeters = tweetRetweeters.list; + } catch (e) { + console.error(e); + return (this.retweetCursor = undefined); + } + let retweetDiv = document.getElementsByClassName("retweets")[0]; - for (let i in tweetRetweeters) { - let u = tweetRetweeters[i]; - let retweetElement = document.createElement("div"); - retweetElement.classList.add("following-item"); - retweetElement.innerHTML = html` -
- -
`; - - let followButton = retweetElement.querySelector(".following-item-btn"); - followButton.addEventListener("click", async () => { - if (followButton.classList.contains("following")) { - await API.user.unfollow(u.screen_name); - followButton.classList.remove("following"); - followButton.classList.add("follow"); - followButton.innerText = LOC.follow.message; + if (!c) { + retweetDiv.innerHTML = ""; + let tweetData = await API.tweet.getV2(id); + let tweet = await this.appendTweet(tweetData, retweetDiv, { + mainTweet: true, + }); + tweet.style.borderBottom = "1px solid var(--border)"; + tweet.style.marginBottom = "10px"; + tweet.style.borderRadius = "5px"; + let h1 = document.createElement("h1"); + h1.innerHTML = html`${LOC.retweeted_by.message} (${LOC.see_quotes.message})`; + h1.className = "cool-header"; + retweetDiv.appendChild(h1); + h1.getElementsByTagName("a")[0].addEventListener( + "click", + async (e) => { + e.preventDefault(); + history.pushState( + {}, + null, + `/${tweetData.user.screen_name}/status/${id}/retweets/with_comments` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweetsWithComments(tid); + this.currentLocation = location.pathname; + } + ); + } + if (!this.retweetCursor) { + this.container.getElementsByClassName( + "retweets-more" + )[0].hidden = true; } else { - await API.user.follow(u.screen_name); - followButton.classList.remove("follow"); - followButton.classList.add("following"); - followButton.innerText = LOC.following_btn.message; + this.container.getElementsByClassName( + "retweets-more" + )[0].hidden = false; } - }); - retweetDiv.appendChild(retweetElement); - } + for (let i in tweetRetweeters) { + let u = tweetRetweeters[i]; + let retweetElement = document.createElement("div"); + retweetElement.classList.add("following-item"); + retweetElement.innerHTML = html` +
+ +
`; - tvl.hidden = true; - } - async updateRetweetsWithComments(id, c) { - let tvl = this.container.getElementsByClassName("tweet-viewer-loading")[0]; - tvl.hidden = false; - let tweetRetweeters; - let tweetData = await API.tweet.getV2(id); - try { - tweetRetweeters = await API.tweet.getQuotes(id, c); - this.retweetCommentsCursor = tweetRetweeters.cursor; - tweetRetweeters = tweetRetweeters.list; - } catch (e) { - console.error(e); - tvl.hidden = true; - this.container.getElementsByClassName( - "retweets_with_comments-more" - )[0].innerText = LOC.load_more.message; - return (this.retweetCommentsCursor = undefined); - } - let retweetDiv = document.getElementsByClassName( - "retweets_with_comments" - )[0]; + let followButton = retweetElement.querySelector( + ".following-item-btn" + ); + followButton.addEventListener("click", async () => { + if (followButton.classList.contains("following")) { + await API.user.unfollow(u.screen_name); + followButton.classList.remove("following"); + followButton.classList.add("follow"); + followButton.innerText = LOC.follow.message; + } else { + await API.user.follow(u.screen_name); + followButton.classList.remove("follow"); + followButton.classList.add("following"); + followButton.innerText = LOC.following_btn.message; + } + }); - if (!c) { - retweetDiv.innerHTML = ""; - let h1 = document.createElement("h1"); - h1.innerHTML = html`${LOC.quote_tweets.message} (${LOC.see_retweets.message})`; - h1.className = "cool-header"; - retweetDiv.appendChild(h1); - h1.getElementsByTagName("a")[0].addEventListener("click", async (e) => { - e.preventDefault(); - let t = await API.tweet.getV2(id); - history.pushState( - {}, - null, - `/${tweetData.user.screen_name}/status/${id}/retweets` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweets(tid); - this.currentLocation = location.pathname; - }); - } - if (!this.retweetCommentsCursor || tweetRetweeters.length === 0) { - this.container.getElementsByClassName( - "retweets_with_comments-more" - )[0].hidden = true; - } else { - this.container.getElementsByClassName( - "retweets_with_comments-more" - )[0].hidden = false; - } - this.container.getElementsByClassName( - "retweets_with_comments-more" - )[0].innerText = LOC.load_more.message; + retweetDiv.appendChild(retweetElement); + } - for (let i in tweetRetweeters) { - await this.appendTweet(tweetRetweeters[i], retweetDiv); + tvl.hidden = true; } + async updateRetweetsWithComments(id, c) { + let tvl = this.container.getElementsByClassName( + "tweet-viewer-loading" + )[0]; + tvl.hidden = false; + let tweetRetweeters; + let tweetData = await API.tweet.getV2(id); + try { + tweetRetweeters = await API.tweet.getQuotes(id, c); + this.retweetCommentsCursor = tweetRetweeters.cursor; + tweetRetweeters = tweetRetweeters.list; + } catch (e) { + console.error(e); + tvl.hidden = true; + this.container.getElementsByClassName( + "retweets_with_comments-more" + )[0].innerText = LOC.load_more.message; + return (this.retweetCommentsCursor = undefined); + } + let retweetDiv = document.getElementsByClassName( + "retweets_with_comments" + )[0]; + + if (!c) { + retweetDiv.innerHTML = ""; + let h1 = document.createElement("h1"); + h1.innerHTML = html`${LOC.quote_tweets.message} (${LOC.see_retweets.message})`; + h1.className = "cool-header"; + retweetDiv.appendChild(h1); + h1.getElementsByTagName("a")[0].addEventListener( + "click", + async (e) => { + e.preventDefault(); + let t = await API.tweet.getV2(id); + history.pushState( + {}, + null, + `/${tweetData.user.screen_name}/status/${id}/retweets` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let tid = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweets(tid); + this.currentLocation = location.pathname; + } + ); + } + if (!this.retweetCommentsCursor || tweetRetweeters.length === 0) { + this.container.getElementsByClassName( + "retweets_with_comments-more" + )[0].hidden = true; + } else { + this.container.getElementsByClassName( + "retweets_with_comments-more" + )[0].hidden = false; + } + this.container.getElementsByClassName( + "retweets_with_comments-more" + )[0].innerText = LOC.load_more.message; - tvl.hidden = true; - } - async appendComposeComponent(container, replyTweet) { - if (!replyTweet) return; - this.tweets.push(["compose", replyTweet]); + for (let i in tweetRetweeters) { + await this.appendTweet(tweetRetweeters[i], retweetDiv); + } - let mentions = replyTweet.full_text.match(/@([\w+]{1,15})/g); - if (mentions) { - mentions = mentions.map((m) => m.slice(1).trim()); - } else { - mentions = []; + tvl.hidden = true; } + async appendComposeComponent(container, replyTweet) { + if (!replyTweet) return; + this.tweets.push(["compose", replyTweet]); - let replyMessage; - if (LOC.reply_to.message.includes("$SCREEN_NAME$")) { - replyMessage = LOC.reply_to.message.replace( - "$SCREEN_NAME$", - replyTweet.user.screen_name - ); - } else { - replyMessage = `${LOC.reply_to.message} @${replyTweet.user.screen_name}`; - } + let mentions = replyTweet.full_text.match(/@([\w+]{1,15})/g); + if (mentions) { + mentions = mentions.map((m) => m.slice(1).trim()); + } else { + mentions = []; + } - let el = document.createElement("div"); - el.className = "new-tweet-container"; - el.innerHTML = html`
- - - - -
- -
- -
`; - container.append(el); - document.getElementsByClassName("new-tweet-avatar")[0].src = `${ - this.user.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(this.user.id_str) % 7 - }_normal.png` - ) - : this.user.profile_image_url_https - }`.replace("_normal", "_bigger"); - document - .getElementsByClassName("new-tweet-view")[0] - .addEventListener("click", async () => { - document.getElementsByClassName("new-tweet-focused")[0].hidden = false; - document.getElementsByClassName("new-tweet-char")[0].hidden = false; - document - .getElementsByClassName("new-tweet-text")[0] - .classList.add("new-tweet-text-focused"); - document - .getElementsByClassName("new-tweet-media-div")[0] - .classList.add("new-tweet-media-div-focused"); - }); - if (mentions.length > 0) { - for (let i = 0; i < mentions.length; i++) { - let u = Object.values(this.users).find( - (u) => u.screen_name === mentions[i] - ); - if (!u) { - if (mentions[i] === this.user.screen_name) { - u = this.user; - } else if ( - typeof pageUser !== "undefined" && - mentions[i] === pageUser.screen_name - ) { - u = pageUser; - } else { - try { - u = await API.user.get(mentions[i], false); - } catch (e) { - console.error(e); - continue; - } - } + let replyMessage; + if (LOC.reply_to.message.includes("$SCREEN_NAME$")) { + replyMessage = LOC.reply_to.message.replace( + "$SCREEN_NAME$", + replyTweet.user.screen_name + ); + } else { + replyMessage = `${LOC.reply_to.message} @${replyTweet.user.screen_name}`; } - if (!u) continue; - this.users[u.id_str] = u; - } - document.getElementsByClassName("new-tweet-button")[0].style = - "margin-right: -50px;"; - document - .getElementsByClassName("new-tweet-mentions")[0] - .addEventListener("click", async () => { - let modal = createModal(html` -
+ + -

${LOC.replying_to.message}

-
- - -
- ${mentions - .map((m) => { - let u = Object.values(this.users).find( - (u) => u.screen_name === m - ); - if (!u) return ""; - return html` -
- - -
- `; - }) - .join("\n")} -
-
+ + +
+ +
+ +

- `); - document - .getElementById("new-tweet-mentions-modal-button") - .addEventListener("click", () => { - let excluded = []; - document - .querySelectorAll( - '#new-tweet-mentions-modal input[type="checkbox"]' - ) - .forEach((c) => { - if (!c.checked) excluded.push(c.dataset.userId); - }); - this.excludeUserMentions = excluded; - console.log(this.excludeUserMentions); - modal.removeModal(); +
`; + container.append(el); + document.getElementsByClassName("new-tweet-avatar")[0].src = `${ + this.user.default_profile_image && vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(this.user.id_str) % 7 + }_normal.png` + ) + : this.user.profile_image_url_https + }`.replace("_normal", "_bigger"); + document + .getElementsByClassName("new-tweet-view")[0] + .addEventListener("click", async () => { + document.getElementsByClassName( + "new-tweet-focused" + )[0].hidden = false; + document.getElementsByClassName( + "new-tweet-char" + )[0].hidden = false; + document + .getElementsByClassName("new-tweet-text")[0] + .classList.add("new-tweet-text-focused"); + document + .getElementsByClassName("new-tweet-media-div")[0] + .classList.add("new-tweet-media-div-focused"); }); - }); - } + if (mentions.length > 0) { + for (let i = 0; i < mentions.length; i++) { + let u = Object.values(this.users).find( + (u) => u.screen_name === mentions[i] + ); + if (!u) { + if (mentions[i] === this.user.screen_name) { + u = this.user; + } else if ( + typeof pageUser !== "undefined" && + mentions[i] === pageUser.screen_name + ) { + u = pageUser; + } else { + try { + u = await API.user.get(mentions[i], false); + } catch (e) { + console.error(e); + continue; + } + } + } + if (!u) continue; + this.users[u.id_str] = u; + } + document.getElementsByClassName("new-tweet-button")[0].style = + "margin-right: -50px;"; + document + .getElementsByClassName("new-tweet-mentions")[0] + .addEventListener("click", async () => { + let modal = createModal(html` +
+

+ ${LOC.replying_to.message} +

+
+ + +
+ ${mentions + .map((m) => { + let u = Object.values(this.users).find( + (u) => u.screen_name === m + ); + if (!u) return ""; + return html` +
+ + +
+ `; + }) + .join("\n")} +
+
+ +
+
+ `); + document + .getElementById("new-tweet-mentions-modal-button") + .addEventListener("click", () => { + let excluded = []; + document + .querySelectorAll( + '#new-tweet-mentions-modal input[type="checkbox"]' + ) + .forEach((c) => { + if (!c.checked) + excluded.push(c.dataset.userId); + }); + this.excludeUserMentions = excluded; + console.log(this.excludeUserMentions); + modal.removeModal(); + }); + }); + } - let mediaList = document.getElementsByClassName("new-tweet-media-c")[0]; + let mediaList = document.getElementsByClassName("new-tweet-media-c")[0]; - let mediaObserver = new MutationObserver(async () => { - if (mediaList.children.length > 0) { - newTweetButton.style.marginRight = "4px"; - } else { - newTweetButton.style.marginRight = - mentions.length > 0 ? "-50px" : "-32px"; - } - }); - mediaObserver.observe(mediaList, { childList: true }); + let mediaObserver = new MutationObserver(async () => { + if (mediaList.children.length > 0) { + newTweetButton.style.marginRight = "4px"; + } else { + newTweetButton.style.marginRight = + mentions.length > 0 ? "-50px" : "-32px"; + } + }); + mediaObserver.observe(mediaList, { childList: true }); - document - .getElementsByClassName("new-tweet-view")[0] - .addEventListener("drop", (e) => { - handleDrop(e, this.mediaToUpload, mediaList); - }); - document - .getElementsByClassName("new-tweet-media-div")[0] - .addEventListener("click", async () => { - getMedia(this.mediaToUpload, mediaList); - }); - let newTweetUserSearch = document.getElementsByClassName( - "new-tweet-user-search" - )[0]; - let newTweetText = document.getElementsByClassName("new-tweet-text")[0]; - let newTweetButton = document.getElementsByClassName("new-tweet-button")[0]; - let selectedIndex = 0; - newTweetText.addEventListener("paste", (event) => { - let items = (event.clipboardData || event.originalEvent.clipboardData) - .items; - for (let index in items) { - let item = items[index]; - if (item.kind === "file") { - let file = item.getAsFile(); - handleFiles( - [file], - this.mediaToUpload, - document.getElementsByClassName("new-tweet-media-c")[0] - ); - } - } - }); - newTweetText.addEventListener("focus", async (e) => { - setTimeout(() => { - if (/(? { - setTimeout(() => { - newTweetUserSearch.hidden = true; - }, 100); - }); - newTweetText.addEventListener("keypress", async (e) => { - if ( - (e.key === "Enter" || e.key === "Tab") && - !newTweetUserSearch.hidden - ) { - let activeSearch = newTweetUserSearch.querySelector( - ".search-result-item-active" - ); - if (!e.ctrlKey) { - e.preventDefault(); - e.stopPropagation(); - newTweetText.value = - newTweetText.value - .split("@") - .slice(0, -1) - .join("@") - .split(" ") - .slice(0, -1) - .join(" ") + - ` @${activeSearch - .querySelector(".search-result-item-screen-name") - .innerText.slice(1)} `; - if (newTweetText.value.startsWith(" ")) - newTweetText.value = newTweetText.value.slice(1); - newTweetUserSearch.innerHTML = ""; - newTweetUserSearch.hidden = true; - } - } - }); - newTweetText.addEventListener("keydown", async (e) => { - if (e.key === "ArrowDown") { - if (selectedIndex < newTweetUserSearch.children.length - 1) { - selectedIndex++; - newTweetUserSearch.children[selectedIndex].classList.add( - "search-result-item-active" - ); - newTweetUserSearch.children[selectedIndex - 1].classList.remove( - "search-result-item-active" - ); - } else { - selectedIndex = 0; - newTweetUserSearch.children[selectedIndex].classList.add( - "search-result-item-active" - ); - newTweetUserSearch.children[ - newTweetUserSearch.children.length - 1 - ].classList.remove("search-result-item-active"); - } - return; - } - if (e.key === "ArrowUp") { - if (selectedIndex > 0) { - selectedIndex--; - newTweetUserSearch.children[selectedIndex].classList.add( - "search-result-item-active" - ); - newTweetUserSearch.children[selectedIndex + 1].classList.remove( - "search-result-item-active" - ); - } else { - selectedIndex = newTweetUserSearch.children.length - 1; - newTweetUserSearch.children[selectedIndex].classList.add( - "search-result-item-active" - ); - newTweetUserSearch.children[0].classList.remove( - "search-result-item-active" - ); - } - return; - } - if (/(? { - let userElement = document.createElement("span"); - userElement.className = "search-result-item"; - if (index === 0) - userElement.classList.add("search-result-item-active"); - userElement.innerHTML = html` - - ${escapeHTML(user.name)} - @${user.screen_name} - `; - userElement.addEventListener("click", () => { - newTweetText.value = - newTweetText.value - .split("@") - .slice(0, -1) - .join("@") - .split(" ") - .slice(0, -1) - .join(" ") + ` @${user.screen_name} `; - if (newTweetText.value.startsWith(" ")) - newTweetText.value = newTweetText.value.slice(1); - if (newTweetText.value.length > 280) - newTweetText.value = newTweetText.value.slice(0, 280); - newTweetText.focus(); - newTweetUserSearch.innerHTML = ""; - newTweetUserSearch.hidden = true; - }); - newTweetUserSearch.appendChild(userElement); + document + .getElementsByClassName("new-tweet-view")[0] + .addEventListener("drop", (e) => { + handleDrop(e, this.mediaToUpload, mediaList); + }); + document + .getElementsByClassName("new-tweet-media-div")[0] + .addEventListener("click", async () => { + getMedia(this.mediaToUpload, mediaList); + }); + let newTweetUserSearch = document.getElementsByClassName( + "new-tweet-user-search" + )[0]; + let newTweetText = document.getElementsByClassName("new-tweet-text")[0]; + let newTweetButton = + document.getElementsByClassName("new-tweet-button")[0]; + let selectedIndex = 0; + newTweetText.addEventListener("paste", (event) => { + let items = ( + event.clipboardData || event.originalEvent.clipboardData + ).items; + for (let index in items) { + let item = items[index]; + if (item.kind === "file") { + let file = item.getAsFile(); + handleFiles( + [file], + this.mediaToUpload, + document.getElementsByClassName("new-tweet-media-c")[0] + ); + } + } }); - } else { - newTweetUserSearch.innerHTML = ""; - newTweetUserSearch.hidden = true; - } - }); - newTweetText.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { - document.getElementsByClassName("new-tweet-button")[0].click(); - } - }); - newTweetText.addEventListener("input", (e) => { - let charElement = document.getElementsByClassName("new-tweet-char")[0]; - let tweet = twttr.txt.parseTweet(e.target.value); - if (localStorage.OTisBlueVerified) { - return (charElement.innerText = `${tweet.weightedLength}`); - } - charElement.innerText = `${tweet.weightedLength}/280`; - if (tweet.weightedLength > 265) { - charElement.style.color = "#c26363"; - } else { - charElement.style.color = ""; - } - if (tweet.weightedLength > 280) { - charElement.style.color = "red"; - newTweetButton.disabled = true; - } else { - newTweetButton.disabled = false; - } - }); - document - .getElementsByClassName("new-tweet-emojis")[0] - .addEventListener("click", () => { - createEmojiPicker( - document.getElementsByClassName("new-tweet-emojis")[0], - newTweetText, - { - marginLeft: "-300px", - } - ); - }); - newTweetButton.addEventListener("click", async () => { - let tweet = document.getElementsByClassName("new-tweet-text")[0].value; - if (tweet.length === 0 && this.mediaToUpload.length === 0) return; - document.getElementsByClassName("new-tweet-button")[0].disabled = true; - let uploadedMedia = []; - for (let i in this.mediaToUpload) { - let media = this.mediaToUpload[i]; - try { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = false; - let mediaId; - if (!media.div.dataset.mediaId) { - mediaId = await API.uploadMedia({ - media_type: media.type, - media_category: media.category, - media: media.data, - alt: media.alt, - cw: media.cw, - loadCallback: (data) => { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = `${data.text} (${data.progress}%)`; - }, + newTweetText.addEventListener("focus", async (e) => { + setTimeout(() => { + if (/(? { + setTimeout(() => { + newTweetUserSearch.hidden = true; + }, 100); + }); + newTweetText.addEventListener("keypress", async (e) => { + if ( + (e.key === "Enter" || e.key === "Tab") && + !newTweetUserSearch.hidden + ) { + let activeSearch = newTweetUserSearch.querySelector( + ".search-result-item-active" + ); + if (!e.ctrlKey) { + e.preventDefault(); + e.stopPropagation(); + newTweetText.value = + newTweetText.value + .split("@") + .slice(0, -1) + .join("@") + .split(" ") + .slice(0, -1) + .join(" ") + + ` @${activeSearch + .querySelector(".search-result-item-screen-name") + .innerText.slice(1)} `; + if (newTweetText.value.startsWith(" ")) + newTweetText.value = newTweetText.value.slice(1); + newTweetUserSearch.innerHTML = ""; + newTweetUserSearch.hidden = true; + } + } + }); + newTweetText.addEventListener("keydown", async (e) => { + if (e.key === "ArrowDown") { + if (selectedIndex < newTweetUserSearch.children.length - 1) { + selectedIndex++; + newTweetUserSearch.children[selectedIndex].classList.add( + "search-result-item-active" + ); + newTweetUserSearch.children[ + selectedIndex - 1 + ].classList.remove("search-result-item-active"); + } else { + selectedIndex = 0; + newTweetUserSearch.children[selectedIndex].classList.add( + "search-result-item-active" + ); + newTweetUserSearch.children[ + newTweetUserSearch.children.length - 1 + ].classList.remove("search-result-item-active"); + } + return; + } + if (e.key === "ArrowUp") { + if (selectedIndex > 0) { + selectedIndex--; + newTweetUserSearch.children[selectedIndex].classList.add( + "search-result-item-active" + ); + newTweetUserSearch.children[ + selectedIndex + 1 + ].classList.remove("search-result-item-active"); + } else { + selectedIndex = newTweetUserSearch.children.length - 1; + newTweetUserSearch.children[selectedIndex].classList.add( + "search-result-item-active" + ); + newTweetUserSearch.children[0].classList.remove( + "search-result-item-active" + ); + } + return; + } + if (/(? { + let userElement = document.createElement("span"); + userElement.className = "search-result-item"; + if (index === 0) + userElement.classList.add("search-result-item-active"); + userElement.innerHTML = html` + + ${escapeHTML(user.name)} + @${user.screen_name} + `; + userElement.addEventListener("click", () => { + newTweetText.value = + newTweetText.value + .split("@") + .slice(0, -1) + .join("@") + .split(" ") + .slice(0, -1) + .join(" ") + ` @${user.screen_name} `; + if (newTweetText.value.startsWith(" ")) + newTweetText.value = newTweetText.value.slice(1); + if (newTweetText.value.length > 280) + newTweetText.value = newTweetText.value.slice( + 0, + 280 + ); + newTweetText.focus(); + newTweetUserSearch.innerHTML = ""; + newTweetUserSearch.hidden = true; + }); + newTweetUserSearch.appendChild(userElement); + }); + } else { + newTweetUserSearch.innerHTML = ""; + newTweetUserSearch.hidden = true; + } + }); + newTweetText.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.ctrlKey) { + document.getElementsByClassName("new-tweet-button")[0].click(); + } + }); + newTweetText.addEventListener("input", (e) => { + let charElement = + document.getElementsByClassName("new-tweet-char")[0]; + let tweet = twttr.txt.parseTweet(e.target.value); + if (localStorage.OTisBlueVerified) { + return (charElement.innerText = `${tweet.weightedLength}`); + } + charElement.innerText = `${tweet.weightedLength}/280`; + if (tweet.weightedLength > 265) { + charElement.style.color = "#c26363"; + } else { + charElement.style.color = ""; + } + if (tweet.weightedLength > 280) { + charElement.style.color = "red"; + newTweetButton.disabled = true; + } else { + newTweetButton.disabled = false; + } + }); + document + .getElementsByClassName("new-tweet-emojis")[0] + .addEventListener("click", () => { + createEmojiPicker( + document.getElementsByClassName("new-tweet-emojis")[0], + newTweetText, + { + marginLeft: "-300px", + } + ); }); - } else { - mediaId = media.div.dataset.mediaId; - } - uploadedMedia.push(mediaId); - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = LOC.uploaded.message; - media.div.dataset.mediaId = mediaId; - } catch (e) { - console.error(e); - alert(e); - for (let j in this.mediaToUpload) { - let media = this.mediaToUpload[j]; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" + newTweetButton.addEventListener("click", async () => { + let tweet = + document.getElementsByClassName("new-tweet-text")[0].value; + if (tweet.length === 0 && this.mediaToUpload.length === 0) return; + document.getElementsByClassName( + "new-tweet-button" + )[0].disabled = true; + let uploadedMedia = []; + for (let i in this.mediaToUpload) { + let media = this.mediaToUpload[i]; + try { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = false; + let mediaId; + if (!media.div.dataset.mediaId) { + mediaId = await API.uploadMedia({ + media_type: media.type, + media_category: media.category, + media: media.data, + alt: media.alt, + cw: media.cw, + loadCallback: (data) => { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = `${data.text} (${data.progress}%)`; + }, + }); + } else { + mediaId = media.div.dataset.mediaId; + } + uploadedMedia.push(mediaId); + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = LOC.uploaded.message; + media.div.dataset.mediaId = mediaId; + } catch (e) { + console.error(e); + alert(e); + for (let j in this.mediaToUpload) { + let media = this.mediaToUpload[j]; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = true; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = ""; + } + document.getElementsByClassName( + "new-tweet-button" + )[0].disabled = false; + return; // cancel tweeting + } + } + let tweetObject = { + status: tweet, + in_reply_to_status_id: replyTweet.id_str, + }; + if (this.excludeUserMentions.length > 0) + tweetObject.exclude_reply_user_ids = + this.excludeUserMentions.join(","); + if (uploadedMedia.length > 0) { + tweetObject.media_ids = uploadedMedia.join(","); + } + try { + let tweet = await API.tweet.postV2(tweetObject); + tweet._ARTIFICIAL = true; + this.appendTweet( + tweet, + document.getElementsByClassName("timeline")[0], + { + after: document.getElementsByClassName( + "new-tweet-view" + )[0].parentElement, + } + ); + } catch (e) { + document.getElementsByClassName( + "new-tweet-button" + )[0].disabled = false; + console.error(e); + } + document.getElementsByClassName("new-tweet-text")[0].value = ""; + document.getElementsByClassName("new-tweet-media-c")[0].innerHTML = + ""; + this.mediaToUpload = []; + this.excludeUserMentions = []; + document.getElementsByClassName( + "new-tweet-focused" )[0].hidden = true; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = ""; - } - document.getElementsByClassName( - "new-tweet-button" - )[0].disabled = false; - return; // cancel tweeting - } - } - let tweetObject = { - status: tweet, - in_reply_to_status_id: replyTweet.id_str, - }; - if (this.excludeUserMentions.length > 0) - tweetObject.exclude_reply_user_ids = this.excludeUserMentions.join(","); - if (uploadedMedia.length > 0) { - tweetObject.media_ids = uploadedMedia.join(","); - } - try { - let tweet = await API.tweet.postV2(tweetObject); - tweet._ARTIFICIAL = true; - this.appendTweet( - tweet, - document.getElementsByClassName("timeline")[0], - { - after: - document.getElementsByClassName("new-tweet-view")[0] - .parentElement, - } - ); - } catch (e) { - document.getElementsByClassName("new-tweet-button")[0].disabled = false; - console.error(e); - } - document.getElementsByClassName("new-tweet-text")[0].value = ""; - document.getElementsByClassName("new-tweet-media-c")[0].innerHTML = ""; - this.mediaToUpload = []; - this.excludeUserMentions = []; - document.getElementsByClassName("new-tweet-focused")[0].hidden = true; - document.getElementsByClassName("new-tweet-char")[0].hidden = true; - document - .getElementsByClassName("new-tweet-text")[0] - .classList.remove("new-tweet-text-focused"); - document - .getElementsByClassName("new-tweet-media-div")[0] - .classList.remove("new-tweet-media-div-focused"); - document.getElementsByClassName("new-tweet-button")[0].disabled = false; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - } - async appendShowMore(container, data, id) { - if (this.insertedMores.includes(data.cursor)) return; - this.tweets.push(["showmore", data, id]); - this.insertedMores.push(data.cursor); - let div = document.createElement("div"); - div.className = "show-more"; - div.innerHTML = html` - - `; - let loading = false; - div - .querySelector(".show-more-button") - .addEventListener("click", async () => { - if (loading) return; - loading = true; - div.children[0].innerText = LOC.loading_tweets.message; - await this.updateReplies(id, data.cursor); - div.remove(); - this.tweets = this.tweets.filter( - (t) => t[0] !== "showmore" || t[1].cursor !== data.cursor - ); - }); - container.appendChild(div); - } - // what dumbfuck thought having 2 almost identical functions was a good idea (me) - async appendTweet(t, timelineContainer, options = {}) { - if (this.seenReplies.includes(t.id_str)) return; - - // verification - if (t.user.ext_verified_type) { - t.user.verified_type = t.user.ext_verified_type; - t.user.verified = true; - } - if ( - vars.twitterBlueCheckmarks && - t.user.ext && - t.user.ext.isBlueVerified && - t.user.ext.isBlueVerified.r && - t.user.ext.isBlueVerified.r.ok - ) { - t.user.verified_type = "Blue"; - t.user.verified = true; - } - if ( - t.user.ext && - t.user.ext.verifiedType && - t.user.ext.verifiedType.r && - t.user.ext.verifiedType.r.ok - ) { - t.user.verified_type = t.user.ext.verifiedType.r.ok; - t.user.verified = true; - } - if (!vars.twitterBlueCheckmarks && t.user.verified_type === "Blue") { - delete t.user.verified_type; - t.user.verified = false; + document.getElementsByClassName("new-tweet-char")[0].hidden = true; + document + .getElementsByClassName("new-tweet-text")[0] + .classList.remove("new-tweet-text-focused"); + document + .getElementsByClassName("new-tweet-media-div")[0] + .classList.remove("new-tweet-media-div-focused"); + document.getElementsByClassName( + "new-tweet-button" + )[0].disabled = false; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); } - if (t.quoted_status) { - if ( - t.quoted_status.user.verified_type === "Blue" && - !vars.twitterBlueCheckmarks - ) { - delete t.quoted_status.user.verified_type; - t.quoted_status.user.verified = false; - } + async appendShowMore(container, data, id) { + if (this.insertedMores.includes(data.cursor)) return; + this.tweets.push(["showmore", data, id]); + this.insertedMores.push(data.cursor); + let div = document.createElement("div"); + div.className = "show-more"; + div.innerHTML = html` + + `; + let loading = false; + div.querySelector(".show-more-button").addEventListener( + "click", + async () => { + if (loading) return; + loading = true; + div.children[0].innerText = LOC.loading_tweets.message; + await this.updateReplies(id, data.cursor); + div.remove(); + this.tweets = this.tweets.filter( + (t) => t[0] !== "showmore" || t[1].cursor !== data.cursor + ); + } + ); + container.appendChild(div); } - this.tweets.push(["tweet", t, options]); - this.seenReplies.push(t.id_str); - const tweet = document.createElement("div"); - tweet.tweet = t; - t.options = options; - t.element = tweet; - if (!options.mainTweet) { - tweet.addEventListener("click", async (e) => { - let selection = window.getSelection(); + // what dumbfuck thought having 2 almost identical functions was a good idea (me) + async appendTweet(t, timelineContainer, options = {}) { + if (this.seenReplies.includes(t.id_str)) return; + + // verification + if (t.user.ext_verified_type) { + t.user.verified_type = t.user.ext_verified_type; + t.user.verified = true; + } if ( - selection.toString().length > 0 && - selection.focusNode && - selection.focusNode.closest(`div.tweet[data-tweet-id="${t.id_str}"]`) + vars.twitterBlueCheckmarks && + t.user.ext && + t.user.ext.isBlueVerified && + t.user.ext.isBlueVerified.r && + t.user.ext.isBlueVerified.r.ok ) { - return; + t.user.verified_type = "Blue"; + t.user.verified = true; } if ( - !e.target.closest(".tweet-button") && - !e.target.closest(".tweet-body-text-span") && - !e.target.closest(".tweet-edit-section") && - !e.target.closest(".dropdown-menu") && - !e.target.closest(".tweet-media-element") && - !e.target.closest("a") && - !e.target.closest("button") + t.user.ext && + t.user.ext.verifiedType && + t.user.ext.verifiedType.r && + t.user.ext.verifiedType.r.ok ) { - this.savePageData(); - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let restored = await this.restorePageData(); - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (this.subpage === "tweet" && !restored) { - this.updateReplies(id); - } else if (this.subpage === "likes") { - this.updateLikes(id); - } else if (this.subpage === "retweets") { - this.updateRetweets(id); - } else if (this.subpage === "retweets_with_comments") { - this.updateRetweetsWithComments(id); - } - this.currentLocation = location.pathname; + t.user.verified_type = t.user.ext.verifiedType.r.ok; + t.user.verified = true; } - }); - tweet.addEventListener("mousedown", (e) => { - if (e.button === 1) { - // tweet-media-element is clickable, since it should open the tweet in a new tab. - if ( - !e.target.closest(".tweet-button") && - !e.target.closest(".tweet-edit-section") && - !e.target.closest(".dropdown-menu") && - !e.target.closest("a") && - !e.target.closest("button") - ) { - e.preventDefault(); - openInNewTab(`/${t.user.screen_name}/status/${t.id_str}`); - } + if (!vars.twitterBlueCheckmarks && t.user.verified_type === "Blue") { + delete t.user.verified_type; + t.user.verified = false; } - }); - } - tweet.tabIndex = -1; - tweet.className = `tweet tweet-view ${ - options.mainTweet ? "tweet-main" : "tweet-replying" - }`; - if (!this.activeTweet) { - tweet.classList.add("tweet-active"); - this.activeTweet = tweet; - } - if (options.threadContinuation) - tweet.classList.add("tweet-self-thread-continuation"); - if (options.noTop) tweet.classList.add("tweet-no-top"); - if (vars.slowLinkColorsInTL) { - if (this.linkColors[t.user.id_str]) { - let sc = makeSeeableColor(this.linkColors[t.user.id_str]); - tweet.style.setProperty("--link-color", sc); - if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); - } else { - if ( - t.user.profile_link_color && - t.user.profile_link_color !== "1DA1F2" - ) { - let sc = makeSeeableColor(t.user.profile_link_color); - tweet.style.setProperty("--link-color", sc); - if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + if (t.quoted_status) { + if ( + t.quoted_status.user.verified_type === "Blue" && + !vars.twitterBlueCheckmarks + ) { + delete t.quoted_status.user.verified_type; + t.quoted_status.user.verified = false; + } } - } - } - let full_text = t.full_text ? t.full_text : ""; - let isMatchingLanguage = languageMatches(t.lang); - let isQuoteMatchingLanguage = - !!t.quoted_status && languageMatches(t.quoted_status.lang); - let videos = - t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.filter((m) => m.type === "video"); - if (!videos || videos.length === 0) { - videos = undefined; - } - if (videos) { - for (let v of videos) { - if (!v.video_info) continue; - v.video_info.variants = v.video_info.variants.sort((a, b) => { - if (!b.bitrate) return -1; - return b.bitrate - a.bitrate; - }); - if (typeof vars.savePreferredQuality !== "boolean") { - chrome.storage.sync.set( - { - savePreferredQuality: true, - }, - () => {} - ); - vars.savePreferredQuality = true; - } - if (localStorage.preferredQuality && vars.savePreferredQuality) { - let closestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return Math.abs( - parseInt(curr.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) < - Math.abs( - parseInt(prev.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) - ? curr - : prev; + this.tweets.push(["tweet", t, options]); + this.seenReplies.push(t.id_str); + const tweet = document.createElement("div"); + tweet.tweet = t; + t.options = options; + t.element = tweet; + if (!options.mainTweet) { + tweet.addEventListener("click", async (e) => { + let selection = window.getSelection(); + if ( + selection.toString().length > 0 && + selection.focusNode && + selection.focusNode.closest( + `div.tweet[data-tweet-id="${t.id_str}"]` + ) + ) { + return; + } + if ( + !e.target.closest(".tweet-button") && + !e.target.closest(".tweet-body-text-span") && + !e.target.closest(".tweet-edit-section") && + !e.target.closest(".dropdown-menu") && + !e.target.closest(".tweet-media-element") && + !e.target.closest("a") && + !e.target.closest("button") + ) { + this.savePageData(); + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let restored = await this.restorePageData(); + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (this.subpage === "tweet" && !restored) { + this.updateReplies(id); + } else if (this.subpage === "likes") { + this.updateLikes(id); + } else if (this.subpage === "retweets") { + this.updateRetweets(id); + } else if (this.subpage === "retweets_with_comments") { + this.updateRetweetsWithComments(id); + } + this.currentLocation = location.pathname; + } }); - let preferredQualityVariantIndex = v.video_info.variants.findIndex( - (v) => v.url === closestQuality.url - ); - if (preferredQualityVariantIndex !== -1) { - let preferredQualityVariant = - v.video_info.variants[preferredQualityVariantIndex]; - v.video_info.variants.splice(preferredQualityVariantIndex, 1); - v.video_info.variants.unshift(preferredQualityVariant); - } - } else if ( - window.navigator && - navigator.connection && - navigator.connection.type === "cellular" && - !vars.disableDataSaver - ) { - let lowestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return parseInt(curr.bitrate) < parseInt(prev.bitrate) - ? curr - : prev; + tweet.addEventListener("mousedown", (e) => { + if (e.button === 1) { + // tweet-media-element is clickable, since it should open the tweet in a new tab. + if ( + !e.target.closest(".tweet-button") && + !e.target.closest(".tweet-edit-section") && + !e.target.closest(".dropdown-menu") && + !e.target.closest("a") && + !e.target.closest("button") + ) { + e.preventDefault(); + openInNewTab( + `/${t.user.screen_name}/status/${t.id_str}` + ); + } + } }); - let lowestQualityVariantIndex = v.video_info.variants.findIndex( - (v) => v.url === lowestQuality.url - ); - if (lowestQualityVariantIndex !== -1) { - let lowestQualityVariant = - v.video_info.variants[lowestQualityVariantIndex]; - v.video_info.variants.splice(lowestQualityVariantIndex, 1); - v.video_info.variants.unshift(lowestQualityVariant); - } } - } - } - if ( - t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY")) - ) { - full_text = ""; - } - if (t.quoted_status_id_str && !t.quoted_status && options.mainTweet) { - //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then) - try { - if (t.quoted_status_result && t.quoted_status_result.result.tweet) { - t.quoted_status = t.quoted_status_result.result.tweet.legacy; - t.quoted_status.user = - t.quoted_status_result.result.tweet.core.user_results.result.legacy; - } else { - t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str); + tweet.tabIndex = -1; + tweet.className = `tweet tweet-view ${ + options.mainTweet ? "tweet-main" : "tweet-replying" + }`; + if (!this.activeTweet) { + tweet.classList.add("tweet-active"); + this.activeTweet = tweet; } - } catch { - t.quoted_status = undefined; - } - } - let followUserText, unfollowUserText, blockUserText, unblockUserText; - let mentionedUserText = ``; - let quoteMentionedUserText = ``; - if ( - LOC.follow_user.message.includes("$SCREEN_NAME$") && - LOC.unfollow_user.message.includes("$SCREEN_NAME$") && - LOC.block_user.message.includes("$SCREEN_NAME$") && - LOC.unblock_user.message.includes("$SCREEN_NAME$") - ) { - followUserText = `${LOC.follow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - unfollowUserText = `${LOC.unfollow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - blockUserText = `${LOC.block_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - unblockUserText = `${LOC.unblock_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - )}`; - } else { - followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; - unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; - blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; - unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; - } - if (t.in_reply_to_screen_name && t.display_text_range) { - t.entities.user_mentions.forEach((user_mention) => { - if (user_mention.indices[0] < t.display_text_range[0]) { - mentionedUserText += `@${user_mention.screen_name} `; + if (options.threadContinuation) + tweet.classList.add("tweet-self-thread-continuation"); + if (options.noTop) tweet.classList.add("tweet-no-top"); + if (vars.slowLinkColorsInTL) { + if (this.linkColors[t.user.id_str]) { + let sc = makeSeeableColor(this.linkColors[t.user.id_str]); + tweet.style.setProperty("--link-color", sc); + if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + } else { + if ( + t.user.profile_link_color && + t.user.profile_link_color !== "1DA1F2" + ) { + let sc = makeSeeableColor(t.user.profile_link_color); + tweet.style.setProperty("--link-color", sc); + if (vars.alwaysShowLinkColor) tweet.classList.add("colour"); + } + } } - //else this is not reply but mention - }); - } - if ( - t.quoted_status && - t.quoted_status.in_reply_to_screen_name && - t.display_text_range - ) { - t.quoted_status.entities.user_mentions.forEach((user_mention) => { - if (user_mention.indices[0] < t.display_text_range[0]) { - quoteMentionedUserText += `@${user_mention.screen_name} `; + let full_text = t.full_text ? t.full_text : ""; + let isMatchingLanguage = languageMatches(t.lang); + let isQuoteMatchingLanguage = + !!t.quoted_status && languageMatches(t.quoted_status.lang); + let videos = + t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.filter((m) => m.type === "video"); + if (!videos || videos.length === 0) { + videos = undefined; } - //else this is not reply but mention - }); - } - console.log(t); - tweet.innerHTML = html` - - ${t.user.name} -
- - ${escapeHTML(t.user.name)} - @${t.user.screen_name} - - ${options.mainTweet && t.user.id_str !== user.id_str - ? `` - : ""} -
- ${timeElapsed(new Date(t.created_at).getTime())} -
-
- ${vars.useOldStyleReply - ? /*html*/ mentionedUserText - : ""}${full_text ? await renderTweetBodyHTML(t) : ""} -
- ${!isMatchingLanguage - ? html` -
- ${LOC.view_translation.message} - ` - : ``} - ${t.extended_entities && t.extended_entities.media - ? html` -
- ${t.extended_entities.media.length === 1 && - t.extended_entities.media[0].type === "video" - ? html` -
- - - - - - -
- ` - : ""} - ${renderMedia(t)} -
- ${t.extended_entities && - t.extended_entities.media && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ? `
GIF
` - : ""} - ${videos - ? html` -
- ${videos[0].ext && - videos[0].ext.mediaStats && - videos[0].ext.mediaStats.r && - videos[0].ext.mediaStats.r.ok - ? `${Number( - videos[0].ext.mediaStats.r.ok.viewCount - ) - .toLocaleString() - .replace(/\s/g, ",")} ${ - LOC.views.message - } • ` - : ""}${LOC.reload.message} - • - ${videos[0].video_info.variants + if (videos) { + for (let v of videos) { + if (!v.video_info) continue; + v.video_info.variants = v.video_info.variants.sort((a, b) => { + if (!b.bitrate) return -1; + return b.bitrate - a.bitrate; + }); + if (typeof vars.savePreferredQuality !== "boolean") { + chrome.storage.sync.set( + { + savePreferredQuality: true, + }, + () => {} + ); + vars.savePreferredQuality = true; + } + if ( + localStorage.preferredQuality && + vars.savePreferredQuality + ) { + let closestQuality = v.video_info.variants .filter((v) => v.bitrate) - .map( - (v) => - `${v.url.match(/\/(\d+)x/)[1] + "p"} ` - ) - .join(" / ")} -
- ` - : ``} - - ` - : ``} - ${t.card ? `
` : ""} - ${t.quoted_status - ? html` - - ${escapeHTML(t.quoted_status.user.name, true)} -
- - { + return Math.abs( + parseInt(curr.url.match(/\/(\d+)x/)[1]) - + parseInt(localStorage.preferredQuality) + ) < + Math.abs( + parseInt(prev.url.match(/\/(\d+)x/)[1]) - + parseInt(localStorage.preferredQuality) + ) + ? curr + : prev; + }); + let preferredQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === closestQuality.url + ); + if (preferredQualityVariantIndex !== -1) { + let preferredQualityVariant = + v.video_info.variants[preferredQualityVariantIndex]; + v.video_info.variants.splice( + preferredQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift(preferredQualityVariant); + } + } else if ( + window.navigator && + navigator.connection && + navigator.connection.type === "cellular" && + !vars.disableDataSaver + ) { + let lowestQuality = v.video_info.variants + .filter((v) => v.bitrate) + .reduce((prev, curr) => { + return parseInt(curr.bitrate) < + parseInt(prev.bitrate) + ? curr + : prev; + }); + let lowestQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === lowestQuality.url + ); + if (lowestQualityVariantIndex !== -1) { + let lowestQualityVariant = + v.video_info.variants[lowestQualityVariantIndex]; + v.video_info.variants.splice( + lowestQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift(lowestQualityVariant); + } + } + } + } + if ( + t.withheld_in_countries && + (t.withheld_in_countries.includes("XX") || + t.withheld_in_countries.includes("XY")) + ) { + full_text = ""; + } + if (t.quoted_status_id_str && !t.quoted_status && options.mainTweet) { + //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then) + try { + if ( + t.quoted_status_result && + t.quoted_status_result.result.tweet + ) { + t.quoted_status = + t.quoted_status_result.result.tweet.legacy; + t.quoted_status.user = + t.quoted_status_result.result.tweet.core.user_results.result.legacy; + } else { + t.quoted_status = await API.tweet.getV2( + t.quoted_status_id_str + ); + } + } catch { + t.quoted_status = undefined; + } + } + let followUserText, unfollowUserText, blockUserText, unblockUserText; + let mentionedUserText = ``; + let quoteMentionedUserText = ``; + if ( + LOC.follow_user.message.includes("$SCREEN_NAME$") && + LOC.unfollow_user.message.includes("$SCREEN_NAME$") && + LOC.block_user.message.includes("$SCREEN_NAME$") && + LOC.unblock_user.message.includes("$SCREEN_NAME$") + ) { + followUserText = `${LOC.follow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + unfollowUserText = `${LOC.unfollow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + blockUserText = `${LOC.block_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + unblockUserText = `${LOC.unblock_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + )}`; + } else { + followUserText = `${LOC.follow_user.message} @${t.user.screen_name}`; + unfollowUserText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; + blockUserText = `${LOC.block_user.message} @${t.user.screen_name}`; + unblockUserText = `${LOC.unblock_user.message} @${t.user.screen_name}`; + } + if (t.in_reply_to_screen_name && t.display_text_range) { + t.entities.user_mentions.forEach((user_mention) => { + if (user_mention.indices[0] < t.display_text_range[0]) { + mentionedUserText += `@${user_mention.screen_name} `; + } + //else this is not reply but mention + }); + } + if ( + t.quoted_status && + t.quoted_status.in_reply_to_screen_name && + t.display_text_range + ) { + t.quoted_status.entities.user_mentions.forEach((user_mention) => { + if (user_mention.indices[0] < t.display_text_range[0]) { + quoteMentionedUserText += `@${user_mention.screen_name} `; + } + //else this is not reply but mention + }); + } + console.log(t); + tweet.innerHTML = html` + + ${t.user.name} + - ${timeElapsed( - new Date(t.quoted_status.created_at).getTime() - )} - ${quoteMentionedUserText !== `` && !vars.useOldStyleReply - ? html` - ${LOC.replying_to_user.message.replace( - "$SCREEN_NAME$", - quoteMentionedUserText - .trim() - .replaceAll(` `, LOC.replying_to_comma.message) - .replace( - LOC.replying_to_comma.message, - LOC.replying_to_and.message - ) - )} - ` - : ""} - ${vars.useOldStyleReply ? quoteMentionedUserText : ""}${t - .quoted_status.full_text - ? await renderTweetBodyHTML(t, true) - : ""} + ${options.mainTweet && t.user.id_str !== user.id_str + ? `` + : ""} +
+ ${timeElapsed(new Date(t.created_at).getTime())} +
+
- ${t.quoted_status.extended_entities && - t.quoted_status.extended_entities.media - ? html` -
- ${t.quoted_status.extended_entities.media - .map( - (m) => - `<${m.type === "photo" ? "img" : "video"} ${ - m.ext_alt_text - ? `alt="${escapeHTML( - m.ext_alt_text, - true - )}" title="${escapeHTML( - m.ext_alt_text, - true - )}"` - : "" - } crossorigin="anonymous" width="${ - quoteSizeFunctions[ - t.quoted_status.extended_entities.media.length - ]( - m.original_info.width, - m.original_info.height - )[0] - }" height="${ - quoteSizeFunctions[ - t.quoted_status.extended_entities.media.length - ]( - m.original_info.width, - m.original_info.height - )[1] - }" loading="lazy" ${ - m.type === "video" - ? "disableRemotePlayback controls" - : "" - } ${ - m.type === "animated_gif" - ? "disableRemotePlayback loop muted" - : "" - }${ - m.type === "animated_gif" && - !vars.disableGifAutoplay - ? " autoplay" - : "" - } src="${ - m.type === "photo" - ? m.media_url_https + - (vars.showOriginalImages && - (m.media_url_https.endsWith(".jpg") || - m.media_url_https.endsWith(".png")) - ? "?name=orig" - : window.navigator && - navigator.connection && - navigator.connection.type === - "cellular" && - !vars.disableDataSaver - ? "?name=small" - : "") - : m.video_info.variants.find( - (v) => v.content_type === "video/mp4" - ).url - }" class="tweet-media-element tweet-media-element-quote ${ - m.type === "animated_gif" - ? "tweet-media-element-quote-gif" - : "" - } ${ - mediaClasses[ - t.quoted_status.extended_entities.media.length - ] - } ${ - !vars.displaySensitiveContent && - t.quoted_status.possibly_sensitive - ? "tweet-media-element-censor" - : "" - }">${m.type === "photo" ? "" : ""}` + ${vars.useOldStyleReply + ? /*html*/ mentionedUserText + : ""}${full_text + ? await renderTweetBodyHTML(t) + : ""} +
+ ${!isMatchingLanguage + ? html` +
+ ${LOC.view_translation.message} + ` + : ``} + ${t.extended_entities && t.extended_entities.media + ? html` +
+ ${t.extended_entities.media.length === 1 && + t.extended_entities.media[0].type === "video" + ? html` +
+ + + + + + +
+ ` + : ""} + ${renderMedia(t)} +
+ ${t.extended_entities && + t.extended_entities.media && + t.extended_entities.media.some( + (m) => m.type === "animated_gif" ) - .join("\n")} -
- ` - : ""} - ${!isQuoteMatchingLanguage - ? html` - ${LOC.view_translation.message} - ` - : ``} - - ` - : ``} - ${t.limited_actions === "limit_trusted_friends_tweet" && - options.mainTweet - ? ` + ? `
GIF
` + : ""} + ${videos + ? html` +
+ ${videos[0].ext && + videos[0].ext.mediaStats && + videos[0].ext.mediaStats.r && + videos[0].ext.mediaStats.r.ok + ? `${Number( + videos[0].ext.mediaStats.r.ok + .viewCount + ) + .toLocaleString() + .replace(/\s/g, ",")} ${ + LOC.views.message + } • ` + : ""}${LOC.reload.message} + • + ${videos[0].video_info.variants + .filter((v) => v.bitrate) + .map( + (v) => + `${ + v.url.match( + /\/(\d+)x/ + )[1] + "p" + } ` + ) + .join(" / ")} +
+ ` + : ``} + + ` + : ``} + ${t.card ? `
` : ""} + ${t.quoted_status + ? html` + + ${escapeHTML(
+                                      t.quoted_status.user.name,
+                                      true
+                                  )} +
+ + ${escapeHTML( + t.quoted_status.user.name + )} + @${t.quoted_status.user + .screen_name} + +
+ ${timeElapsed( + new Date( + t.quoted_status.created_at + ).getTime() + )} + ${quoteMentionedUserText !== `` && + !vars.useOldStyleReply + ? html` + ${LOC.replying_to_user.message.replace( + "$SCREEN_NAME$", + quoteMentionedUserText + .trim() + .replaceAll( + ` `, + LOC.replying_to_comma + .message + ) + .replace( + LOC.replying_to_comma + .message, + LOC.replying_to_and + .message + ) + )} + ` + : ""} + ${vars.useOldStyleReply + ? quoteMentionedUserText + : ""}${t.quoted_status.full_text + ? await renderTweetBodyHTML(t, true) + : ""} + ${t.quoted_status.extended_entities && + t.quoted_status.extended_entities.media + ? html` +
+ ${t.quoted_status.extended_entities.media + .map( + (m) => + `<${ + m.type === "photo" + ? "img" + : "video" + } ${ + m.ext_alt_text + ? `alt="${escapeHTML( + m.ext_alt_text, + true + )}" title="${escapeHTML( + m.ext_alt_text, + true + )}"` + : "" + } crossorigin="anonymous" width="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities + .media + .length + ]( + m.original_info + .width, + m.original_info + .height + )[0] + }" height="${ + quoteSizeFunctions[ + t.quoted_status + .extended_entities + .media + .length + ]( + m.original_info + .width, + m.original_info + .height + )[1] + }" loading="lazy" ${ + m.type === "video" + ? "disableRemotePlayback controls" + : "" + } ${ + m.type === + "animated_gif" + ? "disableRemotePlayback loop muted" + : "" + }${ + m.type === + "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } src="${ + m.type === "photo" + ? m.media_url_https + + (vars.showOriginalImages && + (m.media_url_https.endsWith( + ".jpg" + ) || + m.media_url_https.endsWith( + ".png" + )) + ? "?name=orig" + : window.navigator && + navigator.connection && + navigator + .connection + .type === + "cellular" && + !vars.disableDataSaver + ? "?name=small" + : "") + : m.video_info.variants.find( + (v) => + v.content_type === + "video/mp4" + ).url + }" class="tweet-media-element tweet-media-element-quote ${ + m.type === + "animated_gif" + ? "tweet-media-element-quote-gif" + : "" + } ${ + mediaClasses[ + t.quoted_status + .extended_entities + .media + .length + ] + } ${ + !vars.displaySensitiveContent && + t.quoted_status + .possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${ + m.type === "photo" + ? "" + : "" + }` + ) + .join("\n")} +
+ ` + : ""} + ${!isQuoteMatchingLanguage + ? html` + ${LOC.view_translation + .message} + ` + : ``} +
+ ` + : ``} + ${t.limited_actions === "limit_trusted_friends_tweet" && + options.mainTweet + ? `
${LOC.circle_limited_tweet.message} ${LOC.learn_more.message}
`.replace( - "$SCREEN_NAME$", - tweet.trusted_circle_owner - ? tweet.trusted_circle_owner - : tweetStorage[t.conversation_id_str] - ? tweetStorage[t.conversation_id_str].user.screen_name - : t.in_reply_to_screen_name - ? t.in_reply_to_screen_name - : t.user.screen_name - ) - : ""} - ${t.tombstone ? `
${t.tombstone}
` : ""} - ${(t.withheld_in_countries && - (t.withheld_in_countries.includes("XX") || - t.withheld_in_countries.includes("XY"))) || - t.withheld_scope - ? `
This Tweet has been withheld in response to a report from the copyright holder. Learn more.
` - : ""} - ${t.conversation_control - ? `
${ - t.limited_actions_text - ? t.limited_actions_text - : LOC.limited_tweet.message - }${ - t.conversation_control.policy && - (t.user.id_str === user.id_str || - (t.conversation_control.policy.toLowerCase() === "community" && - (t.user.followed_by || - (full_text && - full_text.includes(`@${user.screen_name}`)))) || - (t.conversation_control.policy.toLowerCase() === - "by_invitation" && - full_text && - full_text.includes(`@${user.screen_name}`))) - ? " " + LOC.you_can_reply.message - : "" - }
` - : ""} - ${options.mainTweet - ? html` -
- `; - let gifs = Array.from( - tweet.querySelectorAll(".tweet-media-gif, .tweet-media-element-quote-gif") - ); - if (gifs.length) { - gifs.forEach((gif) => { - gif.addEventListener("click", () => { - if (gif.paused) gif.play(); - else gif.pause(); - }); - }); - } - // video - let vidOverlay = tweet.getElementsByClassName( - "tweet-media-video-overlay" - )[0]; - if (vidOverlay) { - vidOverlay.addEventListener("click", async () => { - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - try { - let res = await fetch(vid.currentSrc); - if (!res.headers.get("content-length")) await sleep(1000); - } catch (e) { - console.error(e); - } - vid.play(); - vid.controls = true; - vid.classList.remove("tweet-media-element-censor"); - vidOverlay.style.display = "none"; - }); - } - if (videos) { - let vids = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO"); - vids[0].onloadstart = () => { - let src = vids[0].currentSrc; - Array.from(tweet.getElementsByClassName("tweet-video-quality")).forEach( - (el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - } + +
+ +
+
+
+ `; + let gifs = Array.from( + tweet.querySelectorAll( + ".tweet-media-gif, .tweet-media-element-quote-gif" + ) ); - tweet - .getElementsByClassName("tweet-video-reload")[0] - .addEventListener("click", () => { - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - let time = vid.currentTime; - let paused = vid.paused; - vid.load(); - vid.onloadstart = () => { - let src = vid.currentSrc; - vid.currentTime = time; - if (!paused) vid.play(); - Array.from( - tweet.getElementsByClassName("tweet-video-quality") - ).forEach((el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - else el.classList.remove("tweet-video-quality-current"); - }); - }; - }); - Array.from(tweet.getElementsByClassName("tweet-video-quality")).forEach( - (el) => - el.addEventListener("click", () => { - if (el.className.includes("tweet-video-quality-current")) return; - localStorage.preferredQuality = parseInt(el.innerText); - let vid = Array.from( - tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - let time = vid.currentTime; - let paused = vid.paused; - for (let v of videos) { - let closestQuality = v.video_info.variants - .filter((v) => v.bitrate) - .reduce((prev, curr) => { - return Math.abs( - parseInt(curr.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) < - Math.abs( - parseInt(prev.url.match(/\/(\d+)x/)[1]) - - parseInt(localStorage.preferredQuality) - ) - ? curr - : prev; - }); - let preferredQualityVariantIndex = - v.video_info.variants.findIndex( - (v) => v.url === closestQuality.url - ); - if (preferredQualityVariantIndex !== -1) { - let preferredQualityVariant = - v.video_info.variants[preferredQualityVariantIndex]; - v.video_info.variants.splice(preferredQualityVariantIndex, 1); - v.video_info.variants.unshift(preferredQualityVariant); + if (gifs.length) { + gifs.forEach((gif) => { + gif.addEventListener("click", () => { + if (gif.paused) gif.play(); + else gif.pause(); + }); + }); + } + // video + let vidOverlay = tweet.getElementsByClassName( + "tweet-media-video-overlay" + )[0]; + if (vidOverlay) { + vidOverlay.addEventListener("click", async () => { + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0].children + ).filter((e) => e.tagName === "VIDEO")[0]; + try { + let res = await fetch(vid.currentSrc); + if (!res.headers.get("content-length")) await sleep(1000); + } catch (e) { + console.error(e); } - } - tweet.getElementsByClassName("tweet-media")[0].innerHTML = html` - ${t.extended_entities.media - .map( - (m) => - `<${m.type === "photo" ? "img" : "video"} ${ - m.ext_alt_text - ? `alt="${escapeHTML( - m.ext_alt_text, - true - )}" title="${escapeHTML(m.ext_alt_text, true)}"` - : "" - } crossorigin="anonymous" width="${ - sizeFunctions[t.extended_entities.media.length]( - m.original_info.width, - m.original_info.height - )[0] - }" height="${ - sizeFunctions[t.extended_entities.media.length]( - m.original_info.width, - m.original_info.height - )[1] - }" loading="lazy" ${ - m.type === "video" ? "controls" : "" - } ${m.type === "animated_gif" ? "loop muted" : ""}${ - m.type === "animated_gif" && !vars.disableGifAutoplay - ? " autoplay" - : "" - } ${ - m.type === "photo" ? `src="${m.media_url_https}"` : "" - } class="tweet-media-element ${ - mediaClasses[t.extended_entities.media.length] - } ${ - !vars.displaySensitiveContent && t.possibly_sensitive - ? "tweet-media-element-censor" - : "" - }">${ - m.type === "video" || m.type === "animated_gif" - ? ` - ${m.video_info.variants - .map( - (v) => - `` - ) - .join("\n")} - ${LOC.unsupported_video.message} - ` - : "" - }` - ) - .join("\n")} - `; - vid = Array.from( + vid.play(); + vid.controls = true; + vid.classList.remove("tweet-media-element-censor"); + vidOverlay.style.display = "none"; + }); + } + if (videos) { + let vids = Array.from( tweet.getElementsByClassName("tweet-media")[0].children - ).filter((e) => e.tagName === "VIDEO")[0]; - vid.onloadstart = () => { - let src = vid.currentSrc; - vid.currentTime = time; - if (!paused) vid.play(); + ).filter((e) => e.tagName === "VIDEO"); + vids[0].onloadstart = () => { + let src = vids[0].currentSrc; Array.from( - tweet.getElementsByClassName("tweet-video-quality") + tweet.getElementsByClassName("tweet-video-quality") ).forEach((el) => { - if (el.dataset.url === src) - el.classList.add("tweet-video-quality-current"); - else el.classList.remove("tweet-video-quality-current"); + if (el.dataset.url === src) + el.classList.add("tweet-video-quality-current"); }); - }; - vid.addEventListener("mousedown", (e) => { - if (e.button === 1) { - e.preventDefault(); - window.open(vid.currentSrc, "_blank"); + tweet + .getElementsByClassName("tweet-video-reload")[0] + .addEventListener("click", () => { + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + let time = vid.currentTime; + let paused = vid.paused; + vid.load(); + vid.onloadstart = () => { + let src = vid.currentSrc; + vid.currentTime = time; + if (!paused) vid.play(); + Array.from( + tweet.getElementsByClassName( + "tweet-video-quality" + ) + ).forEach((el) => { + if (el.dataset.url === src) + el.classList.add( + "tweet-video-quality-current" + ); + else + el.classList.remove( + "tweet-video-quality-current" + ); + }); + }; + }); + Array.from( + tweet.getElementsByClassName("tweet-video-quality") + ).forEach((el) => + el.addEventListener("click", () => { + if ( + el.className.includes("tweet-video-quality-current") + ) + return; + localStorage.preferredQuality = parseInt(el.innerText); + let vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + let time = vid.currentTime; + let paused = vid.paused; + for (let v of videos) { + let closestQuality = v.video_info.variants + .filter((v) => v.bitrate) + .reduce((prev, curr) => { + return Math.abs( + parseInt( + curr.url.match(/\/(\d+)x/)[1] + ) - + parseInt( + localStorage.preferredQuality + ) + ) < + Math.abs( + parseInt( + prev.url.match(/\/(\d+)x/)[1] + ) - + parseInt( + localStorage.preferredQuality + ) + ) + ? curr + : prev; + }); + let preferredQualityVariantIndex = + v.video_info.variants.findIndex( + (v) => v.url === closestQuality.url + ); + if (preferredQualityVariantIndex !== -1) { + let preferredQualityVariant = + v.video_info.variants[ + preferredQualityVariantIndex + ]; + v.video_info.variants.splice( + preferredQualityVariantIndex, + 1 + ); + v.video_info.variants.unshift( + preferredQualityVariant + ); + } + } + tweet.getElementsByClassName( + "tweet-media" + )[0].innerHTML = html` + ${t.extended_entities.media + .map( + (m) => + `<${ + m.type === "photo" ? "img" : "video" + } ${ + m.ext_alt_text + ? `alt="${escapeHTML( + m.ext_alt_text, + true + )}" title="${escapeHTML( + m.ext_alt_text, + true + )}"` + : "" + } crossorigin="anonymous" width="${ + sizeFunctions[ + t.extended_entities.media.length + ]( + m.original_info.width, + m.original_info.height + )[0] + }" height="${ + sizeFunctions[ + t.extended_entities.media.length + ]( + m.original_info.width, + m.original_info.height + )[1] + }" loading="lazy" ${ + m.type === "video" ? "controls" : "" + } ${ + m.type === "animated_gif" + ? "loop muted" + : "" + }${ + m.type === "animated_gif" && + !vars.disableGifAutoplay + ? " autoplay" + : "" + } ${ + m.type === "photo" + ? `src="${m.media_url_https}"` + : "" + } class="tweet-media-element ${ + mediaClasses[ + t.extended_entities.media.length + ] + } ${ + !vars.displaySensitiveContent && + t.possibly_sensitive + ? "tweet-media-element-censor" + : "" + }">${ + m.type === "video" || + m.type === "animated_gif" + ? ` + ${m.video_info.variants + .map( + (v) => + `` + ) + .join("\n")} + ${LOC.unsupported_video.message} + ` + : "" + }` + ) + .join("\n")} + `; + vid = Array.from( + tweet.getElementsByClassName("tweet-media")[0] + .children + ).filter((e) => e.tagName === "VIDEO")[0]; + vid.onloadstart = () => { + let src = vid.currentSrc; + vid.currentTime = time; + if (!paused) vid.play(); + Array.from( + tweet.getElementsByClassName( + "tweet-video-quality" + ) + ).forEach((el) => { + if (el.dataset.url === src) + el.classList.add( + "tweet-video-quality-current" + ); + else + el.classList.remove( + "tweet-video-quality-current" + ); + }); + }; + vid.addEventListener("mousedown", (e) => { + if (e.button === 1) { + e.preventDefault(); + window.open(vid.currentSrc, "_blank"); + } + }); + }) + ); + }; + for (let vid of vids) { + if (!vars.muteVideos && typeof vars.volume === "number") { + vid.volume = vars.volume; } - }); - }) - ); - }; - for (let vid of vids) { - if (!vars.muteVideos && typeof vars.volume === "number") { - vid.volume = vars.volume; + vid.onvolumechange = () => { + chrome.storage.sync.set( + { + volume: vid.volume, + }, + () => {} + ); + if (vars.muteVideos) return; + let allVids = document.getElementsByTagName("video"); + for (let i = 0; i < allVids.length; i++) { + allVids[i].volume = vid.volume; + } + }; + vid.addEventListener("mousedown", (e) => { + if (e.button === 1) { + e.preventDefault(); + window.open(vid.currentSrc, "_blank"); + } + }); + } } - vid.onvolumechange = () => { - chrome.storage.sync.set( - { - volume: vid.volume, - }, - () => {} - ); - if (vars.muteVideos) return; - let allVids = document.getElementsByTagName("video"); - for (let i = 0; i < allVids.length; i++) { - allVids[i].volume = vid.volume; - } - }; - vid.addEventListener("mousedown", (e) => { - if (e.button === 1) { - e.preventDefault(); - window.open(vid.currentSrc, "_blank"); - } - }); - } - } - let footerFavorites = tweet.getElementsByClassName( - "tweet-footer-favorites" - )[0]; - if (t.card) { - generateCard(t, tweet, user); - } - if (options.top) { - tweet.querySelector(".tweet-top").hidden = false; - const icon = document.createElement("span"); - icon.innerText = options.top.icon; - icon.classList.add("tweet-top-icon"); - icon.style.color = options.top.color; + let footerFavorites = tweet.getElementsByClassName( + "tweet-footer-favorites" + )[0]; + if (t.card) { + generateCard(t, tweet, user); + } + if (options.top) { + tweet.querySelector(".tweet-top").hidden = false; + const icon = document.createElement("span"); + icon.innerText = options.top.icon; + icon.classList.add("tweet-top-icon"); + icon.style.color = options.top.color; - const span = document.createElement("span"); - span.classList.add("tweet-top-text"); - span.innerHTML = options.top.text; - tweet.querySelector(".tweet-top").append(icon, span); - } - if (options.mainTweet) { - let likers = - vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 - ? this.mainTweetLikers.slice(0, 6) - : this.mainTweetLikers.slice(0, 8); - for (let i in likers) { - let liker = likers[i]; - let a = document.createElement("a"); - a.href = `/${liker.screen_name}`; - let likerImg = document.createElement("img"); - likerImg.src = `${ - liker.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(liker.id_str) % 7 - }_normal.png` - ) - : liker.profile_image_url_https - }`; - likerImg.classList.add("tweet-footer-favorites-img"); - likerImg.title = liker.name + " (@" + liker.screen_name + ")"; - likerImg.width = 24; - likerImg.height = 24; - a.appendChild(likerImg); - a.dataset.id = liker.id_str; - footerFavorites.appendChild(a); - } - let likesLink = tweet.getElementsByClassName("tweet-footer-stat-f")[0]; - likesLink.addEventListener("click", (e) => { - e.preventDefault(); - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/likes` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateLikes(id); - this.currentLocation = location.pathname; - }); - let retweetsLink = tweet.getElementsByClassName("tweet-footer-stat-r")[0]; - retweetsLink.addEventListener("click", (e) => { - e.preventDefault(); - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweets(id); - this.currentLocation = location.pathname; - }); - if ( - vars.showQuoteCount && - typeof t.quote_count !== "undefined" && - t.quote_count > 0 - ) { - let quotesLink = tweet.getElementsByClassName("tweet-footer-stat-q")[0]; - quotesLink.addEventListener("click", (e) => { - e.preventDefault(); - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweetsWithComments(id); - this.currentLocation = location.pathname; - }); - } - let repliesLink = tweet.getElementsByClassName("tweet-footer-stat-o")[0]; - repliesLink.addEventListener("click", (e) => { - e.preventDefault(); - if (location.href === `/${t.user.screen_name}/status/${t.id_str}`) - return; - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateReplies(id); - this.currentLocation = location.pathname; - }); - } - if (options.mainTweet && t.user.id_str !== user.id_str) { - const tweetFollow = tweet.getElementsByClassName( - "tweet-header-follow" - )[0]; - tweetFollow.addEventListener("click", async () => { - if (t.user.following) { - await API.user.unfollow(t.user.screen_name); - tweetFollow.innerText = LOC.follow.message; - tweetFollow.classList.remove("following"); - tweetFollow.classList.add("follow"); - t.user.following = false; - } else { - await API.user.follow(t.user.screen_name); - tweetFollow.innerText = LOC.unfollow.message; - tweetFollow.classList.remove("follow"); - tweetFollow.classList.add("following"); - t.user.following = true; + const span = document.createElement("span"); + span.classList.add("tweet-top-text"); + span.innerHTML = options.top.text; + tweet.querySelector(".tweet-top").append(icon, span); } - }); - } - const tweetBody = tweet.getElementsByClassName("tweet-body")[0]; - const tweetBodyText = tweet.getElementsByClassName("tweet-body-text")[0]; - const tweetTranslate = tweet.getElementsByClassName("tweet-translate")[0]; - const tweetQuoteTranslate = tweet.getElementsByClassName( - "tweet-quote-translate" - )[0]; - const tweetBodyQuote = tweet.getElementsByClassName("tweet-body-quote")[0]; - const tweetBodyQuoteText = tweet.getElementsByClassName( - "tweet-body-text-quote" - )[0]; + if (options.mainTweet) { + let likers = + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ? this.mainTweetLikers.slice(0, 6) + : this.mainTweetLikers.slice(0, 8); + for (let i in likers) { + let liker = likers[i]; + let a = document.createElement("a"); + a.href = `/${liker.screen_name}`; + let likerImg = document.createElement("img"); + likerImg.src = `${ + liker.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(liker.id_str) % 7 + }_normal.png` + ) + : liker.profile_image_url_https + }`; + likerImg.classList.add("tweet-footer-favorites-img"); + likerImg.title = liker.name + " (@" + liker.screen_name + ")"; + likerImg.width = 24; + likerImg.height = 24; + a.appendChild(likerImg); + a.dataset.id = liker.id_str; + footerFavorites.appendChild(a); + } + let likesLink = tweet.getElementsByClassName( + "tweet-footer-stat-f" + )[0]; + likesLink.addEventListener("click", (e) => { + e.preventDefault(); + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/likes` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateLikes(id); + this.currentLocation = location.pathname; + }); + let retweetsLink = tweet.getElementsByClassName( + "tweet-footer-stat-r" + )[0]; + retweetsLink.addEventListener("click", (e) => { + e.preventDefault(); + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweets(id); + this.currentLocation = location.pathname; + }); + if ( + vars.showQuoteCount && + typeof t.quote_count !== "undefined" && + t.quote_count > 0 + ) { + let quotesLink = tweet.getElementsByClassName( + "tweet-footer-stat-q" + )[0]; + quotesLink.addEventListener("click", (e) => { + e.preventDefault(); + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweetsWithComments(id); + this.currentLocation = location.pathname; + }); + } + let repliesLink = tweet.getElementsByClassName( + "tweet-footer-stat-o" + )[0]; + repliesLink.addEventListener("click", (e) => { + e.preventDefault(); + if ( + location.href === + `/${t.user.screen_name}/status/${t.id_str}` + ) + return; + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateReplies(id); + this.currentLocation = location.pathname; + }); + } + if (options.mainTweet && t.user.id_str !== user.id_str) { + const tweetFollow = tweet.getElementsByClassName( + "tweet-header-follow" + )[0]; + tweetFollow.addEventListener("click", async () => { + if (t.user.following) { + await API.user.unfollow(t.user.screen_name); + tweetFollow.innerText = LOC.follow.message; + tweetFollow.classList.remove("following"); + tweetFollow.classList.add("follow"); + t.user.following = false; + } else { + await API.user.follow(t.user.screen_name); + tweetFollow.innerText = LOC.unfollow.message; + tweetFollow.classList.remove("follow"); + tweetFollow.classList.add("following"); + t.user.following = true; + } + }); + } + const tweetBody = tweet.getElementsByClassName("tweet-body")[0]; + const tweetBodyText = + tweet.getElementsByClassName("tweet-body-text")[0]; + const tweetTranslate = + tweet.getElementsByClassName("tweet-translate")[0]; + const tweetQuoteTranslate = tweet.getElementsByClassName( + "tweet-quote-translate" + )[0]; + const tweetBodyQuote = + tweet.getElementsByClassName("tweet-body-quote")[0]; + const tweetBodyQuoteText = tweet.getElementsByClassName( + "tweet-body-text-quote" + )[0]; - const tweetReplyCancel = - tweet.getElementsByClassName("tweet-reply-cancel")[0]; - const tweetReplyUpload = - tweet.getElementsByClassName("tweet-reply-upload")[0]; - const tweetReplyAddEmoji = tweet.getElementsByClassName( - "tweet-reply-add-emoji" - )[0]; - const tweetReply = tweet.getElementsByClassName("tweet-reply")[0]; - const tweetReplyButton = - tweet.getElementsByClassName("tweet-reply-button")[0]; - const tweetReplyError = - tweet.getElementsByClassName("tweet-reply-error")[0]; - const tweetReplyText = tweet.getElementsByClassName("tweet-reply-text")[0]; - const tweetReplyChar = tweet.getElementsByClassName("tweet-reply-char")[0]; - const tweetReplyMedia = - tweet.getElementsByClassName("tweet-reply-media")[0]; + const tweetReplyCancel = + tweet.getElementsByClassName("tweet-reply-cancel")[0]; + const tweetReplyUpload = + tweet.getElementsByClassName("tweet-reply-upload")[0]; + const tweetReplyAddEmoji = tweet.getElementsByClassName( + "tweet-reply-add-emoji" + )[0]; + const tweetReply = tweet.getElementsByClassName("tweet-reply")[0]; + const tweetReplyButton = + tweet.getElementsByClassName("tweet-reply-button")[0]; + const tweetReplyError = + tweet.getElementsByClassName("tweet-reply-error")[0]; + const tweetReplyText = + tweet.getElementsByClassName("tweet-reply-text")[0]; + const tweetReplyChar = + tweet.getElementsByClassName("tweet-reply-char")[0]; + const tweetReplyMedia = + tweet.getElementsByClassName("tweet-reply-media")[0]; - const tweetInteract = tweet.getElementsByClassName("tweet-interact")[0]; - const tweetInteractReply = tweet.getElementsByClassName( - "tweet-interact-reply" - )[0]; - const tweetInteractRetweet = tweet.getElementsByClassName( - "tweet-interact-retweet" - )[0]; - const tweetInteractFavorite = tweet.getElementsByClassName( - "tweet-interact-favorite" - )[0]; - const tweetInteractBookmark = tweet.getElementsByClassName( - "tweet-interact-bookmark" - )[0]; - const tweetInteractMore = tweet.getElementsByClassName( - "tweet-interact-more" - )[0]; + const tweetInteract = tweet.getElementsByClassName("tweet-interact")[0]; + const tweetInteractReply = tweet.getElementsByClassName( + "tweet-interact-reply" + )[0]; + const tweetInteractRetweet = tweet.getElementsByClassName( + "tweet-interact-retweet" + )[0]; + const tweetInteractFavorite = tweet.getElementsByClassName( + "tweet-interact-favorite" + )[0]; + const tweetInteractBookmark = tweet.getElementsByClassName( + "tweet-interact-bookmark" + )[0]; + const tweetInteractMore = tweet.getElementsByClassName( + "tweet-interact-more" + )[0]; - const tweetFooter = tweet.getElementsByClassName("tweet-footer")[0]; - const tweetFooterReplies = tweet.getElementsByClassName( - "tweet-footer-stat-replies" - )[0]; - const tweetFooterRetweets = tweet.getElementsByClassName( - "tweet-footer-stat-retweets" - )[0]; - const tweetFooterFavorites = tweet.getElementsByClassName( - "tweet-footer-stat-favorites" - )[0]; + const tweetFooter = tweet.getElementsByClassName("tweet-footer")[0]; + const tweetFooterReplies = tweet.getElementsByClassName( + "tweet-footer-stat-replies" + )[0]; + const tweetFooterRetweets = tweet.getElementsByClassName( + "tweet-footer-stat-retweets" + )[0]; + const tweetFooterFavorites = tweet.getElementsByClassName( + "tweet-footer-stat-favorites" + )[0]; - const tweetQuote = tweet.getElementsByClassName("tweet-quote")[0]; - const tweetQuoteCancel = - tweet.getElementsByClassName("tweet-quote-cancel")[0]; - const tweetQuoteUpload = - tweet.getElementsByClassName("tweet-quote-upload")[0]; - const tweetQuoteAddEmoji = tweet.getElementsByClassName( - "tweet-quote-add-emoji" - )[0]; - const tweetQuoteButton = - tweet.getElementsByClassName("tweet-quote-button")[0]; - const tweetQuoteError = - tweet.getElementsByClassName("tweet-quote-error")[0]; - const tweetQuoteText = tweet.getElementsByClassName("tweet-quote-text")[0]; - const tweetQuoteChar = tweet.getElementsByClassName("tweet-quote-char")[0]; - const tweetQuoteMedia = - tweet.getElementsByClassName("tweet-quote-media")[0]; + const tweetQuote = tweet.getElementsByClassName("tweet-quote")[0]; + const tweetQuoteCancel = + tweet.getElementsByClassName("tweet-quote-cancel")[0]; + const tweetQuoteUpload = + tweet.getElementsByClassName("tweet-quote-upload")[0]; + const tweetQuoteAddEmoji = tweet.getElementsByClassName( + "tweet-quote-add-emoji" + )[0]; + const tweetQuoteButton = + tweet.getElementsByClassName("tweet-quote-button")[0]; + const tweetQuoteError = + tweet.getElementsByClassName("tweet-quote-error")[0]; + const tweetQuoteText = + tweet.getElementsByClassName("tweet-quote-text")[0]; + const tweetQuoteChar = + tweet.getElementsByClassName("tweet-quote-char")[0]; + const tweetQuoteMedia = + tweet.getElementsByClassName("tweet-quote-media")[0]; - const tweetInteractRetweetMenu = tweet.getElementsByClassName( - "tweet-interact-retweet-menu" - )[0]; - const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-retweet" - )[0]; - const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-quote" - )[0]; - const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-quotes" - )[0]; - const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName( - "tweet-interact-retweet-menu-retweeters" - )[0]; + const tweetInteractRetweetMenu = tweet.getElementsByClassName( + "tweet-interact-retweet-menu" + )[0]; + const tweetInteractRetweetMenuRetweet = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-retweet" + )[0]; + const tweetInteractRetweetMenuQuote = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-quote" + )[0]; + const tweetInteractRetweetMenuQuotes = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-quotes" + )[0]; + const tweetInteractRetweetMenuRetweeters = tweet.getElementsByClassName( + "tweet-interact-retweet-menu-retweeters" + )[0]; - const tweetInteractMoreMenu = tweet.getElementsByClassName( - "tweet-interact-more-menu" - )[0]; - const tweetInteractMoreMenuCopy = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy" - )[0]; - const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy-tweet-id" - )[0]; - const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName( - "tweet-interact-more-menu-copy-user-id" - )[0]; - const tweetInteractMoreMenuLog = tweet.getElementsByClassName( - "tweet-interact-more-menu-log" - )[0]; - const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName( - "tweet-interact-more-menu-embed" - )[0]; - const tweetInteractMoreMenuShare = tweet.getElementsByClassName( - "tweet-interact-more-menu-share" - )[0]; - const tweetInteractMoreMenuShareDMs = tweet.getElementsByClassName( - "tweet-interact-more-menu-share-dms" - )[0]; - const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName( - "tweet-interact-more-menu-newtwitter" - )[0]; - const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName( - "tweet-interact-more-menu-analytics" - )[0]; - const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName( - "tweet-interact-more-menu-refresh" - )[0]; - const tweetInteractMoreMenuMute = tweet.getElementsByClassName( - "tweet-interact-more-menu-mute" - )[0]; - const tweetInteractMoreMenuDownload = tweet.getElementsByClassName( - "tweet-interact-more-menu-download" - )[0]; - const tweetInteractMoreMenuDownloadGifs = Array.from( - tweet.getElementsByClassName("tweet-interact-more-menu-download-gif") - ); - const tweetInteractMoreMenuDelete = tweet.getElementsByClassName( - "tweet-interact-more-menu-delete" - )[0]; - const tweetInteractMoreMenuFollow = tweet.getElementsByClassName( - "tweet-interact-more-menu-follow" - )[0]; - const tweetInteractMoreMenuBlock = tweet.getElementsByClassName( - "tweet-interact-more-menu-block" - )[0]; - const tweetInteractMoreMenuMuteUser = tweet.getElementsByClassName( - "tweet-interact-more-menu-mute-user" - )[0]; - const tweetInteractMoreMenuListsAction = tweet.getElementsByClassName( - "tweet-interact-more-menu-lists-action" - )[0]; - const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName( - "tweet-interact-more-menu-bookmark" - )[0]; - const tweetInteractMoreMenuHide = tweet.getElementsByClassName( - "tweet-interact-more-menu-hide" - )[0]; - const tweetInteractMoreMenuSeparate = tweet.getElementsByClassName( - "tweet-interact-more-menu-separate" - )[0]; + const tweetInteractMoreMenu = tweet.getElementsByClassName( + "tweet-interact-more-menu" + )[0]; + const tweetInteractMoreMenuCopy = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy" + )[0]; + const tweetInteractMoreMenuCopyTweetId = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy-tweet-id" + )[0]; + const tweetInteractMoreMenuCopyUserId = tweet.getElementsByClassName( + "tweet-interact-more-menu-copy-user-id" + )[0]; + const tweetInteractMoreMenuLog = tweet.getElementsByClassName( + "tweet-interact-more-menu-log" + )[0]; + const tweetInteractMoreMenuEmbed = tweet.getElementsByClassName( + "tweet-interact-more-menu-embed" + )[0]; + const tweetInteractMoreMenuShare = tweet.getElementsByClassName( + "tweet-interact-more-menu-share" + )[0]; + const tweetInteractMoreMenuShareDMs = tweet.getElementsByClassName( + "tweet-interact-more-menu-share-dms" + )[0]; + const tweetInteractMoreMenuNewtwitter = tweet.getElementsByClassName( + "tweet-interact-more-menu-newtwitter" + )[0]; + const tweetInteractMoreMenuAnalytics = tweet.getElementsByClassName( + "tweet-interact-more-menu-analytics" + )[0]; + const tweetInteractMoreMenuRefresh = tweet.getElementsByClassName( + "tweet-interact-more-menu-refresh" + )[0]; + const tweetInteractMoreMenuMute = tweet.getElementsByClassName( + "tweet-interact-more-menu-mute" + )[0]; + const tweetInteractMoreMenuDownload = tweet.getElementsByClassName( + "tweet-interact-more-menu-download" + )[0]; + const tweetInteractMoreMenuDownloadGifs = Array.from( + tweet.getElementsByClassName( + "tweet-interact-more-menu-download-gif" + ) + ); + const tweetInteractMoreMenuDelete = tweet.getElementsByClassName( + "tweet-interact-more-menu-delete" + )[0]; + const tweetInteractMoreMenuFollow = tweet.getElementsByClassName( + "tweet-interact-more-menu-follow" + )[0]; + const tweetInteractMoreMenuBlock = tweet.getElementsByClassName( + "tweet-interact-more-menu-block" + )[0]; + const tweetInteractMoreMenuMuteUser = tweet.getElementsByClassName( + "tweet-interact-more-menu-mute-user" + )[0]; + const tweetInteractMoreMenuListsAction = tweet.getElementsByClassName( + "tweet-interact-more-menu-lists-action" + )[0]; + const tweetInteractMoreMenuBookmark = tweet.getElementsByClassName( + "tweet-interact-more-menu-bookmark" + )[0]; + const tweetInteractMoreMenuHide = tweet.getElementsByClassName( + "tweet-interact-more-menu-hide" + )[0]; + const tweetInteractMoreMenuSeparate = tweet.getElementsByClassName( + "tweet-interact-more-menu-separate" + )[0]; - if (tweetInteractMoreMenuLog) - tweetInteractMoreMenuLog.addEventListener("click", () => { - console.log(t); - }); + if (tweetInteractMoreMenuLog) + tweetInteractMoreMenuLog.addEventListener("click", () => { + console.log(t); + }); - if (tweetInteractMoreMenuSeparate) - tweetInteractMoreMenuSeparate.addEventListener("click", () => { - tweetBodyText.style = ` + if (tweetInteractMoreMenuSeparate) + tweetInteractMoreMenuSeparate.addEventListener("click", () => { + tweetBodyText.style = ` padding-top: 20px!important; display: block; font-size: 26px; line-height: unset; padding-bottom: 20px; `; - tweetInteractMoreMenuSeparate.style.display = "none"; - }); + tweetInteractMoreMenuSeparate.style.display = "none"; + }); - // Lists - if (tweetInteractMoreMenuListsAction) - tweetInteractMoreMenuListsAction.addEventListener("click", async () => { - createModal(` + // Lists + if (tweetInteractMoreMenuListsAction) + tweetInteractMoreMenuListsAction.addEventListener( + "click", + async () => { + createModal(`

${LOC.from_list.message}

`); - let lists = await API.list.getOwnerships(user.id_str, t.user.id_str); - let container = document.getElementById("modal-lists"); - for (let i in lists) { - let l = lists[i]; - let listElement = document.createElement("div"); - listElement.classList.add("list-item"); - listElement.innerHTML = html` - -
- -
- `; - container.appendChild(listElement); - listElement - .getElementsByClassName("nice-button")[0] - .addEventListener("click", async () => { - if (l.is_member) { - await API.list.removeMember(l.id_str, t.user.id_str); - l.is_member = false; - listElement.getElementsByClassName("nice-button")[0].innerText = - LOC.add.message; - } else { - await API.list.addMember(l.id_str, t.user.id_str); - l.is_member = true; - listElement.getElementsByClassName("nice-button")[0].innerText = - LOC.remove.message; - } - l.is_member = !l.is_member; + let lists = await API.list.getOwnerships( + user.id_str, + t.user.id_str + ); + let container = document.getElementById("modal-lists"); + for (let i in lists) { + let l = lists[i]; + let listElement = document.createElement("div"); + listElement.classList.add("list-item"); + listElement.innerHTML = html` + +
+ +
+ `; + container.appendChild(listElement); + listElement + .getElementsByClassName("nice-button")[0] + .addEventListener("click", async () => { + if (l.is_member) { + await API.list.removeMember( + l.id_str, + t.user.id_str + ); + l.is_member = false; + listElement.getElementsByClassName( + "nice-button" + )[0].innerText = LOC.add.message; + } else { + await API.list.addMember( + l.id_str, + t.user.id_str + ); + l.is_member = true; + listElement.getElementsByClassName( + "nice-button" + )[0].innerText = LOC.remove.message; + } + l.is_member = !l.is_member; + }); + } + } + ); + + // moderating tweets + if (tweetInteractMoreMenuHide) + tweetInteractMoreMenuHide.addEventListener("click", async () => { + if (t.moderated) { + try { + await API.tweet.unmoderate(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetInteractMoreMenuHide.innerText = + LOC.hide_tweet.message; + t.moderated = false; + } else { + let sure = confirm(LOC.hide_tweet_sure.message); + if (!sure) return; + try { + await API.tweet.moderate(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + tweetInteractMoreMenuHide.innerText = + LOC.unhide_tweet.message; + t.moderated = true; + } }); - } - }); - // moderating tweets - if (tweetInteractMoreMenuHide) - tweetInteractMoreMenuHide.addEventListener("click", async () => { - if (t.moderated) { - try { - await API.tweet.unmoderate(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetInteractMoreMenuHide.innerText = LOC.hide_tweet.message; - t.moderated = false; - } else { - let sure = confirm(LOC.hide_tweet_sure.message); - if (!sure) return; - try { - await API.tweet.moderate(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - tweetInteractMoreMenuHide.innerText = LOC.unhide_tweet.message; - t.moderated = true; - } - }); + // community notes + if ( + t.birdwatch && + t.birdwatch.subtitle && + options.mainTweet && + !vars.hideCommunityNotes + ) { + let div = document.createElement("div"); + div.classList.add("tweet-birdwatch", "box"); + let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); + for ( + let e = t.birdwatch.subtitle.entities.length - 1; + e >= 0; + e-- + ) { + let entity = t.birdwatch.subtitle.entities[e]; + if (!entity.ref) continue; + text = arrayInsert(text, entity.toIndex, ""); + text = arrayInsert( + text, + entity.fromIndex, + `` + ); + } + text = text.join(""); - // community notes - if ( - t.birdwatch && - t.birdwatch.subtitle && - options.mainTweet && - !vars.hideCommunityNotes - ) { - let div = document.createElement("div"); - div.classList.add("tweet-birdwatch", "box"); - let text = Array.from(escapeHTML(t.birdwatch.subtitle.text)); - for (let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) { - let entity = t.birdwatch.subtitle.entities[e]; - if (!entity.ref) continue; - text = arrayInsert(text, entity.toIndex, ""); - text = arrayInsert( - text, - entity.fromIndex, - `` - ); - } - text = text.join(""); - - div.innerHTML = html` -
- ${escapeHTML(t.birdwatch.title)} -
-
- ${text} -
- `; - - if (tweetFooter) tweetFooter.before(div); - else tweetInteract.before(div); - } + div.innerHTML = html` +
+ ${escapeHTML(t.birdwatch.title)} +
+
+ ${text} +
+ `; - // rtl languages - if (rtlLanguages.includes(t.lang)) { - tweetBody.classList.add("rtl"); - } + if (tweetFooter) tweetFooter.before(div); + else tweetInteract.before(div); + } - // Quote body - if (tweetBodyQuote) { - tweetBodyQuote.addEventListener("click", (e) => { - e.preventDefault(); - history.pushState( - {}, - null, - `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (this.subpage === "tweet") { - this.updateReplies(id); - } else if (this.subpage === "likes") { - this.updateLikes(id); - } else if (this.subpage === "retweets") { - this.updateRetweets(id); - } else if (this.subpage === "retweets_with_comments") { - this.updateRetweetsWithComments(id); + // rtl languages + if (rtlLanguages.includes(t.lang)) { + tweetBody.classList.add("rtl"); } - this.currentLocation = location.pathname; - }); - if (rtlLanguages.includes(t.quoted_status.lang)) { - tweetBodyQuoteText.classList.add("rtl"); - } else { - tweetBodyQuoteText.classList.add("ltr"); - } - if (tweetQuoteTranslate) { - let quoteTranslating = false; - tweetQuoteTranslate.addEventListener("click", async (e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - if (t.quoted_status.translated || quoteTranslating) return; - quoteTranslating = true; - let translated = await API.tweet.translate(t.quoted_status.id_str); - quoteTranslating = false; - t.quoted_status.translated = true; - tweetQuoteTranslate.hidden = true; - if (!translated.translated_lang || !translated.text) return; - let tt = t.full_text - .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") - .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") - .trim(); - if (translated.text.trim() === tt) return; - if (translated.text.trim() === tt.replace(/(hihi)|(hehe)/g, "lol")) - return; // lol - const { hideOriginalLanguages } = await chrome.storage.sync.get( - "hideOriginalLanguages" - ); + // Quote body + if (tweetBodyQuote) { + tweetBodyQuote.addEventListener("click", (e) => { + e.preventDefault(); + history.pushState( + {}, + null, + `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (this.subpage === "tweet") { + this.updateReplies(id); + } else if (this.subpage === "likes") { + this.updateLikes(id); + } else if (this.subpage === "retweets") { + this.updateRetweets(id); + } else if (this.subpage === "retweets_with_comments") { + this.updateRetweetsWithComments(id); + } + this.currentLocation = location.pathname; + }); + if (rtlLanguages.includes(t.quoted_status.lang)) { + tweetBodyQuoteText.classList.add("rtl"); + } else { + tweetBodyQuoteText.classList.add("ltr"); + } + if (tweetQuoteTranslate) { + let quoteTranslating = false; + tweetQuoteTranslate.addEventListener("click", async (e) => { + e.preventDefault(); + e.stopImmediatePropagation(); - let translatedMessage; - if (LOC.translated_from.message.includes("$LANGUAGE$")) { - translatedMessage = LOC.translated_from.message.replace( - "$LANGUAGE$", - `[${translated.translated_lang}]` - ); - } else { - translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; - } - if (translated.text.length > 600) { - translated.text = translated.text.substring(0, 600) + "..."; - } - if (hideOriginalLanguages) { - translatedMessage = ""; - tweetBodyQuoteText.innerHTML = ""; - } - tweetBodyQuoteText.innerHTML += - `${translatedMessage}:` + - `${escapeHTML( - translated.text - )}`; - if (vars.enableTwemoji) twemoji.parse(tweetBodyQuoteText); - }); - if ( - options.translate || - vars.autotranslateProfiles.includes(t.quoted_status.user.id_str) || - (typeof toAutotranslate !== "undefined" && toAutotranslate) || - (vars.autotranslateLanguages.includes(t.quoted_status.lang) && - vars.autotranslationMode === "whitelist") || - (!vars.autotranslateLanguages.includes(t.quoted_status.lang) && - vars.autotranslationMode === "blacklist") - ) { - onVisible(tweet, () => { - if (!t.quoted_status.translated) { - if (tweetQuoteTranslate) tweetQuoteTranslate.click(); + if (t.quoted_status.translated || quoteTranslating) return; + quoteTranslating = true; + let translated = await API.tweet.translate( + t.quoted_status.id_str + ); + quoteTranslating = false; + t.quoted_status.translated = true; + tweetQuoteTranslate.hidden = true; + if (!translated.translated_lang || !translated.text) return; + let tt = t.full_text + .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") + .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") + .trim(); + if (translated.text.trim() === tt) return; + if ( + translated.text.trim() === + tt.replace(/(hihi)|(hehe)/g, "lol") + ) + return; // lol + const { hideOriginalLanguages } = + await chrome.storage.sync.get("hideOriginalLanguages"); + + let translatedMessage; + if (LOC.translated_from.message.includes("$LANGUAGE$")) { + translatedMessage = LOC.translated_from.message.replace( + "$LANGUAGE$", + `[${translated.translated_lang}]` + ); + } else { + translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; + } + if (translated.text.length > 600) { + translated.text = + translated.text.substring(0, 600) + "..."; + } + if (hideOriginalLanguages) { + translatedMessage = ""; + tweetBodyQuoteText.innerHTML = ""; + } + tweetBodyQuoteText.innerHTML += + `${translatedMessage}:` + + `${escapeHTML( + translated.text + )}`; + if (vars.enableTwemoji) twemoji.parse(tweetBodyQuoteText); + }); + if ( + options.translate || + vars.autotranslateProfiles.includes( + t.quoted_status.user.id_str + ) || + (typeof toAutotranslate !== "undefined" && + toAutotranslate) || + (vars.autotranslateLanguages.includes( + t.quoted_status.lang + ) && + vars.autotranslationMode === "whitelist") || + (!vars.autotranslateLanguages.includes( + t.quoted_status.lang + ) && + vars.autotranslationMode === "blacklist") + ) { + onVisible(tweet, () => { + if (!t.quoted_status.translated) { + if (tweetQuoteTranslate) + tweetQuoteTranslate.click(); + } + }); + } } - }); } - } - } - // Translate - t.translated = false; - let translating = false; - if (tweetTranslate) { - tweetTranslate.addEventListener("click", async () => { - if (t.translated || translating) return; - translating = true; - let translated = await API.tweet.translate(t.id_str); - t.translated = true; - translating = false; - tweetTranslate.hidden = true; - if (!translated.translated_lang || !translated.text) return; - let tt = t.full_text - .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") - .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") - .trim(); - if (translated.text.trim() === tt) return; - if (translated.text.trim() === tt.replace(/(hihi)|(hehe)/g, "lol")) - return; // lol + // Translate + t.translated = false; + let translating = false; + if (tweetTranslate) { + tweetTranslate.addEventListener("click", async () => { + if (t.translated || translating) return; + translating = true; + let translated = await API.tweet.translate(t.id_str); + t.translated = true; + translating = false; + tweetTranslate.hidden = true; + if (!translated.translated_lang || !translated.text) return; + let tt = t.full_text + .replace(/^(@[a-zA-Z0-9_]{1,15}\s?)*/, "") + .replace(/\shttps:\/\/t.co\/[a-zA-Z0-9\-]{8,10}$/, "") + .trim(); + if (translated.text.trim() === tt) return; + if ( + translated.text.trim() === + tt.replace(/(hihi)|(hehe)/g, "lol") + ) + return; // lol - const { hideOriginalLanguages } = await chrome.storage.sync.get( - "hideOriginalLanguages" - ); + const { hideOriginalLanguages } = await chrome.storage.sync.get( + "hideOriginalLanguages" + ); - let translatedMessage; - if (LOC.translated_from.message.includes("$LANGUAGE$")) { - translatedMessage = LOC.translated_from.message.replace( - "$LANGUAGE$", - `[${translated.translated_lang}]` - ); - } else { - translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; + let translatedMessage; + if (LOC.translated_from.message.includes("$LANGUAGE$")) { + translatedMessage = LOC.translated_from.message.replace( + "$LANGUAGE$", + `[${translated.translated_lang}]` + ); + } else { + translatedMessage = `${LOC.translated_from.message} [${translated.translated_lang}]`; + } + let translatedT = { + full_text: translated.text, + entities: translated.entities, + }; + let translatedFrom = document.createElement("span"); + translatedFrom.classList.add("translated-from"); + translatedFrom.innerText = translatedMessage; + + let translatedText = document.createElement("span"); + translatedText.classList.add("tweet-translated-text"); + translatedText.innerHTML = await renderTweetBodyHTML( + translatedT + ); + if (hideOriginalLanguages) { + tweetBodyText.innerHTML = ""; + tweetBodyText.append(translatedText); + } else { + tweetBodyText.append( + document.createElement("br"), + translatedFrom, + translatedText + ); + } + if (vars.enableTwemoji) twemoji.parse(tweetBodyText); + }); + if ( + options.translate || + vars.autotranslateProfiles.includes(t.user.id_str) || + (typeof toAutotranslate !== "undefined" && toAutotranslate) || + (vars.autotranslateLanguages.includes(t.lang) && + vars.autotranslationMode === "whitelist") || + (!vars.autotranslateLanguages.includes(t.lang) && + vars.autotranslationMode === "blacklist") + ) { + onVisible(tweet, () => { + if (!t.translated) { + if (tweetTranslate) tweetTranslate.click(); + } + }); + } } - let translatedT = { - full_text: translated.text, - entities: translated.entities, + + // Bookmarks + let switchingBookmark = false; + let switchBookmark = () => { + if (switchingBookmark) return; + switchingBookmark = true; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + if (t.bookmarked) { + API.bookmarks + .delete(t.id_str) + .then(() => { + toast.info(LOC.unbookmarked_tweet.message); + switchingBookmark = false; + t.bookmarked = false; + t.bookmark_count--; + tweetInteractMoreMenuBookmark.innerText = + LOC.bookmark_tweet.message; + if (tweetInteractBookmark) { + tweetInteractBookmark.classList.remove( + "tweet-interact-bookmarked" + ); + tweetInteractBookmark.innerText = formatLargeNumber( + t.bookmark_count + ).replace(/\s/g, ","); + tweetInteractBookmark.dataset.val = + t.bookmark_count; + } + }) + .catch((e) => { + switchingBookmark = false; + console.error(e); + alert(e); + }); + } else { + API.bookmarks + .create(t.id_str) + .then(() => { + toast.info(LOC.bookmarked_tweet.message); + switchingBookmark = false; + t.bookmarked = true; + t.bookmark_count++; + tweetInteractMoreMenuBookmark.innerText = + LOC.remove_bookmark.message; + if (tweetInteractBookmark) { + tweetInteractBookmark.classList.add( + "tweet-interact-bookmarked" + ); + tweetInteractBookmark.innerText = formatLargeNumber( + t.bookmark_count + ).replace(/\s/g, ","); + tweetInteractBookmark.dataset.val = + t.bookmark_count; + } + }) + .catch((e) => { + switchingBookmark = false; + console.error(e); + alert(e); + }); + } }; - let translatedFrom = document.createElement("span"); - translatedFrom.classList.add("translated-from"); - translatedFrom.innerText = translatedMessage; + if (tweetInteractBookmark) + tweetInteractBookmark.addEventListener("click", switchBookmark); + if (tweetInteractMoreMenuBookmark) + tweetInteractMoreMenuBookmark.addEventListener( + "click", + switchBookmark + ); - let translatedText = document.createElement("span"); - translatedText.classList.add("tweet-translated-text"); - translatedText.innerHTML = await renderTweetBodyHTML(translatedT); - if (hideOriginalLanguages) { - tweetBodyText.innerHTML = ""; - tweetBodyText.append(translatedText); - } else { - tweetBodyText.append( - document.createElement("br"), - translatedFrom, - translatedText - ); + // Media + if (t.extended_entities && t.extended_entities.media) { + const tweetMedia = tweet.getElementsByClassName("tweet-media")[0]; + tweetMedia.addEventListener("click", (e) => { + if ( + e.target.className && + e.target.className.includes("tweet-media-element-censor") + ) { + return e.target.classList.remove( + "tweet-media-element-censor" + ); + } + if (e.target.tagName === "IMG") { + if ( + !e.target.src.includes("?name=") && + !e.target.src.endsWith(":orig") && + !e.target.src.startsWith("data:") + ) { + e.target.src += "?name=orig"; + } else if (e.target.src.includes("?name=small")) { + e.target.src = e.target.src.replace( + "?name=small", + "?name=large" + ); + } + new Viewer(tweetMedia, { + transition: false, + zoomRatio: 0.3, + }); + e.target.click(); + } + }); } - if (vars.enableTwemoji) twemoji.parse(tweetBodyText); - }); - if ( - options.translate || - vars.autotranslateProfiles.includes(t.user.id_str) || - (typeof toAutotranslate !== "undefined" && toAutotranslate) || - (vars.autotranslateLanguages.includes(t.lang) && - vars.autotranslationMode === "whitelist") || - (!vars.autotranslateLanguages.includes(t.lang) && - vars.autotranslationMode === "blacklist") - ) { - onVisible(tweet, () => { - if (!t.translated) { - if (tweetTranslate) tweetTranslate.click(); - } + + // Emojis + [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach((e) => { + e.addEventListener("click", (e) => { + let isReply = e.target.className === "tweet-reply-add-emoji"; + createEmojiPicker( + isReply ? tweetReply : tweetQuote, + isReply ? tweetReplyText : tweetQuoteText, + {} + ); + }); }); - } - } - // Bookmarks - let switchingBookmark = false; - let switchBookmark = () => { - if (switchingBookmark) return; - switchingBookmark = true; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - if (t.bookmarked) { - API.bookmarks - .delete(t.id_str) - .then(() => { - toast.info(LOC.unbookmarked_tweet.message); - switchingBookmark = false; - t.bookmarked = false; - t.bookmark_count--; - tweetInteractMoreMenuBookmark.innerText = - LOC.bookmark_tweet.message; - if (tweetInteractBookmark) { - tweetInteractBookmark.classList.remove( - "tweet-interact-bookmarked" - ); - tweetInteractBookmark.innerText = formatLargeNumber( - t.bookmark_count - ).replace(/\s/g, ","); - tweetInteractBookmark.dataset.val = t.bookmark_count; + // Reply + tweetReplyCancel.addEventListener("click", () => { + tweetReply.hidden = true; + tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); + }); + let replyMedia = []; + tweetReply.addEventListener("drop", (e) => { + handleDrop(e, replyMedia, tweetReplyMedia); + }); + tweetReply.addEventListener("paste", (event) => { + let items = ( + event.clipboardData || event.originalEvent.clipboardData + ).items; + for (let index in items) { + let item = items[index]; + if (item.kind === "file") { + let file = item.getAsFile(); + handleFiles([file], replyMedia, tweetReplyMedia); + } } - }) - .catch((e) => { - switchingBookmark = false; - console.error(e); - alert(e); - }); - } else { - API.bookmarks - .create(t.id_str) - .then(() => { - toast.info(LOC.bookmarked_tweet.message); - switchingBookmark = false; - t.bookmarked = true; - t.bookmark_count++; - tweetInteractMoreMenuBookmark.innerText = - LOC.remove_bookmark.message; - if (tweetInteractBookmark) { - tweetInteractBookmark.classList.add("tweet-interact-bookmarked"); - tweetInteractBookmark.innerText = formatLargeNumber( - t.bookmark_count - ).replace(/\s/g, ","); - tweetInteractBookmark.dataset.val = t.bookmark_count; + }); + tweetReplyUpload.addEventListener("click", () => { + getMedia(replyMedia, tweetReplyMedia); + }); + tweetInteractReply.addEventListener("click", () => { + if (options.mainTweet) { + document.getElementsByClassName("new-tweet-view")[0].click(); + document.getElementsByClassName("new-tweet-text")[0].focus(); + return; } - }) - .catch((e) => { - switchingBookmark = false; - console.error(e); - alert(e); - }); - } - }; - if (tweetInteractBookmark) - tweetInteractBookmark.addEventListener("click", switchBookmark); - if (tweetInteractMoreMenuBookmark) - tweetInteractMoreMenuBookmark.addEventListener("click", switchBookmark); - - // Media - if (t.extended_entities && t.extended_entities.media) { - const tweetMedia = tweet.getElementsByClassName("tweet-media")[0]; - tweetMedia.addEventListener("click", (e) => { - if ( - e.target.className && - e.target.className.includes("tweet-media-element-censor") - ) { - return e.target.classList.remove("tweet-media-element-censor"); - } - if (e.target.tagName === "IMG") { - if ( - !e.target.src.includes("?name=") && - !e.target.src.endsWith(":orig") && - !e.target.src.startsWith("data:") - ) { - e.target.src += "?name=orig"; - } else if (e.target.src.includes("?name=small")) { - e.target.src = e.target.src.replace("?name=small", "?name=large"); - } - new Viewer(tweetMedia, { - transition: false, - zoomRatio: 0.3, - }); - e.target.click(); - } - }); - } + if (!tweetQuote.hidden) tweetQuote.hidden = true; + if (tweetReply.hidden) { + tweetInteractReply.classList.add( + "tweet-interact-reply-clicked" + ); + } else { + tweetInteractReply.classList.remove( + "tweet-interact-reply-clicked" + ); + } + tweetReply.hidden = !tweetReply.hidden; + setTimeout(() => { + tweetReplyText.focus(); + }); + }); + tweetReplyText.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.ctrlKey) { + tweetReplyButton.click(); + } + }); + tweetReplyText.addEventListener("input", (e) => { + let tweet = twttr.txt.parseTweet(e.target.value); + if (localStorage.OTisBlueVerified) { + return (tweetReplyChar.innerText = `${tweet.weightedLength}/25000`); + } + tweetReplyChar.innerText = `${tweet.weightedLength}/280`; + if (tweet.weightedLength > 265) { + tweetReplyChar.style.color = "#c26363"; + } else { + tweetReplyChar.style.color = ""; + } + if (tweet.weightedLength > 280) { + tweetReplyChar.style.color = "red"; + tweetReplyButton.disabled = true; + } else { + tweetReplyButton.disabled = false; + } + }); + tweetReplyButton.addEventListener("click", async () => { + tweetReplyError.innerHTML = ""; + let text = tweetReplyText.value; + if (text.length === 0 && replyMedia.length === 0) return; + tweetReplyButton.disabled = true; + let uploadedMedia = []; + for (let i in replyMedia) { + let media = replyMedia[i]; + try { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = false; + let mediaId; + if (!media.div.dataset.mediaId) { + mediaId = await API.uploadMedia({ + media_type: media.type, + media_category: media.category, + media: media.data, + alt: media.alt, + cw: media.cw, + loadCallback: (data) => { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = `${data.text} (${data.progress}%)`; + }, + }); + } else { + mediaId = media.div.dataset.mediaId; + } + uploadedMedia.push(mediaId); + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = LOC.uploaded.message; + media.div.dataset.mediaId = mediaId; + } catch (e) { + console.error(e); + alert(e); + for (let j in replyMedia) { + let media = replyMedia[j]; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = true; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = ""; + } + tweetReplyButton.disabled = false; + return; // cancel tweeting + } + } + let tweetObject = { + status: text, + in_reply_to_status_id: t.id_str, + }; + if (uploadedMedia.length > 0) { + tweetObject.media_ids = uploadedMedia.join(","); + } + let tweetData; + try { + tweetData = await API.tweet.postV2(tweetObject); + } catch (e) { + tweetReplyError.innerHTML = + (e && e.message ? e.message : e) + "
"; + tweetReplyButton.disabled = false; + return; + } + if (!tweetData) { + tweetReplyButton.disabled = false; + tweetReplyError.innerHTML = html`${LOC.error_sending_tweet + .message}
`; + return; + } + tweetReplyText.value = ""; + tweetReplyChar.innerText = localStorage.OTisBlueVerified + ? "0/25000" + : "0/280"; + tweetReply.hidden = true; + tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); + if (!options.mainTweet) { + tweetInteractReply.dataset.val = + parseInt(tweetInteractReply.dataset.val) + 1; + if (vars.showExactValues || t.reply_count < 10000) + tweetInteractReply.innerText = formatLargeNumber( + parseInt( + tweetInteractReply.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + tweetFooterReplies.dataset.val = + parseInt(tweetFooterReplies.dataset.val) + 1; + if (vars.showExactValues || t.reply_count < 10000) + tweetFooterReplies.innerText = formatLargeNumber( + parseInt( + tweetFooterReplies.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + if (typeof repliesToIgnore !== "undefined") { + repliesToIgnore.push(tweetData.id_str); + } + tweetData._ARTIFICIAL = true; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + if (tweet.getElementsByClassName("tweet-self-thread-div")[0]) + tweet.getElementsByClassName( + "tweet-self-thread-div" + )[0].hidden = false; + tweetReplyButton.disabled = false; + tweetReplyMedia.innerHTML = []; + replyMedia = []; + this.appendTweet( + tweetData, + document.getElementsByClassName("timeline")[0], + { + noTop: true, + after: tweet, + } + ); + }); - // Emojis - [tweetReplyAddEmoji, tweetQuoteAddEmoji].forEach((e) => { - e.addEventListener("click", (e) => { - let isReply = e.target.className === "tweet-reply-add-emoji"; - createEmojiPicker( - isReply ? tweetReply : tweetQuote, - isReply ? tweetReplyText : tweetQuoteText, - {} - ); - }); - }); + // Retweet / Quote Tweet + let retweetClicked = false; + tweetQuoteCancel.addEventListener("click", () => { + tweetQuote.hidden = true; + }); + tweetInteractRetweet.addEventListener("click", async () => { + if ( + tweetInteractRetweet.classList.contains( + "tweet-interact-retweet-disabled" + ) + ) { + return; + } + if (!tweetQuote.hidden) { + tweetQuote.hidden = true; + return; + } + if (tweetInteractRetweetMenu.hidden) { + tweetInteractRetweetMenu.hidden = false; + } + if (retweetClicked) return; + retweetClicked = true; + setTimeout(() => { + document.body.addEventListener( + "click", + () => { + retweetClicked = false; + setTimeout( + () => (tweetInteractRetweetMenu.hidden = true), + 50 + ); + }, + { once: true } + ); + }, 50); + }); - // Reply - tweetReplyCancel.addEventListener("click", () => { - tweetReply.hidden = true; - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - }); - let replyMedia = []; - tweetReply.addEventListener("drop", (e) => { - handleDrop(e, replyMedia, tweetReplyMedia); - }); - tweetReply.addEventListener("paste", (event) => { - let items = (event.clipboardData || event.originalEvent.clipboardData) - .items; - for (let index in items) { - let item = items[index]; - if (item.kind === "file") { - let file = item.getAsFile(); - handleFiles([file], replyMedia, tweetReplyMedia); + tweetInteractRetweetMenuRetweet.addEventListener("click", async () => { + if (!t.retweeted) { + let tweetData; + try { + tweetData = await API.tweet.retweet(t.id_str); + } catch (e) { + console.error(e); + alert(e); + return; + } + if (!tweetData) { + return; + } + tweetInteractRetweetMenuRetweet.innerText = + LOC.unretweet.message; + tweetInteractRetweet.classList.add("tweet-interact-retweeted"); + t.retweeted = true; + t.retweet_count++; + t.newTweetId = tweetData.id_str; + if (!options.mainTweet) { + tweetInteractRetweet.dataset.val = + parseInt(tweetInteractRetweet.dataset.val) + 1; + if (vars.showExactValues || t.retweet_count < 10000) + tweetInteractRetweet.innerText = formatLargeNumber( + parseInt( + tweetInteractRetweet.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + if (vars.showExactValues || t.retweet_count < 10000) + tweetFooterRetweets.innerText = formatLargeNumber( + parseInt( + tweetFooterRetweets.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + let event = new CustomEvent("tweetAction", { + detail: { + action: "retweet", + tweet: t, + tweetData, + }, + }); + document.dispatchEvent(event); + } else { + let tweetData; + try { + tweetData = await API.tweet.unretweet( + t.retweeted_status + ? t.retweeted_status.id_str + : t.id_str + ); + } catch (e) { + console.error(e); + return; + } + if (!tweetData) { + return; + } + tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; + tweetInteractRetweet.classList.remove( + "tweet-interact-retweeted" + ); + t.retweet_count--; + t.retweeted = false; + if (!options.mainTweet) { + tweetInteractRetweet.dataset.val = + parseInt(tweetInteractRetweet.dataset.val) - 1; + if (vars.showExactValues || t.retweet_count < 10000) + tweetInteractRetweet.innerText = formatLargeNumber( + parseInt( + tweetInteractRetweet.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } else { + if (vars.showExactValues || t.retweet_count < 10000) + tweetFooterRetweets.innerText = formatLargeNumber( + parseInt( + tweetFooterRetweets.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } + delete t.newTweetId; + let event = new CustomEvent("tweetAction", { + detail: { + action: "unretweet", + tweet: t, + tweetData, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + if (options.mainTweet) { + tweetInteractRetweetMenuQuotes.addEventListener( + "click", + async () => { + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (this.subpage === "tweet") { + this.updateReplies(id); + } else if (this.subpage === "likes") { + this.updateLikes(id); + } else if (this.subpage === "retweets") { + this.updateRetweets(id); + } else if (this.subpage === "retweets_with_comments") { + this.updateRetweetsWithComments(id); + } + this.currentLocation = location.pathname; + } + ); + tweetInteractRetweetMenuRetweeters.addEventListener( + "click", + async () => { + history.pushState( + {}, + null, + `/${t.user.screen_name}/status/${t.id_str}/retweets` + ); + this.updateSubpage(); + this.mediaToUpload = []; + this.excludeUserMentions = []; + this.linkColors = {}; + this.cursor = undefined; + this.seenReplies = []; + this.mainTweetLikers = []; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (this.subpage === "tweet") { + this.updateReplies(id); + } else if (this.subpage === "likes") { + this.updateLikes(id); + } else if (this.subpage === "retweets") { + this.updateRetweets(id); + } else if (this.subpage === "retweets_with_comments") { + this.updateRetweetsWithComments(id); + } + this.currentLocation = location.pathname; + } + ); } - } - }); - tweetReplyUpload.addEventListener("click", () => { - getMedia(replyMedia, tweetReplyMedia); - }); - tweetInteractReply.addEventListener("click", () => { - if (options.mainTweet) { - document.getElementsByClassName("new-tweet-view")[0].click(); - document.getElementsByClassName("new-tweet-text")[0].focus(); - return; - } - if (!tweetQuote.hidden) tweetQuote.hidden = true; - if (tweetReply.hidden) { - tweetInteractReply.classList.add("tweet-interact-reply-clicked"); - } else { - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - } - tweetReply.hidden = !tweetReply.hidden; - setTimeout(() => { - tweetReplyText.focus(); - }); - }); - tweetReplyText.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { - tweetReplyButton.click(); - } - }); - tweetReplyText.addEventListener("input", (e) => { - let tweet = twttr.txt.parseTweet(e.target.value); - if (localStorage.OTisBlueVerified) { - return (tweetReplyChar.innerText = `${tweet.weightedLength}/25000`); - } - tweetReplyChar.innerText = `${tweet.weightedLength}/280`; - if (tweet.weightedLength > 265) { - tweetReplyChar.style.color = "#c26363"; - } else { - tweetReplyChar.style.color = ""; - } - if (tweet.weightedLength > 280) { - tweetReplyChar.style.color = "red"; - tweetReplyButton.disabled = true; - } else { - tweetReplyButton.disabled = false; - } - }); - tweetReplyButton.addEventListener("click", async () => { - tweetReplyError.innerHTML = ""; - let text = tweetReplyText.value; - if (text.length === 0 && replyMedia.length === 0) return; - tweetReplyButton.disabled = true; - let uploadedMedia = []; - for (let i in replyMedia) { - let media = replyMedia[i]; - try { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = false; - let mediaId; - if (!media.div.dataset.mediaId) { - mediaId = await API.uploadMedia({ - media_type: media.type, - media_category: media.category, - media: media.data, - alt: media.alt, - cw: media.cw, - loadCallback: (data) => { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = `${data.text} (${data.progress}%)`; - }, + tweetInteractRetweetMenuQuote.addEventListener("click", async () => { + if (!tweetReply.hidden) { + tweetInteractReply.classList.remove( + "tweet-interact-reply-clicked" + ); + tweetReply.hidden = true; + } + tweetQuote.hidden = false; + setTimeout(() => { + tweetQuoteText.focus(); }); - } else { - mediaId = media.div.dataset.mediaId; - } - uploadedMedia.push(mediaId); - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = LOC.uploaded.message; - media.div.dataset.mediaId = mediaId; - } catch (e) { - console.error(e); - alert(e); - for (let j in replyMedia) { - let media = replyMedia[j]; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = true; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = ""; - } - tweetReplyButton.disabled = false; - return; // cancel tweeting - } - } - let tweetObject = { - status: text, - in_reply_to_status_id: t.id_str, - }; - if (uploadedMedia.length > 0) { - tweetObject.media_ids = uploadedMedia.join(","); - } - let tweetData; - try { - tweetData = await API.tweet.postV2(tweetObject); - } catch (e) { - tweetReplyError.innerHTML = (e && e.message ? e.message : e) + "
"; - tweetReplyButton.disabled = false; - return; - } - if (!tweetData) { - tweetReplyButton.disabled = false; - tweetReplyError.innerHTML = html`${LOC.error_sending_tweet - .message}
`; - return; - } - tweetReplyText.value = ""; - tweetReplyChar.innerText = localStorage.OTisBlueVerified - ? "0/25000" - : "0/280"; - tweetReply.hidden = true; - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - if (!options.mainTweet) { - tweetInteractReply.dataset.val = - parseInt(tweetInteractReply.dataset.val) + 1; - if (vars.showExactValues || t.reply_count < 10000) - tweetInteractReply.innerText = formatLargeNumber( - parseInt( - tweetInteractReply.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - tweetFooterReplies.dataset.val = - parseInt(tweetFooterReplies.dataset.val) + 1; - if (vars.showExactValues || t.reply_count < 10000) - tweetFooterReplies.innerText = formatLargeNumber( - parseInt( - tweetFooterReplies.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } - if (typeof repliesToIgnore !== "undefined") { - repliesToIgnore.push(tweetData.id_str); - } - tweetData._ARTIFICIAL = true; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - if (tweet.getElementsByClassName("tweet-self-thread-div")[0]) - tweet.getElementsByClassName("tweet-self-thread-div")[0].hidden = false; - tweetReplyButton.disabled = false; - tweetReplyMedia.innerHTML = []; - replyMedia = []; - this.appendTweet( - tweetData, - document.getElementsByClassName("timeline")[0], - { - noTop: true, - after: tweet, - } - ); - }); - - // Retweet / Quote Tweet - let retweetClicked = false; - tweetQuoteCancel.addEventListener("click", () => { - tweetQuote.hidden = true; - }); - tweetInteractRetweet.addEventListener("click", async () => { - if ( - tweetInteractRetweet.classList.contains( - "tweet-interact-retweet-disabled" - ) - ) { - return; - } - if (!tweetQuote.hidden) { - tweetQuote.hidden = true; - return; - } - if (tweetInteractRetweetMenu.hidden) { - tweetInteractRetweetMenu.hidden = false; - } - if (retweetClicked) return; - retweetClicked = true; - setTimeout(() => { - document.body.addEventListener( - "click", - () => { - retweetClicked = false; - setTimeout(() => (tweetInteractRetweetMenu.hidden = true), 50); - }, - { once: true } - ); - }, 50); - }); + }); + let quoteMedia = []; + tweetQuote.addEventListener("drop", (e) => { + handleDrop(e, quoteMedia, tweetQuoteMedia); + }); + tweetQuote.addEventListener("paste", (event) => { + let items = ( + event.clipboardData || event.originalEvent.clipboardData + ).items; + for (let index in items) { + let item = items[index]; + if (item.kind === "file") { + let file = item.getAsFile(); + handleFiles([file], quoteMedia, tweetQuoteMedia); + } + } + }); + tweetQuoteUpload.addEventListener("click", () => { + getMedia(quoteMedia, tweetQuoteMedia); + }); + tweetQuoteText.addEventListener("keydown", (e) => { + if (e.key === "Enter" && e.ctrlKey) { + tweetQuoteButton.click(); + } + }); + tweetQuoteText.addEventListener("input", (e) => { + let tweet = twttr.txt.parseTweet(e.target.value); + if (localStorage.OTisBlueVerified) { + return (tweetQuoteChar.innerText = `${tweet.weightedLength}/25000`); + } + tweetQuoteChar.innerText = `${tweet.weightedLength}/280`; + if (tweet.weightedLength > 265) { + tweetQuoteChar.style.color = "#c26363"; + } else { + tweetQuoteChar.style.color = ""; + } + if (tweet.weightedLength > 280) { + tweetQuoteChar.style.color = "red"; + tweetQuoteButton.disabled = true; + } else { + tweetQuoteButton.disabled = false; + } + }); + tweetQuoteButton.addEventListener("click", async () => { + let text = tweetQuoteText.value; + tweetQuoteError.innerHTML = ""; + if (text.length === 0 && quoteMedia.length === 0) return; + tweetQuoteButton.disabled = true; + let uploadedMedia = []; + for (let i in quoteMedia) { + let media = quoteMedia[i]; + try { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = false; + let mediaId; + if (!media.div.dataset.mediaId) { + mediaId = await API.uploadMedia({ + media_type: media.type, + media_category: media.category, + media: media.data, + alt: media.alt, + cw: media.cw, + loadCallback: (data) => { + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = `${data.text} (${data.progress}%)`; + }, + }); + } else { + mediaId = media.div.dataset.mediaId; + } + uploadedMedia.push(mediaId); + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = LOC.uploaded.message; + media.div.dataset.mediaId = mediaId; + } catch (e) { + console.error(e); + alert(e); + for (let j in quoteMedia) { + let media = quoteMedia[j]; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].hidden = true; + media.div.getElementsByClassName( + "new-tweet-media-img-progress" + )[0].innerText = ""; + } + tweetQuoteButton.disabled = false; + return; // cancel tweeting + } + } + let tweetObject = { + status: text, + attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`, + }; + if (uploadedMedia.length > 0) { + tweetObject.media_ids = uploadedMedia.join(","); + } + let tweetData; + try { + tweetData = await API.tweet.postV2(tweetObject); + } catch (e) { + tweetQuoteError.innerHTML = + (e && e.message ? e.message : e) + "
"; + tweetQuoteButton.disabled = false; + return; + } + if (!tweetData) { + tweetQuoteError.innerHTML = html`${LOC.error_sending_tweet + .message}
`; + tweetQuoteButton.disabled = false; + return; + } + tweetQuoteText.value = ""; + tweetQuote.hidden = true; + tweetData._ARTIFICIAL = true; + quoteMedia = []; + tweetQuoteChar.innerText = localStorage.OTisBlueVerified + ? "0/25000" + : "0/280"; + tweetQuoteButton.disabled = false; + tweetQuoteMedia.innerHTML = ""; + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + this.appendTweet(tweetData, timelineContainer, { prepend: true }); + }); - tweetInteractRetweetMenuRetweet.addEventListener("click", async () => { - if (!t.retweeted) { - let tweetData; - try { - tweetData = await API.tweet.retweet(t.id_str); - } catch (e) { - console.error(e); - alert(e); - return; - } - if (!tweetData) { - return; - } - tweetInteractRetweetMenuRetweet.innerText = LOC.unretweet.message; - tweetInteractRetweet.classList.add("tweet-interact-retweeted"); - t.retweeted = true; - t.retweet_count++; - t.newTweetId = tweetData.id_str; - if (!options.mainTweet) { - tweetInteractRetweet.dataset.val = - parseInt(tweetInteractRetweet.dataset.val) + 1; - if (vars.showExactValues || t.retweet_count < 10000) - tweetInteractRetweet.innerText = formatLargeNumber( - parseInt( - tweetInteractRetweet.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - if (vars.showExactValues || t.retweet_count < 10000) - tweetFooterRetweets.innerText = formatLargeNumber( - parseInt( - tweetFooterRetweets.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } - let event = new CustomEvent("tweetAction", { - detail: { - action: "retweet", - tweet: t, - tweetData, - }, + // Favorite + tweetInteractFavorite.addEventListener("click", () => { + if (t.favorited) { + API.tweet.unfavorite(t.id_str); + t.favorited = false; + t.favorite_count--; + if (!options.mainTweet) { + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) - 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetInteractFavorite.innerText = formatLargeNumber( + parseInt( + tweetInteractFavorite.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } else { + if ( + this.mainTweetLikers.find( + (liker) => liker.id_str === user.id_str + ) + ) { + this.mainTweetLikers.splice( + this.mainTweetLikers.findIndex( + (liker) => liker.id_str === user.id_str + ), + 1 + ); + let likerImg = footerFavorites.querySelector( + `a[data-id="${user.id_str}"]` + ); + if (likerImg) likerImg.remove(); + } + if (vars.showExactValues || t.favorite_count < 10000) + tweetFooterFavorites.innerText = formatLargeNumber( + parseInt( + tweetFooterFavorites.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) - 1 + ).replace(/\s/g, ","); + } + tweetInteractFavorite.classList.remove( + "tweet-interact-favorited" + ); + let event = new CustomEvent("tweetAction", { + detail: { + action: "unfavorite", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + API.tweet.favorite(t.id_str); + t.favorited = true; + t.favorite_count++; + if (!options.mainTweet) { + tweetInteractFavorite.dataset.val = + parseInt(tweetInteractFavorite.dataset.val) + 1; + if (vars.showExactValues || t.favorite_count < 10000) + tweetInteractFavorite.innerText = formatLargeNumber( + parseInt( + tweetInteractFavorite.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } else { + if ( + footerFavorites.children.length < 8 && + !this.mainTweetLikers.find( + (liker) => liker.id_str === user.id_str + ) + ) { + let a = document.createElement("a"); + a.href = `/${user.screen_name}`; + let likerImg = document.createElement("img"); + likerImg.src = `${ + user.default_profile_image && + vars.useOldDefaultProfileImage + ? chrome.runtime.getURL( + `images/default_profile_images/default_profile_${ + Number(user.id_str) % 7 + }_normal.png` + ) + : user.profile_image_url_https + }`; + likerImg.classList.add("tweet-footer-favorites-img"); + likerImg.title = + user.name + " (@" + user.screen_name + ")"; + likerImg.width = 24; + likerImg.height = 24; + a.dataset.id = user.id_str; + a.appendChild(likerImg); + footerFavorites.appendChild(a); + this.mainTweetLikers.push(user); + } + if (vars.showExactValues || t.favorite_count < 10000) + tweetFooterFavorites.innerText = formatLargeNumber( + parseInt( + tweetFooterFavorites.innerText + .replace(/\s/g, "") + .replace(/,/g, "") + .replace(/\./g, "") + ) + 1 + ).replace(/\s/g, ","); + } + tweetInteractFavorite.classList.add("tweet-interact-favorited"); + let event = new CustomEvent("tweetAction", { + detail: { + action: "favorite", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); }); - document.dispatchEvent(event); - } else { - let tweetData; - try { - tweetData = await API.tweet.unretweet( - t.retweeted_status ? t.retweeted_status.id_str : t.id_str - ); - } catch (e) { - console.error(e); - return; - } - if (!tweetData) { - return; - } - tweetInteractRetweetMenuRetweet.innerText = LOC.retweet.message; - tweetInteractRetweet.classList.remove("tweet-interact-retweeted"); - t.retweet_count--; - t.retweeted = false; - if (!options.mainTweet) { - tweetInteractRetweet.dataset.val = - parseInt(tweetInteractRetweet.dataset.val) - 1; - if (vars.showExactValues || t.retweet_count < 10000) - tweetInteractRetweet.innerText = formatLargeNumber( - parseInt( - tweetInteractRetweet.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } else { - if (vars.showExactValues || t.retweet_count < 10000) - tweetFooterRetweets.innerText = formatLargeNumber( - parseInt( - tweetFooterRetweets.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } - delete t.newTweetId; - let event = new CustomEvent("tweetAction", { - detail: { - action: "unretweet", - tweet: t, - tweetData, - }, + + // More + let moreClicked = false; + tweetInteractMore.addEventListener("click", () => { + if (tweetInteractMoreMenu.hidden) { + tweetInteractMoreMenu.hidden = false; + } + if (moreClicked) return; + moreClicked = true; + setTimeout(() => { + document.body.addEventListener( + "click", + () => { + moreClicked = false; + setTimeout( + () => (tweetInteractMoreMenu.hidden = true), + 50 + ); + }, + { once: true } + ); + }, 50); }); - document.dispatchEvent(event); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (options.mainTweet) { - tweetInteractRetweetMenuQuotes.addEventListener("click", async () => { - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets/with_comments` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (this.subpage === "tweet") { - this.updateReplies(id); - } else if (this.subpage === "likes") { - this.updateLikes(id); - } else if (this.subpage === "retweets") { - this.updateRetweets(id); - } else if (this.subpage === "retweets_with_comments") { - this.updateRetweetsWithComments(id); - } - this.currentLocation = location.pathname; - }); - tweetInteractRetweetMenuRetweeters.addEventListener("click", async () => { - history.pushState( - {}, - null, - `/${t.user.screen_name}/status/${t.id_str}/retweets` - ); - this.updateSubpage(); - this.mediaToUpload = []; - this.excludeUserMentions = []; - this.linkColors = {}; - this.cursor = undefined; - this.seenReplies = []; - this.mainTweetLikers = []; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (this.subpage === "tweet") { - this.updateReplies(id); - } else if (this.subpage === "likes") { - this.updateLikes(id); - } else if (this.subpage === "retweets") { - this.updateRetweets(id); - } else if (this.subpage === "retweets_with_comments") { - this.updateRetweetsWithComments(id); - } - this.currentLocation = location.pathname; - }); - } - tweetInteractRetweetMenuQuote.addEventListener("click", async () => { - if (!tweetReply.hidden) { - tweetInteractReply.classList.remove("tweet-interact-reply-clicked"); - tweetReply.hidden = true; - } - tweetQuote.hidden = false; - setTimeout(() => { - tweetQuoteText.focus(); - }); - }); - let quoteMedia = []; - tweetQuote.addEventListener("drop", (e) => { - handleDrop(e, quoteMedia, tweetQuoteMedia); - }); - tweetQuote.addEventListener("paste", (event) => { - let items = (event.clipboardData || event.originalEvent.clipboardData) - .items; - for (let index in items) { - let item = items[index]; - if (item.kind === "file") { - let file = item.getAsFile(); - handleFiles([file], quoteMedia, tweetQuoteMedia); - } - } - }); - tweetQuoteUpload.addEventListener("click", () => { - getMedia(quoteMedia, tweetQuoteMedia); - }); - tweetQuoteText.addEventListener("keydown", (e) => { - if (e.key === "Enter" && e.ctrlKey) { - tweetQuoteButton.click(); - } - }); - tweetQuoteText.addEventListener("input", (e) => { - let tweet = twttr.txt.parseTweet(e.target.value); - if (localStorage.OTisBlueVerified) { - return (tweetQuoteChar.innerText = `${tweet.weightedLength}/25000`); - } - tweetQuoteChar.innerText = `${tweet.weightedLength}/280`; - if (tweet.weightedLength > 265) { - tweetQuoteChar.style.color = "#c26363"; - } else { - tweetQuoteChar.style.color = ""; - } - if (tweet.weightedLength > 280) { - tweetQuoteChar.style.color = "red"; - tweetQuoteButton.disabled = true; - } else { - tweetQuoteButton.disabled = false; - } - }); - tweetQuoteButton.addEventListener("click", async () => { - let text = tweetQuoteText.value; - tweetQuoteError.innerHTML = ""; - if (text.length === 0 && quoteMedia.length === 0) return; - tweetQuoteButton.disabled = true; - let uploadedMedia = []; - for (let i in quoteMedia) { - let media = quoteMedia[i]; - try { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = false; - let mediaId; - if (!media.div.dataset.mediaId) { - mediaId = await API.uploadMedia({ - media_type: media.type, - media_category: media.category, - media: media.data, - alt: media.alt, - cw: media.cw, - loadCallback: (data) => { - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = `${data.text} (${data.progress}%)`; - }, + if (tweetInteractMoreMenuFollow) + tweetInteractMoreMenuFollow.addEventListener("click", async () => { + if (t.user.following) { + await API.user.unfollow(t.user.screen_name); + t.user.following = false; + if (LOC.follow_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuFollow.innerText = + LOC.follow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; + } + let event = new CustomEvent("tweetAction", { + detail: { + action: "unfollow", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + await API.user.follow(t.user.screen_name); + t.user.following = true; + if (LOC.unfollow_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuFollow.innerText = + LOC.unfollow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuFollow.innerText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; + } + let event = new CustomEvent("tweetAction", { + detail: { + action: "follow", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); }); - } else { - mediaId = media.div.dataset.mediaId; - } - uploadedMedia.push(mediaId); - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = LOC.uploaded.message; - media.div.dataset.mediaId = mediaId; - } catch (e) { - console.error(e); - alert(e); - for (let j in quoteMedia) { - let media = quoteMedia[j]; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].hidden = true; - media.div.getElementsByClassName( - "new-tweet-media-img-progress" - )[0].innerText = ""; - } - tweetQuoteButton.disabled = false; - return; // cancel tweeting - } - } - let tweetObject = { - status: text, - attachment_url: `https://twitter.com/${t.user.screen_name}/status/${t.id_str}`, - }; - if (uploadedMedia.length > 0) { - tweetObject.media_ids = uploadedMedia.join(","); - } - let tweetData; - try { - tweetData = await API.tweet.postV2(tweetObject); - } catch (e) { - tweetQuoteError.innerHTML = (e && e.message ? e.message : e) + "
"; - tweetQuoteButton.disabled = false; - return; - } - if (!tweetData) { - tweetQuoteError.innerHTML = html`${LOC.error_sending_tweet - .message}
`; - tweetQuoteButton.disabled = false; - return; - } - tweetQuoteText.value = ""; - tweetQuote.hidden = true; - tweetData._ARTIFICIAL = true; - quoteMedia = []; - tweetQuoteChar.innerText = localStorage.OTisBlueVerified - ? "0/25000" - : "0/280"; - tweetQuoteButton.disabled = false; - tweetQuoteMedia.innerHTML = ""; - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - this.appendTweet(tweetData, timelineContainer, { prepend: true }); - }); + if (tweetInteractMoreMenuBlock) + tweetInteractMoreMenuBlock.addEventListener("click", async () => { + if (t.user.blocking) { + await API.user.unblock(t.user.id_str); + t.user.blocking = false; + if (LOC.block_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuBlock.innerText = + LOC.block_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; + } + tweetInteractMoreMenuFollow.hidden = false; + let event = new CustomEvent("tweetAction", { + detail: { + action: "unblock", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + let blockMessage; + if (LOC.block_sure.message.includes("$SCREEN_NAME$")) { + blockMessage = LOC.block_sure.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; + } + let c = confirm(blockMessage); + if (!c) return; + await API.user.block(t.user.id_str); + t.user.blocking = true; + if (LOC.unblock_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuBlock.innerText = + LOC.unblock_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; + } + tweetInteractMoreMenuFollow.hidden = true; + t.user.following = false; + if (LOC.follow_user.message.includes("$SCREEN_NAME$")) { + tweetInteractMoreMenuFollow.innerText = + LOC.follow_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + } else { + tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; + } + let event = new CustomEvent("tweetAction", { + detail: { + action: "block", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + if (tweetInteractMoreMenuMuteUser) + tweetInteractMoreMenuMuteUser.addEventListener( + "click", + async () => { + if (t.user.muting) { + await API.user.unmute(t.user.id_str); + t.user.muting = false; + tweetInteractMoreMenuMuteUser.innerText = + LOC.mute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); - // Favorite - tweetInteractFavorite.addEventListener("click", () => { - if (t.favorited) { - API.tweet.unfavorite(t.id_str); - t.favorited = false; - t.favorite_count--; - if (!options.mainTweet) { - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) - 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetInteractFavorite.innerText = formatLargeNumber( - parseInt( - tweetInteractFavorite.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } else { - if ( - this.mainTweetLikers.find((liker) => liker.id_str === user.id_str) - ) { - this.mainTweetLikers.splice( - this.mainTweetLikers.findIndex( - (liker) => liker.id_str === user.id_str - ), - 1 + toast.info( + LOC.unmuted_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + ); + } else { + await API.user.mute(t.user.id_str); + t.user.muting = true; + tweetInteractMoreMenuMuteUser.innerText = + LOC.unmute_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ); + + toast.info( + LOC.muted_user.message.replace( + "$SCREEN_NAME$", + t.user.screen_name + ) + ); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + } ); - let likerImg = footerFavorites.querySelector( - `a[data-id="${user.id_str}"]` + tweetInteractMoreMenuCopy.addEventListener("click", () => { + navigator.clipboard.writeText( + `https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}` ); - if (likerImg) likerImg.remove(); - } - if (vars.showExactValues || t.favorite_count < 10000) - tweetFooterFavorites.innerText = formatLargeNumber( - parseInt( - tweetFooterFavorites.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) - 1 - ).replace(/\s/g, ","); - } - tweetInteractFavorite.classList.remove("tweet-interact-favorited"); - let event = new CustomEvent("tweetAction", { - detail: { - action: "unfavorite", - tweet: t, - }, }); - document.dispatchEvent(event); - } else { - API.tweet.favorite(t.id_str); - t.favorited = true; - t.favorite_count++; - if (!options.mainTweet) { - tweetInteractFavorite.dataset.val = - parseInt(tweetInteractFavorite.dataset.val) + 1; - if (vars.showExactValues || t.favorite_count < 10000) - tweetInteractFavorite.innerText = formatLargeNumber( - parseInt( - tweetInteractFavorite.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); - } else { - if ( - footerFavorites.children.length < 8 && - !this.mainTweetLikers.find((liker) => liker.id_str === user.id_str) - ) { - let a = document.createElement("a"); - a.href = `/${user.screen_name}`; - let likerImg = document.createElement("img"); - likerImg.src = `${ - user.default_profile_image && vars.useOldDefaultProfileImage - ? chrome.runtime.getURL( - `images/default_profile_images/default_profile_${ - Number(user.id_str) % 7 - }_normal.png` - ) - : user.profile_image_url_https - }`; - likerImg.classList.add("tweet-footer-favorites-img"); - likerImg.title = user.name + " (@" + user.screen_name + ")"; - likerImg.width = 24; - likerImg.height = 24; - a.dataset.id = user.id_str; - a.appendChild(likerImg); - footerFavorites.appendChild(a); - this.mainTweetLikers.push(user); - } - if (vars.showExactValues || t.favorite_count < 10000) - tweetFooterFavorites.innerText = formatLargeNumber( - parseInt( - tweetFooterFavorites.innerText - .replace(/\s/g, "") - .replace(/,/g, "") - .replace(/\./g, "") - ) + 1 - ).replace(/\s/g, ","); + if (tweetInteractMoreMenuCopyTweetId) + tweetInteractMoreMenuCopyTweetId.addEventListener("click", () => { + navigator.clipboard.writeText(t.id_str); + }); + if (tweetInteractMoreMenuCopyUserId) + tweetInteractMoreMenuCopyUserId.addEventListener("click", () => { + navigator.clipboard.writeText(t.user.id_str); + }); + if (tweetInteractMoreMenuShare) + tweetInteractMoreMenuShare.addEventListener("click", () => { + navigator.share({ + url: `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`, + }); + }); + tweetInteractMoreMenuShareDMs.addEventListener("click", () => { + tweetUrlToShareInDMs = `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`; + document.getElementById("messages").click(); + setTimeout(() => { + let title = document.querySelector( + "div.inbox h1.nice-header.larger" + ); + title.innerText = LOC.share_tweet_to.message; + }); + }); + tweetInteractMoreMenuNewtwitter.addEventListener("click", () => { + openInNewTab( + `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}?newtwitter=true` + ); + }); + tweetInteractMoreMenuEmbed.addEventListener("click", () => { + openInNewTab( + `https://publish.${location.hostname}/?query=https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}&widget=tweet` + ); + }); + if (t.user.id_str === user.id_str) { + tweetInteractMoreMenuAnalytics.addEventListener("click", () => { + openInNewTab( + `/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true` + ); + }); + tweetInteractMoreMenuDelete.addEventListener("click", async () => { + let sure = confirm(LOC.delete_sure.message); + if (!sure) return; + try { + await API.tweet.delete(t.id_str); + } catch (e) { + alert(e); + console.error(e); + return; + } + Array.from( + document + .getElementsByClassName("timeline")[0] + .querySelectorAll( + `div.tweet[data-tweet-id="${t.id_str}"]` + ) + ).forEach((tweet) => { + tweet.remove(); + }); + if (document.getElementById("timeline")) + Array.from( + document + .getElementById("timeline") + .querySelectorAll( + `div.tweet[data-tweet-id="${t.id_str}"]` + ) + ).forEach((tweet) => { + tweet.remove(); + }); + if (options.mainTweet) { + let tweets = Array.from( + timelineContainer.getElementsByClassName("tweet") + ); + if (tweets.length === 0) { + document + .getElementsByClassName("modal-close")[0] + .click(); + } else { + tweets[0].click(); + } + } + if (typeof timeline !== "undefined") { + timeline.data = timeline.data.filter( + (tweet) => tweet.id_str !== t.id_str + ); + } + if (options.after) { + if ( + options.after.getElementsByClassName( + "tweet-self-thread-div" + )[0] + ) + options.after.getElementsByClassName( + "tweet-self-thread-div" + )[0].hidden = true; + if (!options.after.classList.contains("tweet-main")) + options.after.getElementsByClassName( + "tweet-interact-reply" + )[0].innerText = ( + +options.after.getElementsByClassName( + "tweet-interact-reply" + )[0].innerText - 1 + ).toString(); + else + options.after.getElementsByClassName( + "tweet-footer-stat-replies" + )[0].innerText = ( + +options.after.getElementsByClassName( + "tweet-footer-stat-replies" + )[0].innerText - 1 + ).toString(); + } + let event = new CustomEvent("tweetAction", { + detail: { + action: "delete", + tweet: t, + }, + }); + document.dispatchEvent(event); + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); } - tweetInteractFavorite.classList.add("tweet-interact-favorited"); - let event = new CustomEvent("tweetAction", { - detail: { - action: "favorite", - tweet: t, - }, + tweetInteractMoreMenuMute.addEventListener("click", async () => { + if (t.conversation_muted) { + await API.tweet.unmute(t.id_str); + toast.info(LOC.unmuted_convo.message); + t.conversation_muted = false; + tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; + let event = new CustomEvent("tweetAction", { + detail: { + action: "unmute", + tweet: t, + }, + }); + document.dispatchEvent(event); + } else { + await API.tweet.mute(t.id_str); + toast.info(LOC.muted_convo.message); + t.conversation_muted = true; + tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; + let event = new CustomEvent("tweetAction", { + detail: { + action: "mute", + tweet: t, + }, + }); + document.dispatchEvent(event); + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); }); - document.dispatchEvent(event); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); + tweetInteractMoreMenuRefresh.addEventListener("click", async () => { + let tweetData; + try { + tweetData = await API.tweet.getV2(t.id_str); + } catch (e) { + console.error(e); + return; + } + if (!tweetData) { + return; + } + if ( + tweetInteractFavorite.className.includes( + "tweet-interact-favorited" + ) && + !tweetData.favorited + ) { + tweetInteractFavorite.classList.remove( + "tweet-interact-favorited" + ); + } + if ( + tweetInteractRetweet.className.includes( + "tweet-interact-retweeted" + ) && + !tweetData.retweeted + ) { + tweetInteractRetweet.classList.remove( + "tweet-interact-retweeted" + ); + } + if ( + !tweetInteractFavorite.className.includes( + "tweet-interact-favorited" + ) && + tweetData.favorited + ) { + tweetInteractFavorite.classList.add("tweet-interact-favorited"); + } + if ( + !tweetInteractRetweet.className.includes( + "tweet-interact-retweeted" + ) && + tweetData.retweeted + ) { + tweetInteractRetweet.classList.add("tweet-interact-retweeted"); + } + if (!options.mainTweet) { + tweetInteractFavorite.innerText = tweetData.favorite_count; + tweetInteractRetweet.innerText = tweetData.retweet_count; + tweetInteractReply.innerText = tweetData.reply_count; + } + chrome.storage.local.set( + { tweetReplies: {}, tweetDetails: {} }, + () => {} + ); + }); + let downloading = false; + if (t.extended_entities && t.extended_entities.media.length === 1) { + tweetInteractMoreMenuDownload.addEventListener("click", () => { + if (downloading) return; + downloading = true; + let media = t.extended_entities.media[0]; + let url = + media.type === "photo" + ? media.media_url_https + : media.video_info.variants[0].url; + _fetch(url) + .then((res) => res.blob()) + .then((blob) => { + downloading = false; + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); - // More - let moreClicked = false; - tweetInteractMore.addEventListener("click", () => { - if (tweetInteractMoreMenu.hidden) { - tweetInteractMoreMenu.hidden = false; - } - if (moreClicked) return; - moreClicked = true; - setTimeout(() => { - document.body.addEventListener( - "click", - () => { - moreClicked = false; - setTimeout(() => (tweetInteractMoreMenu.hidden = true), 50); - }, - { once: true } - ); - }, 50); - }); - if (tweetInteractMoreMenuFollow) - tweetInteractMoreMenuFollow.addEventListener("click", async () => { - if (t.user.following) { - await API.user.unfollow(t.user.screen_name); - t.user.following = false; - if (LOC.follow_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuFollow.innerText = - LOC.follow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; - } - let event = new CustomEvent("tweetAction", { - detail: { - action: "unfollow", - tweet: t, - }, - }); - document.dispatchEvent(event); - } else { - await API.user.follow(t.user.screen_name); - t.user.following = true; - if (LOC.unfollow_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuFollow.innerText = - LOC.unfollow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuFollow.innerText = `${LOC.unfollow_user.message} @${t.user.screen_name}`; - } - let event = new CustomEvent("tweetAction", { - detail: { - action: "follow", - tweet: t, - }, - }); - document.dispatchEvent(event); + let ts = new Date(t.created_at) + .toISOString() + .split("T")[0]; + let extension = url.split(".").pop(); + //let _index = t.extended_entities.media.length > 1 ? "_"+(index+1) : ""; + let _index = ""; + let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; + let filename_template = vars.customDownloadTemplate; + + // use the filename from the user's custom download template, if any + if (filename_template && filename_template.length > 0) { + const filesave_map = { + user_screen_name: t.user.screen_name, + user_name: t.user.name, + extension: extension, + timestamp: ts, + id: t.id_str, + index: _index, + }; + filename = filename_template.replace( + /\{([\w]+)\}/g, + (_, key) => filesave_map[key] + ); + } + + a.download = filename; + a.click(); + a.remove(); + }) + .catch((e) => { + downloading = false; + console.error(e); + }); + }); } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (tweetInteractMoreMenuBlock) - tweetInteractMoreMenuBlock.addEventListener("click", async () => { - if (t.user.blocking) { - await API.user.unblock(t.user.id_str); - t.user.blocking = false; - if (LOC.block_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuBlock.innerText = - LOC.block_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuBlock.innerText = `${LOC.block_user.message} @${t.user.screen_name}`; - } - tweetInteractMoreMenuFollow.hidden = false; - let event = new CustomEvent("tweetAction", { - detail: { - action: "unblock", - tweet: t, - }, - }); - document.dispatchEvent(event); - } else { - let blockMessage; - if (LOC.block_sure.message.includes("$SCREEN_NAME$")) { - blockMessage = LOC.block_sure.message.replace( - "$SCREEN_NAME$", - t.user.screen_name + if ( + t.extended_entities && + t.extended_entities.media.some((m) => m.type === "animated_gif") + ) { + tweetInteractMoreMenuDownloadGifs.forEach((dgb) => + dgb.addEventListener("click", (e) => { + if (downloading) return; + downloading = true; + let n = parseInt(e.target.dataset.gifno) - 1; + let videos = Array.from( + tweet.getElementsByClassName("tweet-media-gif") + ); + let video = videos[n]; + let canvas = document.createElement("canvas"); + canvas.width = video.videoWidth; + canvas.height = video.videoHeight; + let ctx = canvas.getContext("2d"); + if (video.duration > 10 && !confirm(LOC.long_vid.message)) { + return (downloading = false); + } + let mde = + tweet.getElementsByClassName("tweet-media-data")[0]; + mde.innerText = LOC.initialization.message + "..."; + let gif = new GIF({ + workers: 4, + quality: 15, + debug: true, + workerScript: window.gifWorkerUrl, + }); + video.currentTime = 0; + video.loop = false; + let isFirst = true; + let step = 50; + let interval = setInterval(async () => { + if (isFirst) { + video.currentTime = 0; + isFirst = false; + await sleep(5); + } + mde.innerText = `${ + LOC.initialization.message + }... (${Math.round( + ((video.currentTime / video.duration) * 100) | 0 + )}%)`; + if (video.currentTime + step / 1000 >= video.duration) { + clearInterval(interval); + gif.on("working", (frame, frames) => { + mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; + }); + gif.on("finished", (blob) => { + mde.innerText = ""; + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = `${t.id_str}.gif`; + document.body.append(a); + a.click(); + a.remove(); + downloading = false; + video.loop = true; + video.play(); + }); + gif.render(); + return; + } + ctx.drawImage(video, 0, 0, canvas.width, canvas.height); + let imgData = ctx.getImageData( + 0, + 0, + canvas.width, + canvas.height + ); + gif.addFrame(imgData, { delay: step }); + }, step); + }) ); - } else { - blockMessage = `${LOC.block_sure.message} @${t.user.screen_name}?`; - } - let c = confirm(blockMessage); - if (!c) return; - await API.user.block(t.user.id_str); - t.user.blocking = true; - if (LOC.unblock_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuBlock.innerText = - LOC.unblock_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuBlock.innerText = `${LOC.unblock_user.message} @${t.user.screen_name}`; - } - tweetInteractMoreMenuFollow.hidden = true; - t.user.following = false; - if (LOC.follow_user.message.includes("$SCREEN_NAME$")) { - tweetInteractMoreMenuFollow.innerText = - LOC.follow_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - } else { - tweetInteractMoreMenuFollow.innerText = `${LOC.follow_user.message} @${t.user.screen_name}`; - } - let event = new CustomEvent("tweetAction", { - detail: { - action: "block", - tweet: t, - }, - }); - document.dispatchEvent(event); } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - if (tweetInteractMoreMenuMuteUser) - tweetInteractMoreMenuMuteUser.addEventListener("click", async () => { - if (t.user.muting) { - await API.user.unmute(t.user.id_str); - t.user.muting = false; - tweetInteractMoreMenuMuteUser.innerText = - LOC.mute_user.message.replace("$SCREEN_NAME$", t.user.screen_name); - toast.info( - LOC.unmuted_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ) - ); + if (options.after) { + options.after.after(tweet); + } else if (options.before) { + options.before.before(tweet); + } else if (options.prepend) { + timelineContainer.prepend(tweet); } else { - await API.user.mute(t.user.id_str); - t.user.muting = true; - tweetInteractMoreMenuMuteUser.innerText = - LOC.unmute_user.message.replace( - "$SCREEN_NAME$", - t.user.screen_name - ); - - toast.info( - LOC.muted_user.message.replace("$SCREEN_NAME$", t.user.screen_name) - ); + timelineContainer.append(tweet); } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - tweetInteractMoreMenuCopy.addEventListener("click", () => { - navigator.clipboard.writeText( - `https://${vars.copyLinksAs}/${t.user.screen_name}/status/${t.id_str}` - ); - }); - if (tweetInteractMoreMenuCopyTweetId) - tweetInteractMoreMenuCopyTweetId.addEventListener("click", () => { - navigator.clipboard.writeText(t.id_str); - }); - if (tweetInteractMoreMenuCopyUserId) - tweetInteractMoreMenuCopyUserId.addEventListener("click", () => { - navigator.clipboard.writeText(t.user.id_str); - }); - if (tweetInteractMoreMenuShare) - tweetInteractMoreMenuShare.addEventListener("click", () => { - navigator.share({ - url: `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`, - }); - }); - tweetInteractMoreMenuShareDMs.addEventListener("click", () => { - tweetUrlToShareInDMs = `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}`; - document.getElementById("messages").click(); - setTimeout(() => { - let title = document.querySelector("div.inbox h1.nice-header.larger"); - title.innerText = LOC.share_tweet_to.message; - }); - }); - tweetInteractMoreMenuNewtwitter.addEventListener("click", () => { - openInNewTab( - `https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}?newtwitter=true` - ); - }); - tweetInteractMoreMenuEmbed.addEventListener("click", () => { - openInNewTab( - `https://publish.${location.hostname}/?query=https://${location.hostname}/${t.user.screen_name}/status/${t.id_str}&widget=tweet` - ); - }); - if (t.user.id_str === user.id_str) { - tweetInteractMoreMenuAnalytics.addEventListener("click", () => { - openInNewTab( - `/${t.user.screen_name}/status/${t.id_str}/analytics?newtwitter=true` - ); - }); - tweetInteractMoreMenuDelete.addEventListener("click", async () => { - let sure = confirm(LOC.delete_sure.message); - if (!sure) return; + if (vars.enableTwemoji) twemoji.parse(tweet); + return tweet; + } + async popstateChange(that) { + that.savePageData(that.currentLocation); + that.updateSubpage(); + if ( + location.pathname.includes("retweets/with_comments") && + that.subpage === "retweets_with_comments" && + document.getElementById("this-is-tweet-page") + ) { + return document.querySelector(".modal-close").click(); + } + that.mediaToUpload = []; + that.excludeUserMentions = []; + that.linkColors = {}; + that.cursor = undefined; + that.seenReplies = []; + that.insertedMores = []; + that.mainTweetLikers = []; + let id; try { - await API.tweet.delete(t.id_str); + id = location.pathname.match(/status\/(\d{1,32})/)[1]; } catch (e) { - alert(e); - console.error(e); - return; + return that.container + .getElementsByClassName("modal-close")[0] + .click(); } - Array.from( - document - .getElementsByClassName("timeline")[0] - .querySelectorAll(`div.tweet[data-tweet-id="${t.id_str}"]`) - ).forEach((tweet) => { - tweet.remove(); - }); - if (document.getElementById("timeline")) - Array.from( - document - .getElementById("timeline") - .querySelectorAll(`div.tweet[data-tweet-id="${t.id_str}"]`) - ).forEach((tweet) => { - tweet.remove(); - }); - if (options.mainTweet) { - let tweets = Array.from( - timelineContainer.getElementsByClassName("tweet") - ); - if (tweets.length === 0) { - document.getElementsByClassName("modal-close")[0].click(); - } else { - tweets[0].click(); - } - } - if (typeof timeline !== "undefined") { - timeline.data = timeline.data.filter( - (tweet) => tweet.id_str !== t.id_str - ); - } - if (options.after) { - if (options.after.getElementsByClassName("tweet-self-thread-div")[0]) - options.after.getElementsByClassName( - "tweet-self-thread-div" - )[0].hidden = true; - if (!options.after.classList.contains("tweet-main")) - options.after.getElementsByClassName( - "tweet-interact-reply" - )[0].innerText = ( - +options.after.getElementsByClassName("tweet-interact-reply")[0] - .innerText - 1 - ).toString(); - else - options.after.getElementsByClassName( - "tweet-footer-stat-replies" - )[0].innerText = ( - +options.after.getElementsByClassName( - "tweet-footer-stat-replies" - )[0].innerText - 1 - ).toString(); + let restored = await that.restorePageData(); + if (!restored) { + if (that.subpage === "tweet") { + that.updateReplies(id); + } else if (that.subpage === "likes") { + that.updateLikes(id); + } else if (that.subpage === "retweets") { + that.updateRetweets(id); + } else if (that.subpage === "retweets_with_comments") { + that.updateRetweetsWithComments(id); + } + } else { + this.container.scrollTop = restored.scrollY; } - let event = new CustomEvent("tweetAction", { - detail: { - action: "delete", - tweet: t, - }, - }); - document.dispatchEvent(event); - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); + that.currentLocation = location.pathname; } - tweetInteractMoreMenuMute.addEventListener("click", async () => { - if (t.conversation_muted) { - await API.tweet.unmute(t.id_str); - toast.info(LOC.unmuted_convo.message); - t.conversation_muted = false; - tweetInteractMoreMenuMute.innerText = LOC.mute_convo.message; - let event = new CustomEvent("tweetAction", { - detail: { - action: "unmute", - tweet: t, - }, - }); - document.dispatchEvent(event); - } else { - await API.tweet.mute(t.id_str); - toast.info(LOC.muted_convo.message); - t.conversation_muted = true; - tweetInteractMoreMenuMute.innerText = LOC.unmute_convo.message; - let event = new CustomEvent("tweetAction", { - detail: { - action: "mute", - tweet: t, - }, - }); - document.dispatchEvent(event); - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - tweetInteractMoreMenuRefresh.addEventListener("click", async () => { - let tweetData; - try { - tweetData = await API.tweet.getV2(t.id_str); - } catch (e) { - console.error(e); - return; - } - if (!tweetData) { - return; - } - if ( - tweetInteractFavorite.className.includes("tweet-interact-favorited") && - !tweetData.favorited - ) { - tweetInteractFavorite.classList.remove("tweet-interact-favorited"); - } - if ( - tweetInteractRetweet.className.includes("tweet-interact-retweeted") && - !tweetData.retweeted - ) { - tweetInteractRetweet.classList.remove("tweet-interact-retweeted"); - } - if ( - !tweetInteractFavorite.className.includes("tweet-interact-favorited") && - tweetData.favorited - ) { - tweetInteractFavorite.classList.add("tweet-interact-favorited"); - } - if ( - !tweetInteractRetweet.className.includes("tweet-interact-retweeted") && - tweetData.retweeted - ) { - tweetInteractRetweet.classList.add("tweet-interact-retweeted"); - } - if (!options.mainTweet) { - tweetInteractFavorite.innerText = tweetData.favorite_count; - tweetInteractRetweet.innerText = tweetData.retweet_count; - tweetInteractReply.innerText = tweetData.reply_count; - } - chrome.storage.local.set( - { tweetReplies: {}, tweetDetails: {} }, - () => {} - ); - }); - let downloading = false; - if (t.extended_entities && t.extended_entities.media.length === 1) { - tweetInteractMoreMenuDownload.addEventListener("click", () => { - if (downloading) return; - downloading = true; - let media = t.extended_entities.media[0]; - let url = - media.type === "photo" - ? media.media_url_https - : media.video_info.variants[0].url; - _fetch(url) - .then((res) => res.blob()) - .then((blob) => { - downloading = false; - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - - let ts = new Date(t.created_at).toISOString().split("T")[0]; - let extension = url.split(".").pop(); - //let _index = t.extended_entities.media.length > 1 ? "_"+(index+1) : ""; - let _index = ""; - let filename = `${t.user.screen_name}_${ts}_${t.id_str}${_index}.${extension}`; - let filename_template = vars.customDownloadTemplate; - - // use the filename from the user's custom download template, if any - if (filename_template && filename_template.length > 0) { - const filesave_map = { - user_screen_name: t.user.screen_name, - user_name: t.user.name, - extension: extension, - timestamp: ts, - id: t.id_str, - index: _index, - }; - filename = filename_template.replace( - /\{([\w]+)\}/g, - (_, key) => filesave_map[key] - ); + async onScroll(that) { + if ( + this.container.scrollTop + 300 > + this.container.scrollHeight - this.container.clientHeight && + !that.loadingNewTweets + ) { + if ( + this.moreBtn && + that.subpage === "tweet" && + !this.moreBtn.hidden + ) { + this.moreBtn.click(); } - - a.download = filename; - a.click(); - a.remove(); - }) - .catch((e) => { - downloading = false; - console.error(e); - }); - }); + } } - if ( - t.extended_entities && - t.extended_entities.media.some((m) => m.type === "animated_gif") - ) { - tweetInteractMoreMenuDownloadGifs.forEach((dgb) => - dgb.addEventListener("click", (e) => { - if (downloading) return; - downloading = true; - let n = parseInt(e.target.dataset.gifno) - 1; - let videos = Array.from( - tweet.getElementsByClassName("tweet-media-gif") - ); - let video = videos[n]; - let canvas = document.createElement("canvas"); - canvas.width = video.videoWidth; - canvas.height = video.videoHeight; - let ctx = canvas.getContext("2d"); - if (video.duration > 10 && !confirm(LOC.long_vid.message)) { - return (downloading = false); - } - let mde = tweet.getElementsByClassName("tweet-media-data")[0]; - mde.innerText = LOC.initialization.message + "..."; - let gif = new GIF({ - workers: 4, - quality: 15, - debug: true, - workerScript: window.gifWorkerUrl, - }); - video.currentTime = 0; - video.loop = false; - let isFirst = true; - let step = 50; - let interval = setInterval(async () => { - if (isFirst) { - video.currentTime = 0; - isFirst = false; - await sleep(5); - } - mde.innerText = `${LOC.initialization.message}... (${Math.round( - ((video.currentTime / video.duration) * 100) | 0 - )}%)`; - if (video.currentTime + step / 1000 >= video.duration) { - clearInterval(interval); - gif.on("working", (frame, frames) => { - mde.innerText = `${LOC.converting.message}... (${frame}/${frames})`; - }); - gif.on("finished", (blob) => { - mde.innerText = ""; - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = `${t.id_str}.gif`; - document.body.append(a); - a.click(); - a.remove(); - downloading = false; - video.loop = true; - video.play(); - }); - gif.render(); - return; - } - ctx.drawImage(video, 0, 0, canvas.width, canvas.height); - let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); - gif.addFrame(imgData, { delay: step }); - }, step); - }) - ); + async appendTombstone(timelineContainer, text) { + try { + if (typeof text === "string") + LOC.replacer_post_to_tweet.message.split("|").forEach((el) => { + let [or, nr] = el.split("->"); + or = or[0].toUpperCase() + or.slice(1); + text = text.replace(new RegExp(or, "g"), nr); + }); + } catch (e) { + console.error(e); + } + this.tweets.push(["tombstone", text]); + let tombstone = document.createElement("div"); + tombstone.className = "tweet-tombstone"; + tombstone.innerHTML = text; + timelineContainer.append(tombstone); } + init() { + document + .getElementsByClassName("timeline-more")[0] + .addEventListener("click", async (e) => { + if (!this.cursor || this.loadingNewTweets) return; + this.loadingNewTweets = true; + e.target.innerText = LOC.loading_tweets.message; + let path = location.pathname; + if (path.endsWith("/")) path = path.slice(0, -1); + this.updateReplies(path.split("/").slice(-1)[0], this.cursor); + }); + document + .getElementsByClassName("likes-more")[0] + .addEventListener("click", async () => { + if (!this.likeCursor) return; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateLikes(id, this.likeCursor); + }); + document + .getElementsByClassName("retweets-more")[0] + .addEventListener("click", async () => { + if (!this.retweetCursor) return; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweets(id, this.retweetCursor); + }); + document + .getElementsByClassName("retweets_with_comments-more")[0] + .addEventListener("click", async (e) => { + e.target.innerText = LOC.loading_tweets.message; + if (!this.retweetCommentsCursor) return; + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + this.updateRetweetsWithComments(id, this.retweetCommentsCursor); + }); - if (options.after) { - options.after.after(tweet); - } else if (options.before) { - options.before.before(tweet); - } else if (options.prepend) { - timelineContainer.prepend(tweet); - } else { - timelineContainer.append(tweet); - } - if (vars.enableTwemoji) twemoji.parse(tweet); - return tweet; - } - async popstateChange(that) { - that.savePageData(that.currentLocation); - that.updateSubpage(); - if ( - location.pathname.includes("retweets/with_comments") && - that.subpage === "retweets_with_comments" && - document.getElementById("this-is-tweet-page") - ) { - return document.querySelector(".modal-close").click(); - } - that.mediaToUpload = []; - that.excludeUserMentions = []; - that.linkColors = {}; - that.cursor = undefined; - that.seenReplies = []; - that.insertedMores = []; - that.mainTweetLikers = []; - let id; - try { - id = location.pathname.match(/status\/(\d{1,32})/)[1]; - } catch (e) { - return that.container.getElementsByClassName("modal-close")[0].click(); - } - let restored = await that.restorePageData(); - if (!restored) { - if (that.subpage === "tweet") { - that.updateReplies(id); - } else if (that.subpage === "likes") { - that.updateLikes(id); - } else if (that.subpage === "retweets") { - that.updateRetweets(id); - } else if (that.subpage === "retweets_with_comments") { - that.updateRetweetsWithComments(id); - } - } else { - this.container.scrollTop = restored.scrollY; - } - that.currentLocation = location.pathname; - } - async onScroll(that) { - if ( - this.container.scrollTop + 300 > - this.container.scrollHeight - this.container.clientHeight && - !that.loadingNewTweets - ) { - if (this.moreBtn && that.subpage === "tweet" && !this.moreBtn.hidden) { - this.moreBtn.click(); - } - } - } - async appendTombstone(timelineContainer, text) { - try { - if (typeof text === "string") - LOC.replacer_post_to_tweet.message.split("|").forEach((el) => { - let [or, nr] = el.split("->"); - or = or[0].toUpperCase() + or.slice(1); - text = text.replace(new RegExp(or, "g"), nr); + this.updateSubpage(); + let id = location.pathname.match(/status\/(\d{1,32})/)[1]; + if (this.subpage === "tweet") { + this.updateReplies(id); + } else if (this.subpage === "likes") { + this.updateLikes(id); + } else if (this.subpage === "retweets") { + this.updateRetweets(id); + } else if (this.subpage === "retweets_with_comments") { + this.updateRetweetsWithComments(id); + } + this.popstateHelper = () => this.popstateChange(this); + this.scrollHelper = () => this.onScroll(this); + window.addEventListener("popstate", this.popstateHelper); + this.container.addEventListener("scroll", this.scrollHelper, { + passive: true, }); - } catch (e) { - console.error(e); } - this.tweets.push(["tombstone", text]); - let tombstone = document.createElement("div"); - tombstone.className = "tweet-tombstone"; - tombstone.innerHTML = text; - timelineContainer.append(tombstone); - } - init() { - document - .getElementsByClassName("timeline-more")[0] - .addEventListener("click", async (e) => { - if (!this.cursor || this.loadingNewTweets) return; - this.loadingNewTweets = true; - e.target.innerText = LOC.loading_tweets.message; - let path = location.pathname; - if (path.endsWith("/")) path = path.slice(0, -1); - this.updateReplies(path.split("/").slice(-1)[0], this.cursor); - }); - document - .getElementsByClassName("likes-more")[0] - .addEventListener("click", async () => { - if (!this.likeCursor) return; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateLikes(id, this.likeCursor); - }); - document - .getElementsByClassName("retweets-more")[0] - .addEventListener("click", async () => { - if (!this.retweetCursor) return; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweets(id, this.retweetCursor); - }); - document - .getElementsByClassName("retweets_with_comments-more")[0] - .addEventListener("click", async (e) => { - e.target.innerText = LOC.loading_tweets.message; - if (!this.retweetCommentsCursor) return; - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - this.updateRetweetsWithComments(id, this.retweetCommentsCursor); - }); - - this.updateSubpage(); - let id = location.pathname.match(/status\/(\d{1,32})/)[1]; - if (this.subpage === "tweet") { - this.updateReplies(id); - } else if (this.subpage === "likes") { - this.updateLikes(id); - } else if (this.subpage === "retweets") { - this.updateRetweets(id); - } else if (this.subpage === "retweets_with_comments") { - this.updateRetweetsWithComments(id); + close() { + document.removeEventListener("scroll", this.onscroll); + window.removeEventListener("popstate", this.popstateHelper); + this.container.removeEventListener("scroll", this.scrollHelper); } - this.popstateHelper = () => this.popstateChange(this); - this.scrollHelper = () => this.onScroll(this); - window.addEventListener("popstate", this.popstateHelper); - this.container.addEventListener("scroll", this.scrollHelper, { - passive: true, - }); - } - close() { - document.removeEventListener("scroll", this.onscroll); - window.removeEventListener("popstate", this.popstateHelper); - this.container.removeEventListener("scroll", this.scrollHelper); - } } diff --git a/scripts/xIconRemove.js b/scripts/xIconRemove.js index 802d4a80..59c8118a 100644 --- a/scripts/xIconRemove.js +++ b/scripts/xIconRemove.js @@ -1,55 +1,55 @@ setInterval(() => { - let xIcon = document.querySelector( - 'a[href^="https://twitter.com/home"] > div > svg, a[href^="https://x.com/home"] > div > svg' - ); - if (xIcon) { - let parent = xIcon.parentElement; - let img = document.createElement("img"); - img.src = chrome.runtime.getURL("images/logo32_new.png"); - img.style.cssText = - "width: 2em;height: 2em;image-rendering: -webkit-optimize-contrast;"; - parent.appendChild(img); - xIcon.remove(); - } + let xIcon = document.querySelector( + 'a[href^="https://twitter.com/home"] > div > svg, a[href^="https://x.com/home"] > div > svg' + ); + if (xIcon) { + let parent = xIcon.parentElement; + let img = document.createElement("img"); + img.src = chrome.runtime.getURL("images/logo32_new.png"); + img.style.cssText = + "width: 2em;height: 2em;image-rendering: -webkit-optimize-contrast;"; + parent.appendChild(img); + xIcon.remove(); + } - let title = document.querySelector("title"); - if (title) { - if (title.innerText.endsWith(" / X")) { - title.innerText = title.innerText.replace(" / X", " / Twitter"); + let title = document.querySelector("title"); + if (title) { + if (title.innerText.endsWith(" / X")) { + title.innerText = title.innerText.replace(" / X", " / Twitter"); + } } - } }, 200); function removeAndReplaceX(element) { - if (element) { - let parent = element.parentElement; - let img = document.createElement("img"); - img.src = chrome.runtime.getURL("images/logo32_new.png"); - img.style.cssText = - "width: 2em;height: 2em;image-rendering: -webkit-optimize-contrast;display: block;top: 50%;position: absolute;left: 50%;transform: translate(-50%, -50%);"; - parent.appendChild(img); - element.remove(); - xObserver.disconnect(); + if (element) { + let parent = element.parentElement; + let img = document.createElement("img"); + img.src = chrome.runtime.getURL("images/logo32_new.png"); + img.style.cssText = + "width: 2em;height: 2em;image-rendering: -webkit-optimize-contrast;display: block;top: 50%;position: absolute;left: 50%;transform: translate(-50%, -50%);"; + parent.appendChild(img); + element.remove(); + xObserver.disconnect(); - setTimeout(() => { - img.remove(); - }, 500); - } + setTimeout(() => { + img.remove(); + }, 500); + } } const xObserver = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === "childList" && mutation.addedNodes.length > 0) { - mutation.addedNodes.forEach((node) => { - if (node.nodeType === Node.ELEMENT_NODE) { - if (node.tagName === "SVG") { - removeAndReplaceX(node); - } - node.querySelectorAll("svg").forEach(removeAndReplaceX); + mutations.forEach((mutation) => { + if (mutation.type === "childList" && mutation.addedNodes.length > 0) { + mutation.addedNodes.forEach((node) => { + if (node.nodeType === Node.ELEMENT_NODE) { + if (node.tagName === "SVG") { + removeAndReplaceX(node); + } + node.querySelectorAll("svg").forEach(removeAndReplaceX); + } + }); } - }); - } - }); + }); }); // Start observing the page for changes From 5a6aa92fec7e1101d394d956df269bbe8942d197 Mon Sep 17 00:00:00 2001 From: Shinon Date: Sat, 28 Jun 2025 18:54:44 +0800 Subject: [PATCH 26/29] Fix missing "tweet-footer-favorites" element --- scripts/tweetConstructor.js | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index b7c20f37..d75af085 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1040,6 +1040,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { tweet_footer_node = elNew("div", { class: ["tweet-footer"] }, [ elNew("div", { class: ["tweet-footer-stats"] }, statsArray), + elNew("div", { class: ["tweet-footer-favorites"] }), ]); } var dt = []; From 9d87f5d93d6ba520b377619f3899feb697ca7f29 Mon Sep 17 00:00:00 2001 From: Shinon Date: Sun, 29 Jun 2025 15:59:40 +0800 Subject: [PATCH 27/29] favorited -> bookmarked --- scripts/tweetConstructor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/tweetConstructor.js b/scripts/tweetConstructor.js index d75af085..8d0b94ef 100644 --- a/scripts/tweetConstructor.js +++ b/scripts/tweetConstructor.js @@ -1102,7 +1102,7 @@ async function constructTweet(t, tweetConstructorArgs, options = {}) { } var bookmarkClasses = ["tweet-button", "tweet-interact-bookmark"]; - if (t.favorited) { + if (t.bookmarked) { bookmarkClasses.push("tweet-interact-bookmarked"); } From 0f57e70e0916da74c06b4fd01a127ed083cb26d4 Mon Sep 17 00:00:00 2001 From: Shinon Date: Sun, 29 Jun 2025 16:06:11 +0800 Subject: [PATCH 28/29] Fixup locale. --- _locales/ar/messages.json | 2 +- _locales/bg/messages.json | 2 +- _locales/ca/messages.json | 2 +- _locales/cs/messages.json | 2 +- _locales/de/messages.json | 2 +- _locales/el/messages.json | 4 ++-- _locales/es/messages.json | 2 +- _locales/fi/messages.json | 2 +- _locales/fr/messages.json | 2 +- _locales/gl/messages.json | 2 +- _locales/he/messages.json | 4 ++-- _locales/id/messages.json | 2 +- _locales/is/messages.json | 2 +- _locales/it/messages.json | 2 +- _locales/ja/messages.json | 2 +- _locales/ko/messages.json | 2 +- _locales/lv/messages.json | 4 ++-- _locales/nb/messages.json | 2 +- _locales/ne/messages.json | 2 +- _locales/nl/messages.json | 2 +- _locales/pl/messages.json | 2 +- _locales/pt_BR/messages.json | 4 ++-- _locales/ro/messages.json | 2 +- _locales/ru/messages.json | 2 +- _locales/sv/messages.json | 2 +- _locales/th/messages.json | 2 +- _locales/tl/messages.json | 2 +- _locales/tr/messages.json | 2 +- _locales/uk/messages.json | 2 +- _locales/vi/messages.json | 2 +- _locales/zh_CN/messages.json | 2 +- _locales/zh_TW/messages.json | 2 +- 32 files changed, 36 insertions(+), 36 deletions(-) diff --git a/_locales/ar/messages.json b/_locales/ar/messages.json index 94d8f9e6..03b5441b 100644 --- a/_locales/ar/messages.json +++ b/_locales/ar/messages.json @@ -175,7 +175,7 @@ "algo": { "message": "حسابي" }, "timeline_type": { "message": "الجدول الزمني نوع" }, "show_topic_tweets": { "message": "عرض الموضوع تغريدات (algo فقط)" }, - "show_colors_in_timeline": { "message": "عرض ألوان الارتباط المخصصة للمستخدمين في المخطط الزمني الرئيسي" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "تمكين Twemoji" }, "hearts_instead_stars": { "message": "إظهار القلوب (أعجبني) بدلاً من النجوم (المفضلة)" }, "dark_mode": { "message": "النسق الداكن" }, diff --git a/_locales/bg/messages.json b/_locales/bg/messages.json index e0b6d806..592af761 100644 --- a/_locales/bg/messages.json +++ b/_locales/bg/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Алгоритмично" }, "timeline_type": { "message": "Тип на лента" }, "show_topic_tweets": { "message": "Покажи тематични туитове (само алгоритмично)" }, - "show_colors_in_timeline": { "message": "Покажи потребителски цветове в лентата" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Включи Twemoji" }, "hearts_instead_stars": { "message": "Покажи сърца (харесвания) вместо звездички (любими)" }, "dark_mode": { "message": "Тъмна тема" }, diff --git a/_locales/ca/messages.json b/_locales/ca/messages.json index bc23db5c..8c654c1f 100644 --- a/_locales/ca/messages.json +++ b/_locales/ca/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorítmic V1 (sense límit de velocitat)" }, "timeline_type": { "message": "Tipus de cronologia" }, "show_topic_tweets": { "message": "Mostra els tuits relacionats (només algo)" }, - "show_colors_in_timeline": { "message": "Mostra enllaços amb colors personalitzats dels usuaris a Inici" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Activar Twemoji" }, "hearts_instead_stars": { "message": "Mostra cors (M'agrades) en lloc d'estrelles (Preferits)" }, "dark_mode": { "message": "Mode fosc" }, diff --git a/_locales/cs/messages.json b/_locales/cs/messages.json index e68d5a20..c379b9da 100644 --- a/_locales/cs/messages.json +++ b/_locales/cs/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmicky" }, "timeline_type": { "message": "Typ časové osy" }, "show_topic_tweets": { "message": "Zobrazovat tematické tweety (pouze algoritmicky)" }, - "show_colors_in_timeline": { "message": "Zobrazovat vlastní barvy odkazů na domovské stránce" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Povolit Twemoji" }, "hearts_instead_stars": { "message": "Zobrazit srdíčka (lajky) namísto hvězd (oblíbené)" }, "dark_mode": { "message": "Tmavý režim" }, diff --git a/_locales/de/messages.json b/_locales/de/messages.json index c9697634..6338f7e4 100644 --- a/_locales/de/messages.json +++ b/_locales/de/messages.json @@ -183,7 +183,7 @@ "algo": {"message": "Algorithmisch V1 (ohne Ratenbegrenzung)"}, "timeline_type": {"message": "Zeitlinientyp"}, "show_topic_tweets": {"message": "Themen-Tweets anzeigen (nur Algorithmisch)"}, - "show_colors_in_timeline": {"message": "Benutzerdefinierte Linkfarben in der Startzeitachse anzeigen"}, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": {"message": "Twemoji aktivieren"}, "hearts_instead_stars": {"message": "Herzen (Likes) statt Sterne (Favoriten) anzeigen"}, "dark_mode": {"message": "Dunkler Modus"}, diff --git a/_locales/el/messages.json b/_locales/el/messages.json index faebc61a..4581052c 100644 --- a/_locales/el/messages.json +++ b/_locales/el/messages.json @@ -559,8 +559,8 @@ "show_topic_tweets": { "message": "Εμφάνιση θεματικών tweets (μόνο algo)" }, - "show_colors_in_timeline": { - "message": "Εμφάνιση προσαρμοσμένων χρωμάτων συνδέσμων χρηστών στο αρχικό χρονολογικό" + "slow_show_colors_in_timeline": { + "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Ενεργοποιήστε Twemoji" diff --git a/_locales/es/messages.json b/_locales/es/messages.json index 0981a351..b1f694ed 100644 --- a/_locales/es/messages.json +++ b/_locales/es/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Por algoritmo" }, "timeline_type": { "message": "Tipo de cronología" }, "show_topic_tweets": { "message": "Mostrar tweets relacionados (solo por algoritmo)" }, - "show_colors_in_timeline": { "message": "Mostrar enlaces con colores personalizados de los usuarios en Inicio" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Activar Twemoji" }, "hearts_instead_stars": { "message": "Mostrar corazones (Me gusta) en lugar de estrellas (Favorito)" }, "dark_mode": { "message": "Modo oscuro" }, diff --git a/_locales/fi/messages.json b/_locales/fi/messages.json index 6c3597b6..0bfcc7bc 100644 --- a/_locales/fi/messages.json +++ b/_locales/fi/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritminen" }, "timeline_type": { "message": "Aikajanatyyppi" }, "show_topic_tweets": { "message": "Näytä aiheen twiitit (vain algo)" }, - "show_colors_in_timeline": { "message": "Näytä käyttäjien mukautetut linkkien värit aikajanalla" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Käytä Twemojia" }, "hearts_instead_stars": { "message": "Näytä sydämet (tykkäykset) tähtien (suosikit) sijasta" }, "dark_mode": { "message": "Tumma tila" }, diff --git a/_locales/fr/messages.json b/_locales/fr/messages.json index fa35f2b0..15c7f1d5 100644 --- a/_locales/fr/messages.json +++ b/_locales/fr/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorithmique" }, "timeline_type": { "message": "Type de chronologie" }, "show_topic_tweets": { "message": "Afficher les tweets de sujet (algorithme seulement)" }, - "show_colors_in_timeline": { "message": "Afficher les couleurs des liens des utilisateurs dans la chronologie d'accueil" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "Toujours afficher les couleurs des liens personnalisés plutôt que de les afficher au survol" }, "enable_twemoji": { "message": "Activer Twemoji" }, "hearts_instead_stars": { "message": "Afficher des cœurs (J'aime) à la place d'étoiles (Favoris)" }, diff --git a/_locales/gl/messages.json b/_locales/gl/messages.json index 0c87d7cc..bcad7f38 100644 --- a/_locales/gl/messages.json +++ b/_locales/gl/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorítmico" }, "timeline_type": { "message": "Tipo de cronoloxía" }, "show_topic_tweets": { "message": "Amosar chíos de temas (só algorítmico)" }, - "show_colors_in_timeline": { "message": "Amosar cores de ligazóns personalizadas dos usuarios na cronoloxía principal" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "Amosar sempre cores de ligazóns personalizadas en lugar de só ao pasar o rato" }, "enable_twemoji": { "message": "Activar Twemoji" }, "hearts_instead_stars": { "message": "Amosar corazóns (gústame) en lugar de estrelas (favoritos)" }, diff --git a/_locales/he/messages.json b/_locales/he/messages.json index 1e8565f7..37b5bca0 100644 --- a/_locales/he/messages.json +++ b/_locales/he/messages.json @@ -539,8 +539,8 @@ "show_topic_tweets": { "message": "הצג ציוצי נושא (אלגוריתמי בלבד) " }, - "show_colors_in_timeline": { - "message": "הצג קישור צבע מותאם אישית של משתמשים בציר זמן דף הבית " + "slow_show_colors_in_timeline": { + "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "הפעל Twemoji" diff --git a/_locales/id/messages.json b/_locales/id/messages.json index 53bf0b19..94421d76 100644 --- a/_locales/id/messages.json +++ b/_locales/id/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmik" }, "timeline_type": { "message": "Tipe linimasa" }, "show_topic_tweets": { "message": "Tampilkan tweet bertopik (algo saja)" }, - "show_colors_in_timeline": { "message": "Perlihatkan warna tautan khusus pengguna di linimasa beranda" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "Selalu tampilkan warna tautan khusus, bukan saat diarahkan pointer tetikus" }, "enable_twemoji": { "message": "Gunakan Twemoji" }, "hearts_instead_stars": { "message": "Tampilkan hati (likes) instead of stars (favorites)" }, diff --git a/_locales/is/messages.json b/_locales/is/messages.json index b83a678b..edee9b7b 100644 --- a/_locales/is/messages.json +++ b/_locales/is/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Reikniritlegur" }, "timeline_type": { "message": "Tegund tímalínu" }, "show_topic_tweets": { "message": "Sýna tíst um efni (aðeins reiknirit)" }, - "show_colors_in_timeline": { "message": "Sýndu sérsniðna tenglaliti notenda á tímalínunni heima" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Virkja Twemoji" }, "hearts_instead_stars": { "message": "Sýna hjörtu (líkar) í stað stjarna (uppáhalds)" }, "dark_mode": { "message": "Dökkstilling" }, diff --git a/_locales/it/messages.json b/_locales/it/messages.json index 3ee089f0..dfcfa0ca 100644 --- a/_locales/it/messages.json +++ b/_locales/it/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmico" }, "timeline_type": { "message": "Tipo di timeline" }, "show_topic_tweets": { "message": "Mostra tweet topici (solo per timeline algoritmica)" }, - "show_colors_in_timeline": { "message": "Mostra i colori personalizzati dei link degli utenti nella timeline della home" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Attiva Twemoji" }, "hearts_instead_stars": { "message": "Mostra cuori (like) invece di stelle (preferiti)" }, "dark_mode": { "message": "Modalità scura" }, diff --git a/_locales/ja/messages.json b/_locales/ja/messages.json index 88c21683..1caa7fb7 100644 --- a/_locales/ja/messages.json +++ b/_locales/ja/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "あなたへのおすすめ" }, "timeline_type": { "message": "タイムラインの種類" }, "show_topic_tweets": { "message": "トピックツイートの表示(あなたへのおすすめのみ)" }, - "show_colors_in_timeline": { "message": "ユーザーがカスタマイズしたプロフィールのリンクカラーを適用する" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Twitter絵文字ライブラリの有効化" }, "hearts_instead_stars": { "message": "お気に入りをいいねに変更する" }, "dark_mode": { "message": "ダークモード" }, diff --git a/_locales/ko/messages.json b/_locales/ko/messages.json index a4fe3a9e..7be8fbda 100644 --- a/_locales/ko/messages.json +++ b/_locales/ko/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "맞춤 트윗 (추천 V1) (리밋 없음)" }, "timeline_type": { "message": "타임라인의 종류" }, "show_topic_tweets": { "message": "토픽 트윗을 표시합니다 (나를 위한 추천 전용)" }, - "show_colors_in_timeline": { "message": "홈 타임라인에 커스텀 링크 색상을 적용합니다" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "마우스를 올리지 않아도 항상 사용자 지정 링크 색상을 사용합니다" }, "enable_twemoji": { "message": "트위터 이모티콘을 사용합니다" }, "hearts_instead_stars": { "message": "별(관심글) 대신 하트(마음에 들어요)를 표시합니다" }, diff --git a/_locales/lv/messages.json b/_locales/lv/messages.json index 1dc77e37..a6f047e3 100644 --- a/_locales/lv/messages.json +++ b/_locales/lv/messages.json @@ -566,8 +566,8 @@ "show_topic_tweets": { "message": "Rādīt tēmu tvītus (tikai algo)" }, - "show_colors_in_timeline": { - "message": "Rādīt pielāgotas lietotāju saišu krāsas sākuma feeda" + "slow_show_colors_in_timeline": { + "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Iezlēgt Twemoji" diff --git a/_locales/nb/messages.json b/_locales/nb/messages.json index fc2b9da6..54ec9716 100644 --- a/_locales/nb/messages.json +++ b/_locales/nb/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorytmisk" }, "timeline_type": { "message": "Tidslinjetype" }, "show_topic_tweets": { "message": "Vis emne-tweets (kun algorytmisk)" }, - "show_colors_in_timeline": { "message": "Vis brukerdefinerte profilfarger i hjemmetidslinjen" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Bruk Twemoji" }, "hearts_instead_stars": { "message": "Vis hjerter (likes) instedenfor stjerner (favoritter)" }, "dark_mode": { "message": "Mørk modus" }, diff --git a/_locales/ne/messages.json b/_locales/ne/messages.json index ac11a5e7..cf2b09d5 100644 --- a/_locales/ne/messages.json +++ b/_locales/ne/messages.json @@ -176,7 +176,7 @@ "algo": { "message": "एल्गोरिदमिकल" }, "timeline_type": { "message": "कस्तो खालको कलक्रम" }, "show_topic_tweets": { "message": "विषयको ट्वीट देखाउन (एल्गोरिदमिकल मात्रै)" }, - "show_colors_in_timeline": { "message": "मान्छेहरूको अनुकूल रँग कलक्रममा देखाउन" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Twemoji लगाउन" }, "hearts_instead_stars": { "message": "ताराहरू होइन मुटुहरू मात्रै देखाउन" }, "dark_mode": { "message": "अँधेरो मोड" }, diff --git a/_locales/nl/messages.json b/_locales/nl/messages.json index ea090912..e02a7c8d 100644 --- a/_locales/nl/messages.json +++ b/_locales/nl/messages.json @@ -177,7 +177,7 @@ "algo": { "message": "Algoritmisch" }, "timeline_type": { "message": "Tijdlijn soort" }, "show_topic_tweets": { "message": "Toon onderwerp tweets (alleen algo)" }, - "show_colors_in_timeline": { "message": "Toon aangepaste koppeling kleuren of gebruikers in thuis tijdlijn" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Twemoji inschakelen" }, "hearts_instead_stars": { "message": "Toon hartjes (likes) inplaats van sterren (favorieten)" }, "dark_mode": { "message": "Dark mode" }, diff --git a/_locales/pl/messages.json b/_locales/pl/messages.json index efbdad29..6c6682a6 100644 --- a/_locales/pl/messages.json +++ b/_locales/pl/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algorytmiczna" }, "timeline_type": { "message": "Typ strony głównej" }, "show_topic_tweets": { "message": "Pokaż tweety Tematów (tylko algortym)" }, - "show_colors_in_timeline": { "message": "Pokaż linki kolorów użytkowników na głównej" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Włącz Twemoji" }, "hearts_instead_stars": { "message": "Wyświetl serduszka zamiast gwiazdek" }, "dark_mode": { "message": "Tryb ciemny" }, diff --git a/_locales/pt_BR/messages.json b/_locales/pt_BR/messages.json index b2567d39..8090dd16 100644 --- a/_locales/pt_BR/messages.json +++ b/_locales/pt_BR/messages.json @@ -601,8 +601,8 @@ "show_topic_tweets": { "message": "Mostrar tweets de tópicos (somente algoritmo)" }, - "show_colors_in_timeline": { - "message": "Mostrar links em cores de outros usuários" + "slow_show_colors_in_timeline": { + "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Usar Twemoji" diff --git a/_locales/ro/messages.json b/_locales/ro/messages.json index 462fd3e0..367122dd 100644 --- a/_locales/ro/messages.json +++ b/_locales/ro/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmic V1 (fără rate limiting)" }, "timeline_type": { "message": "Tip timeline" }, "show_topic_tweets": { "message": "Afișați tweet-uri din subiecte (doar algoritmic)" }, - "show_colors_in_timeline": { "message": "Afișați culorile personalizate ale legăturilor utilizatorilor în cronologia de acasă" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Activare Twemoji" }, "hearts_instead_stars": { "message": "Afișați inimioare (aprecieri) în loc de stele (favorite)" }, "dark_mode": { "message": "Mod întunecat" }, diff --git a/_locales/ru/messages.json b/_locales/ru/messages.json index ef1a2d9d..a229037d 100644 --- a/_locales/ru/messages.json +++ b/_locales/ru/messages.json @@ -191,7 +191,7 @@ "algo": { "message": "Алгоритмическая" }, "timeline_type": { "message": "Тип ленты" }, "show_topic_tweets": { "message": "Показывать твиты тем (только алгоритмическая)" }, - "show_colors_in_timeline": { "message": "Показывать пользовательские цвета в ленте" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Включить Twemoji" }, "hearts_instead_stars": { "message": "Показывать сердца (лайки) вместо звёзд (любимое)" }, "dark_mode": { "message": "Тёмная тема" }, diff --git a/_locales/sv/messages.json b/_locales/sv/messages.json index 11a2dd09..604afd45 100644 --- a/_locales/sv/messages.json +++ b/_locales/sv/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmisk ordning" }, "timeline_type": { "message": "Typ av tidslinje" }, "show_topic_tweets": { "message": "Visa ämnesrelaterade Tweets (endast för algoritmisk ordning)" }, - "show_colors_in_timeline": { "message": "Visa anpassade länkfärger i tidslinjen" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Använd Twitters emojis" }, "hearts_instead_stars": { "message": "Visa hjärtan (gilla-markeringar) istället för stjärnor (favoritmarkeringar)" }, "dark_mode": { "message": "Mörkt läge" }, diff --git a/_locales/th/messages.json b/_locales/th/messages.json index cd53bc5d..db10695e 100644 --- a/_locales/th/messages.json +++ b/_locales/th/messages.json @@ -176,7 +176,7 @@ "algo": { "message": "ด้วยอัลกอริทึม" }, "timeline_type": { "message": "ชนิดของไทม์ไลน์" }, "show_topic_tweets": { "message": "เเสดงหัวข้อทวิต (อัลกอริทึ่มเท่านั้น)" }, - "show_colors_in_timeline": { "message": "เเสดงสีลิ้งค์ที่กำหนดเอง ในไทม์ไลน์หน้าหลัก" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "เปิดใช้งาน Twemoji" }, "hearts_instead_stars": { "message": "เเสดงหัวใจ(ไลค์) เเทนดาว(ชื่นชอบ)" }, "dark_mode": { "message": "โหมดทมิฬ" }, diff --git a/_locales/tl/messages.json b/_locales/tl/messages.json index ee21f984..67294d2a 100644 --- a/_locales/tl/messages.json +++ b/_locales/tl/messages.json @@ -177,7 +177,7 @@ "algo": { "message": "Algorithmical" }, "timeline_type": { "message": "Uri ng timeline" }, "show_topic_tweets": { "message": "Ipakita ang mga tweet ng paksa (algo lang)" }, - "show_colors_in_timeline": { "message": "Ipakita ang mga custom na kulay ng link ng mga user sa home timeline" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Paganahin ang Twemoji" }, "hearts_instead_stars": { "message": "Ipakita ang puso (mga kagustuhan) kaysa sa bituin (mga paborito)" }, "dark_mode": { "message": "Dark mode" }, diff --git a/_locales/tr/messages.json b/_locales/tr/messages.json index 8d15ae2f..bafd7a68 100644 --- a/_locales/tr/messages.json +++ b/_locales/tr/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Algoritmik" }, "timeline_type": { "message": "Zaman akışı tipi" }, "show_topic_tweets": { "message": "Konu tweet'lerini göster (yalnızca algo)" }, - "show_colors_in_timeline": { "message": "Ana sayfa akışında kullanıcıların özel bağlantı renklerini göster" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "Fareyle üzerinde gelinmesi harici her zaman özel bağlantı renklerini göster." }, "enable_twemoji": { "message": "Twemoji'yi etkinleştir" }, "hearts_instead_stars": { "message": "ıldızlar yerine (favoriler) kalpler (beğeniler) göster" }, diff --git a/_locales/uk/messages.json b/_locales/uk/messages.json index 06d834cf..91f942d3 100644 --- a/_locales/uk/messages.json +++ b/_locales/uk/messages.json @@ -177,7 +177,7 @@ "algo": { "message": "Алгоритмічна V1 (без ліміту)" }, "timeline_type": { "message": "Тип стрічки" }, "show_topic_tweets": { "message": "Показувати твіти тем (тільки алгорітмічна)" }, - "show_colors_in_timeline": { "message": "Показувати кольори посилань користувачів в стрічці" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Включити Twemoji" }, "hearts_instead_stars": { "message": "Показувати сердця (лайки) замість зірок (улюблене)" }, "dark_mode": { "message": "Темна тема" }, diff --git a/_locales/vi/messages.json b/_locales/vi/messages.json index cb0ba560..6b7097e1 100644 --- a/_locales/vi/messages.json +++ b/_locales/vi/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "Theo thuật toán (\"For You\"/\"Dành cho bạn\")" }, "timeline_type": { "message": "Kiểu dòng thời gian" }, "show_topic_tweets": { "message": "Hiện các tweet chủ đề (theo thuật toán)" }, - "show_colors_in_timeline": { "message": "Hiện màu link tùy chỉnh của người dùng trên dòng thời gian trang chủ" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "enable_twemoji": { "message": "Sử dụng Twemoji" }, "hearts_instead_stars": { "message": "Hiện trái tim (likes) thay vì sao (favorites)" }, "dark_mode": { "message": "Giao diện ban đêm" }, diff --git a/_locales/zh_CN/messages.json b/_locales/zh_CN/messages.json index 72141137..97385541 100644 --- a/_locales/zh_CN/messages.json +++ b/_locales/zh_CN/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "算法(为您推荐)" }, "timeline_type": { "message": "时间线类型" }, "show_topic_tweets": { "message": "显示话题推文(仅限算法)" }, - "show_colors_in_timeline": { "message": "在主页时间线中显示用户的自定义链接颜色" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "始终显示自定义链接颜色,而不是仅在悬停时显示" }, "enable_twemoji": { "message": "启用 Twemoji" }, "hearts_instead_stars": { "message": "显示心形(喜欢)而不是星形(收藏)" }, diff --git a/_locales/zh_TW/messages.json b/_locales/zh_TW/messages.json index 9fef6e16..d40178f0 100644 --- a/_locales/zh_TW/messages.json +++ b/_locales/zh_TW/messages.json @@ -183,7 +183,7 @@ "algo": { "message": "舊版推薦演算法" }, "timeline_type": { "message": "時間軸類型" }, "show_topic_tweets": { "message": "顯示話題推文(僅限推薦演算法)" }, - "show_colors_in_timeline": { "message": "在首頁時間軸中顯示使用者的自訂連結顏色" }, + "slow_show_colors_in_timeline": { "message": "Show custom link colors of users in home timeline (This can cause slow downs on some devices!)" }, "always_show_link_color": { "message": "總是顯示自訂連結顏色,而非僅在懸停時顯示" }, "enable_twemoji": { "message": "啟用 Twemoji" }, "hearts_instead_stars": { "message": "顯示心型(喜歡),而非星型(收藏)" }, From dd3feeb02d030d5a25b797283b3df6a0475550ab Mon Sep 17 00:00:00 2001 From: Shinon Date: Sun, 29 Jun 2025 16:12:56 +0800 Subject: [PATCH 29/29] Remove accidental timing data. --- layouts/profile/script.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/layouts/profile/script.js b/layouts/profile/script.js index 2f3366a9..8e028019 100644 --- a/layouts/profile/script.js +++ b/layouts/profile/script.js @@ -1848,7 +1848,6 @@ async function renderProfile() { }; async function renderTimeline(append = false, sliceAmount = 0) { - const timer = Date.now(); let timelineContainer = document.getElementById('timeline'); if(!append) timelineContainer.innerHTML = ''; let data = timeline.data.slice(sliceAmount, timeline.data.length);; @@ -1903,7 +1902,6 @@ async function renderTimeline(append = false, sliceAmount = 0) { }; document.getElementById('loading-box').hidden = true; loadingNewTweets = false; - console.log(`[renderTimeline@profile] Took ${(Date.now() - timer) / 1000}s to complete`) return true; } function renderNewTweetsButton() {