webxdc.js

  1// Based on GPLv3 code from deltachat-android
  2// https://github.com/deltachat/deltachat-android/blob/master/res/raw/webxdc.js
  3
  4window.webxdc = (() => {
  5	let setUpdateListenerPromise = null
  6	var update_listener = () => {};
  7	var last_serial = 0;
  8	var realtime_listener = (data) => {};
  9
 10	window.__webxdcUpdate = () => {
 11		var updates = JSON.parse(InternalJSApi.getStatusUpdates(last_serial));
 12		updates.forEach((update) => {
 13				update_listener(update);
 14				last_serial = update.serial;
 15		});
 16		if (setUpdateListenerPromise) {
 17			setUpdateListenerPromise();
 18			setUpdateListenerPromise = null;
 19		}
 20	};
 21
 22	window.__webxdcRealtimeData = (data) => {
 23		realtime_listener(Uint8Array.from(atob(data), c => c.charCodeAt(0)));
 24	};
 25
 26	return {
 27		selfAddr: InternalJSApi.selfAddr(),
 28
 29		selfName: InternalJSApi.selfName(),
 30
 31		setUpdateListener: (cb, serial) => {
 32				last_serial = typeof serial === "undefined" ? 0 : parseInt(serial);
 33				update_listener = cb;
 34				var promise = new Promise((res, _rej) => {
 35					setUpdateListenerPromise = res;
 36				});
 37				window.__webxdcUpdate();
 38				return promise;
 39		},
 40
 41		sendUpdate: (payload, descr) => {
 42			var serialized = JSON.stringify(payload);
 43			if (serialized.length > 128 * 1024) {
 44				throw new Error("sendUpdate() payload too large: " + serialized.length + " bytes (max 128 KB)");
 45			}
 46			InternalJSApi.sendStatusUpdate(serialized, descr);
 47		},
 48
 49		importFiles: (filters) => {
 50			var element = document.createElement("input");
 51			element.type = "file";
 52			element.accept = [
 53					...(filters.extensions || []),
 54					...(filters.mimeTypes || []),
 55			].join(",");
 56			element.multiple = filters.multiple || false;
 57			const promise = new Promise((resolve, _reject) => {
 58					element.onchange = (_ev) => {
 59							const files = Array.from(element.files || []);
 60							document.body.removeChild(element);
 61							resolve(files);
 62					};
 63			});
 64			element.style.display = "none";
 65			document.body.appendChild(element);
 66			element.click();
 67			return promise;
 68		},
 69
 70		sendToChat: async (message) => {
 71			const data = {};
 72			if (!message.file && !message.text) {
 73				return Promise.reject("sendToChat() error: file or text missing");
 74			}
 75			const blobToBase64 = (file) => {
 76				const dataStart = ";base64,";
 77				return new Promise((resolve, reject) => {
 78					const reader = new FileReader();
 79					reader.readAsDataURL(file);
 80					reader.onload = () => {
 81						let data = reader.result;
 82						resolve(data.slice(data.indexOf(dataStart) + dataStart.length));
 83					};
 84					reader.onerror = () => reject(reader.error);
 85				});
 86			};
 87			if (message.text) {
 88				data.text = message.text;
 89			}
 90
 91			if (message.file) {
 92				let base64content;
 93				if (!message.file.name) {
 94					return Promise.reject("sendToChat() error: file name missing");
 95				}
 96				if (
 97					Object.keys(message.file).filter((key) =>
 98						["blob", "base64", "plainText"].includes(key)
 99					).length > 1
100				) {
101					return Promise.reject("sendToChat() error: only one of blob, base64 or plainText allowed");
102				}
103
104				if (message.file.blob instanceof Blob) {
105					base64content = await blobToBase64(message.file.blob);
106				} else if (typeof message.file.base64 === "string") {
107					base64content = message.file.base64;
108				} else if (typeof message.file.plainText === "string") {
109					base64content = await blobToBase64(
110						new Blob([message.file.plainText])
111					);
112				} else {
113					return Promise.reject("sendToChat() error: none of blob, base64 or plainText set correctly");
114				}
115				data.base64 = base64content;
116				data.name = message.file.name;
117			}
118
119			const errorMsg = InternalJSApi.sendToChat(JSON.stringify(data));
120			if (errorMsg) {
121				return Promise.reject(errorMsg);
122			}
123		},
124
125		joinRealtimeChannel: () => {
126			return {
127				leave: () => {},
128				send: (data) => {
129					if (!(data instanceof Uint8Array)) {
130						throw new Error('realtime listener data must be a Uint8Array')
131					}
132					InternalJSApi.sendRealtime(data);
133				},
134				setListener: (listener) => {
135					realtime_listener = listener;
136				}
137			};
138		},
139
140	};
141})();