diff --git a/crx-chromium-version.txt b/crx-chromium-version.txt deleted file mode 100644 index 2d4f0bd..0000000 --- a/crx-chromium-version.txt +++ /dev/null @@ -1 +0,0 @@ -Chromium 73.0.3683.75 built on Debian buster/sid, running on Debian buster/sid diff --git a/src/Images/ImageExpand.js b/src/Images/ImageExpand.js index efb4954..af46459 100644 --- a/src/Images/ImageExpand.js +++ b/src/Images/ImageExpand.js @@ -121,7 +121,13 @@ var ImageExpand = { ) { return } - return $.queueTask(func, post) + return $.queueTask(function () { + if (file.isExpanded) { + return ImageExpand.contract(post) + } else { + return ImageExpand.expand(post) + } + }) } if ( diff --git a/src/Posting/QR.js b/src/Posting/QR.js index 7b848bf..5d3f1d6 100644 --- a/src/Posting/QR.js +++ b/src/Posting/QR.js @@ -1275,7 +1275,7 @@ var QR = { QR.cooldown.changes = dict(); QR.cooldown.auto = false; QR.cooldown.update(); - return $.queueTask($.delete, 'cooldowns'); + return $.queueTask($.delete('cooldowns', dict())); }, update() { diff --git a/src/platform/$.ts b/src/platform/$.ts index 2137979..c899629 100644 --- a/src/platform/$.ts +++ b/src/platform/$.ts @@ -1,30 +1,29 @@ +/// + import Notice from "../classes/Notice"; import { c, Conf, d, doc, g } from "../globals/globals"; import CrossOrigin from "./CrossOrigin"; import { debounce, dict, MINUTE, platform, SECOND } from "./helpers"; -import { AjaxPageOptions, ElementProperties } from "../types/$"; - +import { AjaxPageOptions, Dict, ElementProperties, SyncObject, WhenModifiedOptions } from "../types/$"; +import Callbacks from "../classes/Callbacks"; // not chainable const $ = (selector, root = document.body) => root.querySelector(selector); -type AjaxPageRequest = XMLHttpRequest & { - abort: () => void; -} $.id = id => d.getElementById(id); $.cache = dict(); -$.ajaxPage = function (url, options) { +$.ajaxPage = function (url: string, options: AjaxPageOptions = {}) { if (options == null) { options = {}; } const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options; const r = new XMLHttpRequest(); - const id = ++Request; + const id = Date.now() + Math.random(); const e = new CustomEvent('4chanXAjax', { detail: { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } }); d.dispatchEvent(e); r.onloadend = function () { - delete window.FCX.requests[id]; - return onloadend.apply(this, arguments); + if (onloadend) { onloadend.call(r, r); } + return d.dispatchEvent(new CustomEvent('4chanXAjaxEnd', { detail: { id } })); }; return r; } -$.ready = function (fc) { +$.ready = function (fc: () => void) { if (d.readyState !== 'loading') { $.queueTask(fc); return; @@ -36,7 +35,7 @@ $.ready = function (fc) { return $.on(d, 'DOMContentLoaded', cb); }; -$.formData = function (form) { +$.formData = function (form: FormData | ElementProperties) { if (form instanceof HTMLFormElement) { return new FormData(form); } @@ -54,28 +53,27 @@ $.formData = function (form) { return fd; }; -$.extend = function (object, properties) { +$.extend = function (object: Object, properties: Object) { for (var key in properties) { - var val = properties[key]; - object[key] = val; + var value = properties[key]; + object[key] = value; } + return object; }; -$.hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key); +$.hasOwn = function (obj: Object, key: string) { return Object.prototype.hasOwnProperty.call(obj, key); }; -$.getOwn = function (obj, key) { - if (Object.prototype.hasOwnProperty.call(obj, key)) { return obj[key]; } else { return undefined; } -}; +$.getOwn = function (obj: Object, key: string) { if ($.hasOwn(obj, key)) { return obj[key]; } }; $.ajax = (function () { - let pageXHR; + let pageXHR = XMLHttpRequest; if (window.wrappedJSObject && !XMLHttpRequest.wrappedJSObject) { pageXHR = XPCNativeWrapper(window.wrappedJSObject.XMLHttpRequest); } else { pageXHR = XMLHttpRequest; } - const r = (function (url, options = {}) { + const r = (function (url, options: AjaxPageOptions = {}) { if (options.responseType == null) { options.responseType = 'json'; } if (!options.type) { options.type = (options.form && 'post') || 'get'; } // XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310 @@ -86,8 +84,8 @@ $.ajax = (function () { return $.ajaxPage(url, options); } } - const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options; - const r = new pageXHR(); + const { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options as AjaxPageOptions; + const r = new pageXHR() as XMLHttpRequest; try { r.open(type, url, true); const object = headers || {}; @@ -103,7 +101,8 @@ $.ajax = (function () { // https://bugs.chromium.org/p/chromium/issues/detail?id=920638 $.on(r, 'load', () => { if (!Conf['Work around CORB Bug'] && r.readyState === 4 && r.status === 200 && r.statusText === '' && r.response === null) { - $.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now())); + $.set('Work around CORB Bug', (Conf['Work around CORB Bug'] = Date.now()), cb => cb()); + return c.warn(`4chan X failed to load: ${url}`); } }); } @@ -112,8 +111,8 @@ $.ajax = (function () { // XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error. if (err.result !== 0x805e0006) { throw err; } r.onloadend = onloadend; - $.queueTask($.event, 'error', null, r); - $.queueTask($.event, 'loadend', null, r); + $.queueTask($.event); + $.queueTask($.event); } return r; }); @@ -127,12 +126,13 @@ $.ajax = (function () { $.ajaxPageInit = function () { $.global(function () { + //@ts-ignore window.FCX.requests = Object.create(null); - document.addEventListener('4chanXAjax', function (e) { let fd, r; const { url, timeout, responseType, withCredentials, type, onprogress, form, headers, id } = e.detail; - window.FCX.requests[id] = (r = new XMLHttpRequest()); + //@ts-ignore + window.FCX.requests[id] = r = new pageXHR(); r.open(type, url, true); const object = headers || {}; for (var key in object) { @@ -150,6 +150,7 @@ $.ajax = (function () { }; } r.onloadend = function () { + //@ts-ignore delete window.FCX.requests[id]; const { status, statusText, response } = this; const responseHeaderString = this.getAllResponseHeaders(); @@ -174,20 +175,21 @@ $.ajax = (function () { return document.addEventListener('4chanXAjaxAbort', function (e) { let r; + //@ts-ignore if (!(r = window.FCX.requests[e.detail.id])) { return; } return r.abort(); } , false); - }); + }, 0); - $.on(d, '4chanXAjaxProgress', function (e) { - let req; + $.on(d, '4chanXAjaxProgress', function (e: CustomEvent) { + let req: any; if (!(req = requests[e.detail.id])) { return; } - return req.upload.onprogress.call(req.upload, e.detail); + return req.onprogress(e); }); - return $.on(d, '4chanXAjaxLoadend', function (e) { - let req; + return $.on(d, '4chanXAjaxLoadend', function (e: CustomEvent) { + let req: any; if (!(req = requests[e.detail.id])) { return; } delete requests[e.detail.id]; if (e.detail.status) { @@ -203,14 +205,22 @@ $.ajax = (function () { }; return $.ajaxPage = function (url, options = {}) { - let req; - let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options; + let req: any; + let { onloadend, timeout, responseType, withCredentials, type, onprogress, form, headers } = options || {}; const id = requestID++; requests[id] = (req = new CrossOrigin.Request()); $.extend(req, { responseType, onloadend }); req.upload = { onprogress }; req.abort = () => $.event('4chanXAjaxAbort', { id }); - if (form) { form = Array.from(form.entries()); } + if (form) { + form = new FormData(form); + for (var entry of form) { + if (entry[0] === 'json') { + form.delete(entry[0]); + form.append(entry[0], JSON.stringify(entry[1])); + } + } + } $.event('4chanXAjax', { url, timeout, responseType, withCredentials, type, onprogress: !!onprogress, form, headers, id }); return req; }; @@ -221,26 +231,32 @@ $.ajax = (function () { // With the `If-Modified-Since` header we only receive the HTTP headers and no body for 304 responses. // This saves a lot of bandwidth and CPU time for both the users and the servers. $.lastModified = dict(); -$.whenModified = function (url, bucket, cb, options = {}) { +$.whenModified = function ( + url: string, + bucket: string, + cb: (this: JQueryXHR) => void, + options: WhenModifiedOptions = {} +): JQueryXHR { const { timeout, ajax = $.ajax } = options; - let params = []; - let lastModifiedTime; + const params: string[] = []; + const originalUrl = url; - if ($.engine === 'blink') { + if ($.engine === "blink") { params.push(`s=${bucket}`); } - if (url.split('/')[2] === 'a.4cdn.org') { + if (url.split("/")[2] === "a.4cdn.org") { params.push(`t=${Date.now()}`); } - const originalUrl = url; if (params.length) { - url += '?' + params.join('&'); + url += "?" + params.join("&"); } - const headers = {}; - if ((lastModifiedTime = $.lastModified[bucket]?.[originalUrl]) != null) { - headers['If-Modified-Since'] = lastModifiedTime; + const headers: { [key: string]: string } = {}; + const lastModifiedTime = $.lastModified[bucket]?.[originalUrl]; + + if (lastModifiedTime != null) { + headers["If-Modified-Since"] = lastModifiedTime; } return ajax(url, { @@ -251,15 +267,15 @@ $.whenModified = function (url, bucket, cb, options = {}) { cb.call(this); }, timeout, - headers + headers, }); }; (function () { const reqs = dict(); - $.cache = function (url, cb, options = {}) { - let req; + $.cache = function (url, cb, options: { ajax?: typeof $.ajax } = {}) { + let req: any; const { ajax } = options; if (req = reqs[url]) { if (req.callbacks) { @@ -294,49 +310,49 @@ $.whenModified = function (url, bucket, cb, options = {}) { $.cb = { checked() { if ($.hasOwn(Conf, this.name)) { - $.set(this.name, this.checked); + $.set(this.name, this.checked, true); return Conf[this.name] = this.checked; } }, value() { if ($.hasOwn(Conf, this.name)) { - $.set(this.name, this.value.trim()); + $.set(this.name, this.value.trim(), cb => { + if (cb) { + return this.value = cb; + } + }); + } return Conf[this.name] = this.value; } - } -}; + }, -$.asap = function (test, cb) { +$.asap = function (test: () => boolean, cb: () => void) { if (test()) { return cb(); - } else { - return setTimeout($.asap, 25, test, cb); } + return setTimeout(() => $.asap(test, cb), 0); }; -$.onExists = function (root, selector, cb) { - let el; - if (el = $(selector, root)) { - return cb(el); - } - var observer = new MutationObserver(function () { - if (el = $(selector, root)) { +$.onExists = function (root: HTMLElement, selector: string, cb: (el: HTMLElement) => void): MutationObserver { + const observer = new MutationObserver(() => { + const el = root.querySelector(selector); + if (el) { observer.disconnect(); - return cb(el); + return cb(root.querySelector(selector)); } }); - return observer.observe(root, { childList: true, subtree: true }); + observer.observe(root, { childList: true, subtree: true }); + return observer; }; -$.addStyle = function (css, id, test = 'head') { - const style = $.el('style', - { textContent: css }); - if (id != null) { style.id = id; } +$.addStyle = function (css: string, id: string, test = 'head') { + if (id && d.getElementById(id)) { return; } + const style = $.el('style', { id, textContent: css }); $.onExists(doc, test, () => $.add(d.head, style)); return style; }; -$.addCSP = function (policy) { +$.addCSP = function (policy: string) { const meta = $.el('meta', { httpEquiv: 'Content-Security-Policy', content: policy }, { display: 'none' }); $.onExists(doc, 'head', () => $.add(d.head, meta)); return meta; @@ -440,8 +456,24 @@ if (platform === 'userscript') { return new CustomEvent('x', { detail: {} }); } catch (err) { const unsafeConstructors = { - Object: unsafeWindow.Object, - Array: unsafeWindow.Array + 'Object': Object, + 'Array': Array, + 'String': String, + 'Number': Number, + 'Boolean': Boolean, + 'RegExp': RegExp, + 'Date': Date, + 'Error': Error, + 'EvalError': EvalError, + 'RangeError': RangeError, + 'ReferenceError': ReferenceError, + 'SyntaxError': SyntaxError, + 'TypeError': TypeError, + 'URIError': URIError, + 'Map': Map, + 'Set': Set, + 'WeakMap': WeakMap, + 'WeakSet': WeakSet, }; var clone = function (obj) { let constructor; @@ -494,32 +526,16 @@ $.debounce = function (wait, fn) { return timeout = setTimeout(exec, wait); }; }; - -$.queueTask = (function () { - // inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007 - const taskQueue = []; - const execTask = function () { - const task = taskQueue.shift(); - const func = task[0]; - const args = Array.prototype.slice.call(task, 1); - return func.apply(func, args); - }; - if (window.MessageChannel) { - const taskChannel = new MessageChannel(); - taskChannel.port1.onmessage = execTask; - return function () { - taskQueue.push(arguments); - return taskChannel.port2.postMessage(null); - }; - } else { // XXX Firefox - return function () { - taskQueue.push(arguments); - return setTimeout(execTask, 0); - }; +//ok +$.queueTask = function (fn) { + if (typeof requestIdleCallback === 'function') { + return requestIdleCallback(fn); + } else { + return setTimeout(fn, 0); } -})(); +}; -$.global = function (fn, data) { +$.global = function (fn: Function, data: object) { if (doc) { const script = $.el('script', { textContent: `(${fn}).call(document.currentScript.dataset);` }); @@ -536,7 +552,7 @@ $.global = function (fn, data) { } }; -$.bytesToString = function (size) { +$.bytesToString = function (size: number) { if (size < 1024) { return `${size} B`; } else if (size < 1048576) { @@ -548,7 +564,7 @@ $.bytesToString = function (size) { } }; -$.minmax = (value, min, max) => Math.max(min, Math.min(max, value)); +$.minmax = (value: number, min: number, max: number): number => Math.max(min, Math.min(max, value)); $.hasAudio = function (el: HTMLVideoElement | HTMLAudioElement) { if (el.tagName === 'VIDEO') { @@ -565,13 +581,13 @@ $.luma = (rgb: number[]) => { // rgb: [r, g, b] return 0.2126 * r + 0.7152 * g + 0.0722 * b; }; -$.unescape = function (text) { +$.unescape = function (text: string): string { if (text == null) { return text; } return text.replace(/<[^>]*>/g, '').replace(/&(amp|#039|quot|lt|gt|#44);/g, c => ({ '&': '&', ''': "'", '"': '"', '<': '<', '>': '>', ',': ',' })[c]); }; -$.isImage = url => /\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/i.test(url); -$.isVideo = url => /\.(webm|mp4|og[gv]|m4v|mov|avi|flv|wmv|mpg|mpeg|mkv|rm|rmvb|3gp|3g2|asf|swf|vob)$/i.test(url); +$.isImage = (url: string) => /\.(jpe?g|png|gif|bmp|webp|svg|ico|tiff?)$/i.test(url); +$.isVideo = (url: string) => /\.(webm|mp4|og[gv]|m4v|mov|avi|flv|wmv|mpg|mpeg|mkv|rm|rmvb|3gp|3g2|asf|swf|vob)$/i.test(url); $.engine = (function () { if (/Edge\//.test(navigator.userAgent)) { return 'edge'; } @@ -582,9 +598,9 @@ $.engine = (function () { $.hasStorage = (function () { try { - if (localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true') { return true; } - localStorage.setItem(g.NAMESPACE + 'hasStorage', 'true'); - return localStorage.getItem(g.NAMESPACE + 'hasStorage') === 'true'; + localStorage.setItem('test', 'test'); + localStorage.removeItem('test'); + return true; } catch (error) { return false; } @@ -630,8 +646,8 @@ if (platform === 'crx') { } } }); - $.sync = (key, cb) => $.syncing[key] = cb; - $.forceSync = function () { }; + $.sync = (key: string, cb: () => void) => $.syncing[key] = cb; + $.forceSync = function (): void { }; $.crxWorking = function () { try { @@ -649,6 +665,7 @@ if (platform === 'crx') { return false; }; + $.get = $.oneItemSugar(function (data, cb) { if (!$.crxWorking()) { return; } const results = {}; @@ -674,9 +691,8 @@ if (platform === 'crx') { } results[area] = result; if (results.local && results.sync) { - $.extend(data, results.sync); - $.extend(data, results.local); - return cb(data); + for (key in results.local) { var val = results.local[key]; if (val != null) { results.sync[key] = val; } } + cb(results.sync); } }); }; @@ -741,7 +757,7 @@ if (platform === 'crx') { }); }; - var setSync = debounce(SECOND, () => setArea('sync')); + var setSync = debounce(SECOND, () => setArea('sync', () => $.forceSync())); $.set = $.oneItemSugar(function (data, cb) { if (!$.crxWorking()) { return; } @@ -839,7 +855,11 @@ if (platform === 'crx') { }); }); - $.clear = cb => GM.listValues().then(keys => $.delete(keys.map(key => key.replace(g.NAMESPACE, '')), cb)).catch(() => $.delete(Object.keys(Conf).concat(['previousversion', 'QR Size', 'QR.persona']), cb)); + $.clear = async function (cb: () => void) { + return GM.listValues().then(function (keys) { + return $.delete(keys.map(key => key.slice(g.NAMESPACE.length)), cb); + }); + }; } else { if (typeof GM_deleteValue === 'undefined' || GM_deleteValue === null) { @@ -902,12 +922,19 @@ if (platform === 'crx') { } if (typeof GM_addValueChangeListener !== 'undefined' && GM_addValueChangeListener !== null) { - $.sync = (key, cb) => $.syncing[key] = GM_addValueChangeListener(g.NAMESPACE + key, function (key2, oldValue, newValue, remote) { - if (remote) { - if (newValue !== undefined) { newValue = dict.json(newValue); } - return cb(newValue, key); - } - }); + $.sync = function (key, cb) { + key = g.NAMESPACE + key; + $.syncing[key] = cb; + return GM_addValueChangeListener(key, function (name, oldVal, newVal) { + if (newVal != null) { + if (newVal === oldVal) { return; } + return cb(dict.json(newVal), name); + } + }); + }; + $.forceSync = function () { return GM.getValue(g.NAMESPACE + 'forceSync', 0).then(function (val) { + return GM.setValue(g.NAMESPACE + 'forceSync', val + 1); + }); }; $.forceSync = function () { }; } else if ((typeof GM_deleteValue !== 'undefined' && GM_deleteValue !== null) || $.hasStorage) { $.sync = function (key, cb) { @@ -945,16 +972,12 @@ if (platform === 'crx') { $.forceSync = function () { }; } - $.delete = function (keys) { - if (!(keys instanceof Array)) { - keys = [keys]; - } - for (var key of keys) { - $.deleteValue(g.NAMESPACE + key); - } + $.delete = function (keys, cb) { + if (keys.length === 0) { return cb?.(); } + return Promise.all(keys.map(key => $.deleteValue(g.NAMESPACE + key))).then(cb); }; - $.get = $.oneItemSugar((items, cb) => $.queueTask($.getSync, items, cb)); + $.get = $.oneItemSugar((items, cb) => $.queueTask(() => $.getSync(items, cb))); $.getSync = function (items, cb) { for (var key in items) { @@ -963,7 +986,6 @@ if (platform === 'crx') { try { items[key] = dict.json(val2); } catch (err) { - // XXX https://github.com/ccd0/4chan-x/issues/2218 if (!/^(?:undefined)*$/.test(val2)) { throw err; } @@ -987,14 +1009,15 @@ if (platform === 'crx') { $.clear = function (cb) { // XXX https://github.com/greasemonkey/greasemonkey/issues/2033 // Also support case where GM_listValues is not defined. - $.delete(Object.keys(Conf)); - $.delete(['previousversion', 'QR Size', 'QR.persona']); + $.delete(Object.keys(Conf), cb); + $.delete(Object.keys(Conf), cb); try { - $.delete($.listValues().map(key => key.replace(g.NAMESPACE, ''))); + //delete(keys, cb) + $.delete($.listValues(), cb); } catch (error) { } return cb?.(); }; } } -export default $; +export default $; \ No newline at end of file diff --git a/src/types/$.d.ts b/src/types/$.d.ts index 8ed4540..82fe3c6 100644 --- a/src/types/$.d.ts +++ b/src/types/$.d.ts @@ -1,3 +1,5 @@ +import SimpleDict from "../classes/SimpleDict"; + export interface ElementProperties { [key: string]: any; } @@ -10,4 +12,34 @@ interface AjaxPageOptions { onprogress?: (this: XMLHttpRequest, ev: ProgressEvent) => void; form?: FormData; headers?: Record; -} \ No newline at end of file +} +export interface LastModified { + [bucket: string]: { [url: string]: string | undefined }; +} +export interface WhenModifiedOptions { + timeout?: number; + ajax?: (url: string, settings?: AjaxPageOptions) => Promise; +} + + declare global { + interface JQueryStatic { + engine?: string; + lastModified: LastModified; + whenModified: ( + url: string, + bucket: string, + cb: (this: JQueryXHR) => void, + options?: WhenModifiedOptions + ) => JQueryXHR; + } + } + export type Dict = { [key: string]: any }; + export interface SyncObject { + setValue: (key: string, val: any) => void; + deleteValue: (key: string) => void; + oldValue?: Dict; + syncing?: Dict; + hasStorage?: boolean; + cantSync?: boolean; + cantSet?: boolean; + } \ No newline at end of file