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