diff --git a/dev.html b/dev.html
index 19eeb4bd5..180fd708a 100644
--- a/dev.html
+++ b/dev.html
@@ -40,8 +40,8 @@
muc_show_logs_before_join: true,
notify_all_room_messages: ['discuss@conference.conversejs.org'],
view_mode: 'fullscreen',
- websocket_url: 'wss://conversejs.org/xmpp-websocket',
- // websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
+ // websocket_url: 'wss://conversejs.org/xmpp-websocket',
+ websocket_url: 'ws://chat.example.org:5380/xmpp-websocket',
whitelisted_plugins: ['converse-debug'],
// connection_options: { worker: '/dist/shared-connection-worker.js' }
});
diff --git a/package-lock.json b/package-lock.json
index e02a08a47..bcc78b0b8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -20,6 +20,7 @@
"dayjs": "^1.11.8",
"dompurify": "^2.3.1",
"favico.js-slevomat": "^0.3.11",
+ "gifuct-js": "^2.1.2",
"jed": "1.1.1",
"lit": "^2.4.0",
"localforage-webextensionstorage-driver": "^3.0.0",
@@ -5612,6 +5613,14 @@
"safe-buffer": "^5.1.1"
}
},
+ "node_modules/gifuct-js": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/gifuct-js/-/gifuct-js-2.1.2.tgz",
+ "integrity": "sha512-rI2asw77u0mGgwhV3qA+OEgYqaDn5UNqgs+Bx0FGwSpuqfYn+Ir6RQY5ENNQ8SbIiG/m5gVa7CD5RriO4f4Lsg==",
+ "dependencies": {
+ "js-binary-schema-parser": "^2.0.3"
+ }
+ },
"node_modules/glob": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
@@ -6632,6 +6641,11 @@
"integrity": "sha512-umpJ0/k8X0MvD1ds0P9SfowREz2LenHsQaxSohMZ5OMNEU2r0tf8pdeEFTHMFxWVxKNyU9rTtK3CWzUCTKJUeQ==",
"peer": true
},
+ "node_modules/js-binary-schema-parser": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/js-binary-schema-parser/-/js-binary-schema-parser-2.0.3.tgz",
+ "integrity": "sha512-xezGJmOb4lk/M1ZZLTR/jaBHQ4gG/lqQnJqdIv4721DMggsa1bDVlHXNeHYogaIEHD9vCRv0fcL4hMA+Coarkg=="
+ },
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
diff --git a/package.json b/package.json
index 58f4dfa80..f9948883b 100644
--- a/package.json
+++ b/package.json
@@ -119,6 +119,7 @@
"dayjs": "^1.11.8",
"dompurify": "^2.3.1",
"favico.js-slevomat": "^0.3.11",
+ "gifuct-js": "^2.1.2",
"jed": "1.1.1",
"lit": "^2.4.0",
"localforage-webextensionstorage-driver": "^3.0.0",
diff --git a/src/shared/components/gif.js b/src/shared/components/gif.js
index 4df4c9bd2..0b4437af8 100644
--- a/src/shared/components/gif.js
+++ b/src/shared/components/gif.js
@@ -28,6 +28,7 @@ export default class ConverseGIFElement extends CustomElement {
constructor () {
super();
+ this.src = null;
this.autoplay = false;
this.noloop = false;
this.fallback = 'url';
@@ -90,8 +91,6 @@ export default class ConverseGIFElement extends CustomElement {
onControlsClicked (ev) {
ev.preventDefault();
-
-
if (this.supergif.playing) {
this.supergif.pause();
} else if (this.supergif.frames.length > 0) {
diff --git a/src/shared/components/image.js b/src/shared/components/image.js
index 29d6bdbc5..6d6cc9154 100644
--- a/src/shared/components/image.js
+++ b/src/shared/components/image.js
@@ -16,6 +16,14 @@ export default class Image extends CustomElement {
}
}
+ constructor () {
+ super();
+ this.src = null;
+ this.href = null;
+ this.onImgClick = null;
+ this.onImgLoad = null;
+ }
+
render () {
if (isGIFURL(this.src) && shouldRenderMediaFromURL(this.src, 'image')) {
return tplGif(filterQueryParamsFromURL(this.src), true);
diff --git a/src/shared/gif/index.js b/src/shared/gif/index.js
index abfa05f74..55d8d32cf 100644
--- a/src/shared/gif/index.js
+++ b/src/shared/gif/index.js
@@ -1,22 +1,10 @@
-/**
- * @copyright Shachaf Ben-Kiki, JC Brand
- * @description
- * Started as a fork of Shachaf Ben-Kiki's jsgif library
- * https://github.com/shachaf/jsgif
- * @license MIT License
- */
-import Stream from './stream.js';
import { getOpenPromise } from '@converse/openpromise';
-import { parseGIF } from './utils.js';
-
-const DELAY_FACTOR = 10;
-
+import { parseGIF, decompressFrames } from 'gifuct-js';
export default class ConverseGif {
-
/**
* Creates a new ConverseGif instance
- * @param { HTMLElement } el
+ * @param { import('lit').LitElement } el
* @param { Object } [options]
* @param { Number } [options.width] - The width, in pixels, of the canvas
* @param { Number } [options.height] - The height, in pixels, of the canvas
@@ -30,7 +18,8 @@ export default class ConverseGif {
* @param { Number } [options.progress_bar_height=5]
*/
constructor (el, opts) {
- this.options = Object.assign({
+ this.options = Object.assign(
+ {
width: null,
height: null,
autoplay: true,
@@ -38,7 +27,7 @@ export default class ConverseGif {
show_progress_bar: true,
progress_bg_color: 'rgba(0,0,0,0.4)',
progress_color: 'rgba(255,0,22,.8)',
- progress_bar_height: 5
+ progress_bar_height: 5,
},
opts
);
@@ -51,21 +40,15 @@ export default class ConverseGif {
this.offscreenCanvas = document.createElement('canvas');
this.ctx_scaled = false;
- this.disposal_method = null;
- this.disposal_restore_from_idx = null;
- this.frame = null;
- this.frame_offsets = []; // elements have .x and .y properties
this.frames = [];
- this.last_disposal_method = null;
- this.last_img = null;
this.load_error = null;
this.playing = this.options.autoplay;
- this.transparency = null;
- this.frame_delay = null;
this.frame_idx = 0;
this.iteration_count = 0;
this.start = null;
+ this.hovering = null;
+ this.frameImageData = null;
this.initialize();
}
@@ -75,7 +58,7 @@ export default class ConverseGif {
this.setSizes(this.options.width, this.options.height);
}
const data = await this.fetchGIF(this.gif_el.src);
- requestAnimationFrame(() => this.startParsing(data));
+ requestAnimationFrame(() => this.handleGIFResponse(data));
}
initPlayer () {
@@ -90,7 +73,7 @@ export default class ConverseGif {
this.putFrame(this.frame_idx);
if (this.options.autoplay) {
- const delay = (this.frames[this.frame_idx]?.delay ?? 0) * DELAY_FACTOR;
+ const delay = (this.frames[this.frame_idx]?.delay ?? 0);
setTimeout(() => this.play(), delay);
}
}
@@ -137,8 +120,8 @@ export default class ConverseGif {
* `putFrame(0)` needs to be called *before* this method, otherwise the
* animation will incorrectly start from frame #1 (this is done in `initPlayer`).
*
- * @param { DOMHighRestTimestamp } timestamp - The timestamp as returned by `requestAnimationFrame`
- * @param { DOMHighRestTimestamp } previous_timestamp - The timestamp from the previous iteration of this method.
+ * @param { DOMHighResTimeStamp } timestamp - The timestamp as returned by `requestAnimationFrame`
+ * @param { DOMHighResTimeStamp } previous_timestamp - The timestamp from the previous iteration of this method.
* We need this in order to calculate whether we have waited long enough to
* show the next frame.
* @param { Number } frame_delay - The delay (in 1/100th of a second)
@@ -148,10 +131,10 @@ export default class ConverseGif {
if (!this.playing) {
return;
}
- if ((timestamp - previous_timestamp) < frame_delay) {
+ if (timestamp - previous_timestamp < frame_delay) {
this.hovering ? this.drawPauseIcon() : this.putFrame(this.frame_idx);
// We need to wait longer
- requestAnimationFrame(ts => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
+ requestAnimationFrame((ts) => this.onAnimationFrame(ts, previous_timestamp, frame_delay));
return;
}
const next_frame = this.getNextFrameNo();
@@ -160,8 +143,8 @@ export default class ConverseGif {
}
this.frame_idx = next_frame;
this.putFrame(this.frame_idx);
- const delay = (this.frames[this.frame_idx]?.delay || 8) * DELAY_FACTOR;
- requestAnimationFrame(ts => this.onAnimationFrame(ts, timestamp, delay));
+ const delay = (this.frames[this.frame_idx]?.delay || 8);
+ requestAnimationFrame((ts) => this.onAnimationFrame(ts, timestamp, delay));
}
setSizes (w, h) {
@@ -175,19 +158,6 @@ export default class ConverseGif {
this.offscreenCanvas.getContext('2d').setTransform(1, 0, 0, 1, 0, 0);
}
- setFrameOffset (frame, offset) {
- if (!this.frame_offsets[frame]) {
- this.frame_offsets[frame] = offset;
- return;
- }
- if (typeof offset.x !== 'undefined') {
- this.frame_offsets[frame].x = offset.x;
- }
- if (typeof offset.y !== 'undefined') {
- this.frame_offsets[frame].y = offset.y;
- }
- }
-
doShowProgress (pos, length, draw) {
if (draw && this.options.show_progress_bar) {
let height = this.options.progress_bar_height;
@@ -207,32 +177,20 @@ export default class ConverseGif {
/**
* Starts parsing the GIF stream data by calling `parseGIF` and passing in
* a map of handler functions.
- * @param { String } data - The GIF file data, as returned by the server
+ * @param {ArrayBuffer} data - The GIF file data, as returned by the server
*/
- startParsing (data) {
- const stream = new Stream(data);
- /**
- * @typedef { Object } GIFParserHandlers
- * A map of callback functions passed `parseGIF`. These functions are
- * called as various parts of the GIF file format are parsed.
- * @property { Function } hdr - Callback to handle the GIF header data
- * @property { Function } gce - Callback to handle the GIF Graphic Control Extension data
- * @property { Function } com - Callback to handle the comment extension block
- * @property { Function } img - Callback to handle image data
- * @property { Function } eof - Callback once the end of file has been reached
- */
- const handler = {
- 'hdr': this.withProgress(stream, header => this.handleHeader(header)),
- 'gce': this.withProgress(stream, gce => this.handleGCE(gce)),
- 'com': this.withProgress(stream, ),
- 'img': this.withProgress(stream, img => this.doImg(img), true),
- 'eof': () => this.handleEOF(stream)
- };
+ handleGIFResponse (data) {
try {
- parseGIF(stream, handler);
+ const gif = parseGIF(data);
+ this.hdr = gif.header;
+ this.lsd = gif.lsd;
+ this.setSizes(this.options.width ?? this.lsd.width, this.options.height ?? this.lsd.height);
+ this.frames = decompressFrames(gif, true);
} catch (err) {
- this.showError('parse');
+ this.showError();
}
+ this.initPlayer();
+ !this.options.autoplay && this.drawPlayIcon();
}
drawError () {
@@ -240,23 +198,23 @@ export default class ConverseGif {
this.ctx.fillRect(
0,
0,
- this.options.width ? this.options.width : this.hdr.width,
- this.options.height ? this.options.height : this.hdr.height
+ this.options.width ? this.options.width : this.lsd.width,
+ this.options.height ? this.options.height : this.lsd.height
);
this.ctx.strokeStyle = 'red';
this.ctx.lineWidth = 3;
this.ctx.moveTo(0, 0);
this.ctx.lineTo(
- this.options.width ? this.options.width : this.hdr.width,
- this.options.height ? this.options.height : this.hdr.height
+ this.options.width ? this.options.width : this.lsd.width,
+ this.options.height ? this.options.height : this.lsd.height
);
- this.ctx.moveTo(0, this.options.height ? this.options.height : this.hdr.height);
- this.ctx.lineTo(this.options.width ? this.options.width : this.hdr.width, 0);
+ this.ctx.moveTo(0, this.options.height ? this.options.height : this.lsd.height);
+ this.ctx.lineTo(this.options.width ? this.options.width : this.lsd.width, 0);
this.ctx.stroke();
}
- showError (errtype) {
- this.load_error = errtype;
+ showError () {
+ this.load_error = true;
this.hdr = {
width: this.gif_el.width,
height: this.gif_el.height,
@@ -266,164 +224,49 @@ export default class ConverseGif {
this.el.requestUpdate();
}
- handleHeader (header) {
- this.hdr = header;
- this.setSizes(
- this.options.width ?? this.hdr.width,
- this.options.height ?? this.hdr.height
- );
- }
-
- /**
- * Handler for GIF Graphic Control Extension (GCE) data
- */
- handleGCE (gce) {
- this.pushFrame();
- this.clear();
- this.frame_delay = gce.delayTime;
- this.transparency = gce.transparencyGiven ? gce.transparencyIndex : null;
- this.disposal_method = gce.disposalMethod;
- }
-
- /**
- * Handler for when the end of the GIF's file has been reached
- */
- handleEOF (stream) {
- this.pushFrame();
- this.doDecodeProgress(stream, false);
- this.initPlayer();
- !this.options.autoplay && this.drawPlayIcon();
- }
-
- pushFrame () {
- if (!this.frame) return;
- this.frames.push({
- data: this.frame.getImageData(0, 0, this.hdr.width, this.hdr.height),
- delay: this.frame_delay
- });
- this.frame_offsets.push({ x: 0, y: 0 });
- }
-
- doImg (img) {
- this.frame = this.frame || this.offscreenCanvas.getContext('2d');
- const currIdx = this.frames.length;
-
- //ct = color table, gct = global color table
- const ct = img.lctFlag ? img.lct : this.hdr.gct; // TODO: What if neither exists?
-
- /*
- * Disposal method indicates the way in which the graphic is to
- * be treated after being displayed.
- *
- * Values : 0 - No disposal specified. The decoder is
- * not required to take any action.
- * 1 - Do not dispose. The graphic is to be left
- * in place.
- * 2 - Restore to background color. The area used by the
- * graphic must be restored to the background color.
- * 3 - Restore to previous. The decoder is required to
- * restore the area overwritten by the graphic with
- * what was there prior to rendering the graphic.
- *
- * Importantly, "previous" means the frame state
- * after the last disposal of method 0, 1, or 2.
- */
- if (currIdx > 0) {
- if (this.last_disposal_method === 3) {
- // Restore to previous
- // If we disposed every frame including first frame up to this point, then we have
- // no composited frame to restore to. In this case, restore to background instead.
- if (this.disposal_restore_from_idx !== null) {
- this.frame.putImageData(this.frames[this.disposal_restore_from_idx].data, 0, 0);
- } else {
- this.frame.clearRect(
- this.last_img.leftPos,
- this.last_img.topPos,
- this.last_img.width,
- this.last_img.height
- );
- }
- } else {
- this.disposal_restore_from_idx = currIdx - 1;
- }
-
- if (this.last_disposal_method === 2) {
- // Restore to background color
- // Browser implementations historically restore to transparent; we do the same.
- // http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
- this.frame.clearRect(
- this.last_img.leftPos,
- this.last_img.topPos,
- this.last_img.width,
- this.last_img.height
- );
- }
- }
- // else, Undefined/Do not dispose.
- // frame contains final pixel data from the last frame; do nothing
-
- //Get existing pixels for img region after applying disposal method
- const imgData = this.frame.getImageData(img.leftPos, img.topPos, img.width, img.height);
-
- //apply color table colors
- img.pixels.forEach((pixel, i) => {
- // imgData.data === [R,G,B,A,R,G,B,A,...]
- if (pixel !== this.transparency) {
- imgData.data[i * 4 + 0] = ct[pixel][0];
- imgData.data[i * 4 + 1] = ct[pixel][1];
- imgData.data[i * 4 + 2] = ct[pixel][2];
- imgData.data[i * 4 + 3] = 255; // Opaque.
- }
- });
-
- this.frame.putImageData(imgData, img.leftPos, img.topPos);
-
- if (!this.ctx_scaled) {
- this.ctx.scale(this.getCanvasScale(), this.getCanvasScale());
- this.ctx_scaled = true;
- }
-
- if (!this.last_img) {
- // This is the first received image, so we draw it
- this.ctx.drawImage(this.offscreenCanvas, 0, 0);
- }
- this.last_img = img;
- }
-
/**
* Draws a gif frame at a specific index inside the canvas.
- * @param { Number } i - The frame index
+ * @param {number|string} i - The frame index
*/
- putFrame (i, show_pause_on_hover=true) {
- if (!this.frames.length) return
+ putFrame (i, show_pause_on_hover = true) {
+ if (!this.frames.length) return;
- i = parseInt(i, 10);
+ i = parseInt(i.toString(), 10);
if (i > this.frames.length - 1 || i < 0) {
i = 0;
}
- const offset = this.frame_offsets[i];
- this.offscreenCanvas.getContext('2d').putImageData(this.frames[i].data, offset.x, offset.y);
+
+ const frame = this.frames[i];
+ const dims = frame.dims;
+
+ if (
+ !this.frameImageData ||
+ dims.width != this.frameImageData.width ||
+ dims.height != this.frameImageData.height
+ ) {
+ this.offscreenCanvas.width = dims.width;
+ this.offscreenCanvas.height = dims.height;
+ this.frameImageData = this.offscreenCanvas.getContext('2d').createImageData(dims.width, dims.height);
+ }
+
+ // set the patch data as an override
+ this.frameImageData.data.set(frame.patch);
+
+ this.offscreenCanvas.getContext('2d').putImageData(this.frameImageData, 0, 0);
this.ctx.globalCompositeOperation = 'copy';
- this.ctx.drawImage(this.offscreenCanvas, 0, 0);
+ this.ctx.drawImage(this.offscreenCanvas, dims.left, dims.top);
if (show_pause_on_hover && this.hovering) {
this.drawPauseIcon();
}
}
- clear () {
- this.transparency = null;
- this.last_disposal_method = this.disposal_method;
- this.disposal_method = null;
- this.frame = null;
- }
-
/**
* Start playing the gif
*/
play () {
this.playing = true;
- requestAnimationFrame(ts => this.onAnimationFrame(ts, 0, 0));
+ requestAnimationFrame((ts) => this.onAnimationFrame(ts, 0, 0));
}
/**
@@ -431,7 +274,7 @@ export default class ConverseGif {
*/
pause () {
this.playing = false;
- requestAnimationFrame(() => this.drawPlayIcon())
+ requestAnimationFrame(() => this.drawPlayIcon());
}
drawPauseIcon () {
@@ -447,40 +290,32 @@ export default class ConverseGif {
this.ctx.fillStyle = 'rgb(0, 0, 0, 0.25)';
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
- const icon_size = this.canvas.height*0.1;
+ const icon_size = this.canvas.height * 0.1;
// Draw bars
- this.ctx.lineWidth = this.canvas.height*0.04;
+ this.ctx.lineWidth = this.canvas.height * 0.04;
this.ctx.beginPath();
- this.ctx.moveTo(this.canvas.width/2-icon_size/2, this.canvas.height/2-icon_size);
- this.ctx.lineTo(this.canvas.width/2-icon_size/2, this.canvas.height/2+icon_size);
+ this.ctx.moveTo(this.canvas.width / 2 - icon_size / 2, this.canvas.height / 2 - icon_size);
+ this.ctx.lineTo(this.canvas.width / 2 - icon_size / 2, this.canvas.height / 2 + icon_size);
this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
this.ctx.stroke();
this.ctx.beginPath();
- this.ctx.moveTo(this.canvas.width/2+icon_size/2, this.canvas.height/2-icon_size);
- this.ctx.lineTo(this.canvas.width/2+icon_size/2, this.canvas.height/2+icon_size);
+ this.ctx.moveTo(this.canvas.width / 2 + icon_size / 2, this.canvas.height / 2 - icon_size);
+ this.ctx.lineTo(this.canvas.width / 2 + icon_size / 2, this.canvas.height / 2 + icon_size);
this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
this.ctx.stroke();
// Draw circle
- this.ctx.lineWidth = this.canvas.height*0.02;
+ this.ctx.lineWidth = this.canvas.height * 0.02;
this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
this.ctx.beginPath();
- this.ctx.arc(
- this.canvas.width/2,
- this.canvas.height/2,
- icon_size*1.5,
- 0,
- 2*Math.PI
- );
+ this.ctx.arc(this.canvas.width / 2, this.canvas.height / 2, icon_size * 1.5, 0, 2 * Math.PI);
this.ctx.stroke();
}
drawPlayIcon () {
- if (this.playing) {
- return;
- }
+ if (this.playing) return;
// Clear the potential pause button by re-rendering the current frame
this.putFrame(this.frame_idx, false);
@@ -492,52 +327,28 @@ export default class ConverseGif {
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Draw triangle
- const triangle_size = this.canvas.height*0.1;
+ const triangle_size = this.canvas.height * 0.1;
const region = new Path2D();
- region.moveTo(this.canvas.width/2+triangle_size, this.canvas.height/2); // start at the pointy end
- region.lineTo(this.canvas.width/2-triangle_size/2, this.canvas.height/2+triangle_size);
- region.lineTo(this.canvas.width/2-triangle_size/2, this.canvas.height/2-triangle_size);
+ region.moveTo(this.canvas.width / 2 + triangle_size, this.canvas.height / 2); // start at the pointy end
+ region.lineTo(this.canvas.width / 2 - triangle_size / 2, this.canvas.height / 2 + triangle_size);
+ region.lineTo(this.canvas.width / 2 - triangle_size / 2, this.canvas.height / 2 - triangle_size);
region.closePath();
this.ctx.fillStyle = 'rgb(200, 200, 200, 0.75)';
this.ctx.fill(region);
// Draw circle
- const circle_size = triangle_size*1.5;
- this.ctx.lineWidth = this.canvas.height*0.02;
+ const circle_size = triangle_size * 1.5;
+ this.ctx.lineWidth = this.canvas.height * 0.02;
this.ctx.strokeStyle = 'rgb(200, 200, 200, 0.75)';
this.ctx.beginPath();
- this.ctx.arc(
- this.canvas.width/2,
- this.canvas.height/2,
- circle_size,
- 0,
- 2*Math.PI
- );
+ this.ctx.arc(this.canvas.width / 2, this.canvas.height / 2, circle_size, 0, 2 * Math.PI);
this.ctx.stroke();
}
- doDecodeProgress (stream, draw) {
- this.doShowProgress(stream.pos, stream.data.length, draw);
- }
-
- /**
- * @param{boolean=} draw Whether to draw progress bar or not;
- * this is not idempotent because of translucency.
- * Note that this means that the text will be unsynchronized
- * with the progress bar on non-frames;
- * but those are typically so small (GCE etc.) that it doesn't really matter
- */
- withProgress (stream, fn, draw) {
- return block => {
- fn?.(block);
- this.doDecodeProgress(stream, draw);
- };
- }
-
getCanvasScale () {
let scale;
- if (this.options.max_width && this.hdr && this.hdr.width > this.options.max_width) {
- scale = this.options.max_width / this.hdr.width;
+ if (this.options.max_width && this.hdr && this.lsd.width > this.options.max_width) {
+ scale = this.options.max_width / this.lsd.width;
} else {
scale = 1;
}
@@ -547,22 +358,24 @@ export default class ConverseGif {
/**
* Makes an HTTP request to fetch a GIF
* @param { String } url
- * @returns { Promise } Returns a promise which resolves with the response data.
+ * @returns { Promise } Returns a promise which resolves with the response data.
*/
fetchGIF (url) {
const promise = getOpenPromise();
const h = new XMLHttpRequest();
h.open('GET', url, true);
+ h.responseType = 'arraybuffer';
+
h?.overrideMimeType('text/plain; charset=x-user-defined');
h.onload = () => {
if (h.status != 200) {
- this.showError('xhr - response');
+ this.showError();
return promise.reject();
}
promise.resolve(h.response);
};
- h.onprogress = (e) => (e.lengthComputable && this.doShowProgress(e.loaded, e.total, true));
- h.onerror = () => this.showError('xhr');
+ h.onprogress = (e) => e.lengthComputable && this.doShowProgress(e.loaded, e.total, true);
+ h.onerror = () => this.showError();
h.send();
return promise;
}
diff --git a/src/shared/gif/utils.js b/src/shared/gif/utils.js
deleted file mode 100644
index 65f076155..000000000
--- a/src/shared/gif/utils.js
+++ /dev/null
@@ -1,319 +0,0 @@
-/**
- * @copyright Shachaf Ben-Kiki and the Converse.js contributors
- * @description
- * Started as a fork of Shachaf Ben-Kiki's jsgif library
- * https://github.com/shachaf/jsgif
- * @license MIT License
- */
-
-function bitsToNum (ba) {
- return ba.reduce(function (s, n) {
- return s * 2 + n;
- }, 0);
-}
-
-function byteToBitArr (bite) {
- const a = [];
- for (let i = 7; i >= 0; i--) {
- a.push( !! (bite & (1 << i)));
- }
- return a;
-}
-
-function lzwDecode (minCodeSize, data) {
- // TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
- let pos = 0; // Maybe this streaming thing should be merged with the Stream?
- function readCode (size) {
- let code = 0;
- for (let i = 0; i < size; i++) {
- if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
- code |= 1 << i;
- }
- pos++;
- }
- return code;
- }
-
- const output = [];
- const clearCode = 1 << minCodeSize;
- const eoiCode = clearCode + 1;
-
- let codeSize = minCodeSize + 1;
- let dict = [];
-
- const clear = function () {
- dict = [];
- codeSize = minCodeSize + 1;
- for (let i = 0; i < clearCode; i++) {
- dict[i] = [i];
- }
- dict[clearCode] = [];
- dict[eoiCode] = null;
- };
-
- let code = clearCode;
- let last;
- clear();
-
- while (true) { // eslint-disable-line no-constant-condition
- last = code;
- code = readCode(codeSize);
-
- if (code === clearCode) {
- clear();
- continue;
- }
- if (code === eoiCode) break;
-
- if (code < dict.length) {
- if (last !== clearCode) {
- dict.push(dict[last].concat(dict[code][0]));
- }
- }
- else {
- if (code !== dict.length) throw new Error('Invalid LZW code.');
- dict.push(dict[last].concat(dict[last][0]));
- }
- output.push.apply(output, dict[code]);
-
- if (dict.length === (1 << codeSize) && codeSize < 12) {
- // If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
- codeSize++;
- }
- }
- // I don't know if this is technically an error, but some GIFs do it.
- //if (Math.ceil(pos / 8) !== data.length) throw new Error('Extraneous LZW bytes.');
- return output;
-}
-
-
-function readSubBlocks (st) {
- let size, data;
- data = '';
- do {
- size = st.readByte();
- data += st.read(size);
- } while (size !== 0);
- return data;
-}
-
-/**
- * Parses GIF image color table information
- * @param { Stream } st
- * @param { Number } entries
- */
-function parseCT (st, entries) { // Each entry is 3 bytes, for RGB.
- const ct = [];
- for (let i = 0; i < entries; i++) {
- ct.push(st.readBytes(3));
- }
- return ct;
-}
-
-/**
- * Parses GIF image information
- * @param { Stream } st
- * @param { ByteStream } img
- * @param { Function } [callback]
- */
-function parseImg (st, img, callback) {
- function deinterlace (pixels, width) {
- // Of course this defeats the purpose of interlacing. And it's *probably*
- // the least efficient way it's ever been implemented. But nevertheless...
- const newPixels = new Array(pixels.length);
- const rows = pixels.length / width;
- function cpRow (toRow, fromRow) {
- const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
- newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
- }
-
- // See appendix E.
- const offsets = [0, 4, 2, 1];
- const steps = [8, 8, 4, 2];
- let fromRow = 0;
- for (let pass = 0; pass < 4; pass++) {
- for (let toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
- cpRow(toRow, fromRow)
- fromRow++;
- }
- }
- return newPixels;
- }
-
- img.leftPos = st.readUnsigned();
- img.topPos = st.readUnsigned();
- img.width = st.readUnsigned();
- img.height = st.readUnsigned();
-
- const bits = byteToBitArr(st.readByte());
- img.lctFlag = bits.shift();
- img.interlaced = bits.shift();
- img.sorted = bits.shift();
- img.reserved = bits.splice(0, 2);
- img.lctSize = bitsToNum(bits.splice(0, 3));
-
- if (img.lctFlag) {
- img.lct = parseCT(st, 1 << (img.lctSize + 1));
- }
- img.lzwMinCodeSize = st.readByte();
-
- const lzwData = readSubBlocks(st);
- img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
-
- if (img.interlaced) { // Move
- img.pixels = deinterlace(img.pixels, img.width);
- }
- callback?.(img);
-}
-
-/**
- * Parses GIF header information
- * @param { Stream } st
- * @param { Function } [callback]
- */
-function parseHeader (st, callback) {
- const hdr = {};
- hdr.sig = st.read(3);
- hdr.ver = st.read(3);
- if (hdr.sig !== 'GIF') {
- throw new Error('Not a GIF file.');
- }
- hdr.width = st.readUnsigned();
- hdr.height = st.readUnsigned();
-
- const bits = byteToBitArr(st.readByte());
- hdr.gctFlag = bits.shift();
- hdr.colorRes = bitsToNum(bits.splice(0, 3));
- hdr.sorted = bits.shift();
- hdr.gctSize = bitsToNum(bits.splice(0, 3));
-
- hdr.bgColor = st.readByte();
- hdr.pixelAspectRatio = st.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
- if (hdr.gctFlag) {
- hdr.gct = parseCT(st, 1 << (hdr.gctSize + 1));
- }
- callback?.(hdr);
-}
-
-function parseExt (st, block, handler) {
-
- function parseGCExt (block) {
- st.readByte(); // blocksize, always 4
- const bits = byteToBitArr(st.readByte());
- block.reserved = bits.splice(0, 3); // Reserved; should be 000.
- block.disposalMethod = bitsToNum(bits.splice(0, 3));
- block.userInput = bits.shift();
- block.transparencyGiven = bits.shift();
- block.delayTime = st.readUnsigned();
- block.transparencyIndex = st.readByte();
- block.terminator = st.readByte();
- handler?.gce(block);
- }
-
- function parseComExt (block) {
- block.comment = readSubBlocks(st);
- handler.com && handler.com(block);
- }
-
- function parsePTExt (block) {
- // No one *ever* uses this. If you use it, deal with parsing it yourself.
- st.readByte(); // blocksize, always 12
- block.ptHeader = st.readBytes(12);
- block.ptData = readSubBlocks(st);
- handler.pte && handler.pte(block);
- }
-
- function parseAppExt (block) {
- function parseNetscapeExt (block) {
- st.readByte(); // blocksize, always 3
- block.unknown = st.readByte(); // ??? Always 1? What is this?
- block.iterations = st.readUnsigned();
- block.terminator = st.readByte();
- handler.app && handler.app.NETSCAPE && handler.app.NETSCAPE(block);
- }
-
- function parseUnknownAppExt (block) {
- block.appData = readSubBlocks(st);
- // FIXME: This won't work if a handler wants to match on any identifier.
- handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
- }
-
- st.readByte(); // blocksize, always 11
- block.identifier = st.read(8);
- block.authCode = st.read(3);
- switch (block.identifier) {
- case 'NETSCAPE':
- parseNetscapeExt(block);
- break;
- default:
- parseUnknownAppExt(block);
- break;
- }
- }
-
- function parseUnknownExt (block) {
- block.data = readSubBlocks(st);
- handler.unknown && handler.unknown(block);
- }
-
- block.label = st.readByte();
- switch (block.label) {
- case 0xF9:
- block.extType = 'gce';
- parseGCExt(block);
- break;
- case 0xFE:
- block.extType = 'com';
- parseComExt(block);
- break;
- case 0x01:
- block.extType = 'pte';
- parsePTExt(block);
- break;
- case 0xFF:
- block.extType = 'app';
- parseAppExt(block);
- break;
- default:
- block.extType = 'unknown';
- parseUnknownExt(block);
- break;
- }
-}
-
-/**
- * @param { Stream } st
- * @param { GIFParserHandlers } handler
- */
-function parseBlock (st, handler) {
- const block = {}
- block.sentinel = st.readByte();
- switch (String.fromCharCode(block.sentinel)) { // For ease of matching
- case '!':
- block.type = 'ext';
- parseExt(st, block, handler);
- break;
- case ',':
- block.type = 'img';
- parseImg(st, block, handler?.img);
- break;
- case ';':
- block.type = 'eof';
- handler?.eof(block);
- break;
- default:
- throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
- }
- if (block.type !== 'eof') setTimeout(() => parseBlock(st, handler), 0);
-}
-
-/**
- * Takes a Stream and parses it for GIF data, calling the relevant handler
- * methods on the passed in `handler` object.
- * @param { Stream } st
- * @param { GIFParserHandlers } handler
- */
-export function parseGIF (st, handler={}) {
- parseHeader(st, handler?.hdr);
- setTimeout(() => parseBlock(st, handler), 0);
-}
diff --git a/src/shared/rich-text.js b/src/shared/rich-text.js
index 019aa9bec..f52e094af 100644
--- a/src/shared/rich-text.js
+++ b/src/shared/rich-text.js
@@ -283,11 +283,6 @@ export class RichText extends String {
/**
* Parse the text and add template references for rendering the "rich" parts.
- *
- * @param { RichText } text
- * @param { Boolean } show_images - Should URLs of images be rendered as `` tags?
- * @param { Function } onImgLoad
- * @param { Function } onImgClick
**/
async addTemplates () {
/**
diff --git a/tsconfig.json b/tsconfig.json
index f89f3d91f..5404c2e3a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -11,6 +11,11 @@
"target": "es2016",
"module": "esnext",
+ "lib": [
+ "ES2020",
+ "dom"
+ ],
+
"allowJs": true,
"checkJs": true,