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})();