gamja-webpush.diff

· antonio's pastes · raw

expires: never

  1diff --git a/commands.js b/commands.js
  2index d881ec2..20db813 100644
  3--- a/commands.js
  4+++ b/commands.js
  5@@ -164,6 +164,13 @@ export default {
  6 			app.disconnect();
  7 		},
  8 	},
  9+	"clear-localstorage": {
 10+		description: "Clear application local storage",
 11+		execute: (app, args) => {
 12+			localStorage.clear();
 13+			window.location.reload();
 14+		},
 15+	},
 16 	"help": {
 17 		description: "Show help menu",
 18 		execute: (app, args) => {
 19diff --git a/components/app.js b/components/app.js
 20index 4788788..dd5643e 100644
 21--- a/components/app.js
 22+++ b/components/app.js
 23@@ -28,6 +28,21 @@ const baseConfig = {
 24 	server: {},
 25 };
 26 
 27+function urlBase64ToUint8Array(base64String) {
 28+	var padding = '='.repeat((4 - base64String.length % 4) % 4);
 29+	var base64 = (base64String + padding)
 30+		.replace(/\-/g, '+')
 31+		.replace(/_/g, '/');
 32+
 33+	var rawData = window.atob(base64);
 34+	var outputArray = new Uint8Array(rawData.length);
 35+
 36+	for (var i = 0; i < rawData.length; ++i) {
 37+		outputArray[i] = rawData.charCodeAt(i);
 38+	}
 39+	return outputArray;
 40+}
 41+
 42 const configPromise = fetch("./config.json")
 43 	.then((resp) => {
 44 		if (resp.ok) {
 45@@ -227,6 +242,7 @@ export default class App extends Component {
 46 		this.handleOpenSettingsClick = this.handleOpenSettingsClick.bind(this);
 47 		this.handleSettingsChange = this.handleSettingsChange.bind(this);
 48 		this.handleSettingsDisconnect = this.handleSettingsDisconnect.bind(this);
 49+		this.handleSettingsClearLocalStorage = this.handleSettingsClearLocalStorage.bind(this);
 50 		this.handleSwitchSubmit = this.handleSwitchSubmit.bind(this);
 51 		this.handleWindowFocus = this.handleWindowFocus.bind(this);
 52 
 53@@ -324,10 +340,6 @@ export default class App extends Component {
 54 			this.debug = true;
 55 		}
 56 
 57-		if (window.location.hash) {
 58-			autojoin = window.location.hash.split(",");
 59-		}
 60-
 61 		this.config = config;
 62 
 63 		if (!connectParams.nick && connectParams.autoconnect) {
 64@@ -367,8 +379,9 @@ export default class App extends Component {
 65 
 66 			connectParams.saslOauthBearer = saslOauthBearer;
 67 
 68-			if (saslOauthBearer.username && !connectParams.nick) {
 69+			if (saslOauthBearer.username) {
 70 				connectParams.nick = saslOauthBearer.username;
 71+				connectParams.username = saslOauthBearer.username;
 72 			}
 73 		}
 74 
 75@@ -386,6 +399,9 @@ export default class App extends Component {
 76 
 77 		if (connectParams.autoconnect) {
 78 			this.setState({ connectForm: false });
 79+			setTimeout(function() {
 80+				store.autoconnect.put(connectParams);
 81+			}, 500);
 82 			this.connect(connectParams);
 83 		}
 84 	}
 85@@ -790,6 +806,40 @@ export default class App extends Component {
 86 				if (errorID) {
 87 					this.dismissError(errorID);
 88 				}
 89+
 90+				function handlePush(data) {
 91+					const bouncerNetwork = data.message.tags.bouncerNetwork
 92+					const serverID = this.serverFromBouncerNetwork(bouncerNetwork);
 93+
 94+					let bufferName = data.message.params[0];
 95+					if (bufferName == client.nick) {
 96+						bufferName = data.message.prefix.name;
 97+					}
 98+
 99+					this.switchBuffer({ server: serverID, name: bufferName });
100+				}
101+				handlePush = handlePush.bind(this);
102+
103+				window.addEventListener("pushMessageReceived", (event) => {
104+					if (event.detail.message.tags.bouncerNetwork !== client.params.bouncerNetwork) {
105+						return;
106+					}
107+
108+					handlePush(event.detail);
109+				});
110+
111+				if (window.notificationData) {
112+					if (window.notificationData.message.tags.bouncerNetwork !== client.params.bouncerNetwork) {
113+						break;
114+					}
115+
116+					setTimeout(() => {
117+						if (window.notificationData) {
118+							handlePush(window.notificationData);
119+							window.notificationData = null;
120+						}
121+					}, 500);
122+				}
123 				break;
124 			}
125 		});
126@@ -981,9 +1031,50 @@ export default class App extends Component {
127 				affectedBuffers.push(prefix.name);
128 			}
129 			return affectedBuffers;
130+		case irc.RPL_ISUPPORT: {
131+			const params = msg.params;
132+			for (const p of params) {
133+				if (p.indexOf("VAPID") === 0) {
134+					const value = p.replace("VAPID=", "");
135+					const vapidPubKey = urlBase64ToUint8Array(value);
136+
137+					navigator.serviceWorker.ready.then((registration) => {
138+						return registration.pushManager.getSubscription()
139+							.then((subscription) => {
140+								if (subscription) {
141+									return subscription;
142+								}
143+								return registration.pushManager.subscribe({
144+									userVisibleOnly: true,
145+									applicationServerKey: vapidPubKey,
146+								});
147+							}).then((subscription) => {
148+								var filterCommands = [
149+									"PRIVMSG",
150+								];
151+
152+								if (client.params.bouncerNetwork == null) {
153+									filterCommands.push("NOTE");
154+								}
155+
156+								var data = subscription.toJSON();
157+								data.keys.filterCommands =filterCommands.join(",");
158+								var keysStr = irc.formatTags(data.keys);
159+
160+								client.send({
161+									command: "WEBPUSH",
162+									params: ["REGISTER", data.endpoint, keysStr],
163+								});
164+
165+								return [SERVER_BUFFER];
166+							});
167+					});
168+				}
169+			}
170+			return [];
171+		}
172 		case irc.RPL_YOURHOST:
173 		case irc.RPL_MYINFO:
174-		case irc.RPL_ISUPPORT:
175 		case irc.RPL_ENDOFMOTD:
176 		case irc.ERR_NOMOTD:
177 		case irc.RPL_AWAY:
178@@ -1913,6 +2004,11 @@ export default class App extends Component {
179 		this.disconnectAll();
180 	}
181 
182+	handleSettingsClearLocalStorage() {
183+		localStorage.clear();
184+		window.location.reload();
185+	}
186+
187 	handleSwitchSubmit(buf) {
188 		this.dismissDialog();
189 		if (buf) {
190@@ -2112,6 +2208,7 @@ export default class App extends Component {
191 						showProtocolHandler=${dialogData.showProtocolHandler}
192 						onChange=${this.handleSettingsChange}
193 						onDisconnect=${this.handleSettingsDisconnect}
194+						onClearLocalStorage=${this.handleSettingsClearLocalStorage}
195 						onClose=${this.dismissDialog}
196 					/>
197 				</>
198diff --git a/components/connect-form.js b/components/connect-form.js
199index 6eab2df..3358ad2 100644
200--- a/components/connect-form.js
201+++ b/components/connect-form.js
202@@ -7,7 +7,7 @@ export default class ConnectForm extends Component {
203 		pass: "",
204 		nick: "",
205 		password: "",
206-		rememberMe: false,
207+		rememberMe: true,
208 		username: "",
209 		realname: "",
210 		autojoin: true,
211diff --git a/components/settings-form.js b/components/settings-form.js
212index 31e045e..a09a084 100644
213--- a/components/settings-form.js
214+++ b/components/settings-form.js
215@@ -103,6 +103,9 @@ export default class SettingsForm extends Component {
216 				<button type="button" class="danger" onClick=${() => this.props.onDisconnect()}>
217 					Disconnect
218 				</button>
219+				<button type="button" class="danger" onClick=${() => this.props.onClearLocalStorage()}>
220+					Clear LocalStorage
221+				</button>
222 				<button>
223 					Close
224 				</button>
225diff --git a/lib/client.js b/lib/client.js
226index a1a969a..90e736e 100644
227--- a/lib/client.js
228+++ b/lib/client.js
229@@ -25,6 +25,7 @@ const permanentCaps = [
230 	"draft/read-marker",
231 
232 	"soju.im/bouncer-networks",
233+	"soju.im/webpush",
234 ];
235 
236 const RECONNECT_MIN_DELAY_MSEC = 10 * 1000; // 10s
237diff --git a/lib/index.js b/lib/index.js
238index 1b480b3..e214ff5 100644
239--- a/lib/index.js
240+++ b/lib/index.js
241@@ -6,3 +6,6 @@ export const html = htm.bind(h);
242 
243 import * as linkifyjs from "../node_modules/linkifyjs/dist/linkify.module.js";
244 export { linkifyjs };
245+
246+import * as idbkv from "../node_modules/idb-keyval/dist/index.js";
247+export { idbkv };
248diff --git a/main.js b/main.js
249index 2b73e7d..9f1f905 100644
250--- a/main.js
251+++ b/main.js
252@@ -1,4 +1,54 @@
253-import { html, render } from "./lib/index.js";
254+import { html, render, idbkv } from "./lib/index.js";
255 import App from "./components/app.js";
256 
257+(function () {
258+    let updated = false;
259+    let activated = false;
260+
261+    if (!'serviceWorker' in navigator) {
262+        return;
263+    }
264+
265+    navigator.serviceWorker.addEventListener('controllerchange', () => {
266+        updated = true;
267+        checkUpdate();
268+    });
269+
270+    navigator.serviceWorker.addEventListener("message", (event) => {
271+        const data = event.data;
272+
273+        window.dispatchEvent(new CustomEvent("pushMessageReceived", {
274+            detail: data,
275+        }));
276+    });
277+
278+    idbkv.get("notification").then((data) => {
279+        if (data) {
280+            window.notificationData = data;
281+        }
282+        return idbkv.del("notification");
283+    });
284+
285+    navigator.serviceWorker.register(
286+        new URL('service-worker.js', import.meta.url),
287+        {type: 'module'}
288+    ).then((registration) => {
289+        registration.addEventListener("updatefound", () => {
290+            const worker = registration.installing;
291+            worker.addEventListener('statechange', () => {
292+                if (worker.state === "activated") {
293+                    activated = true;
294+                    checkUpdate();
295+                }
296+            });
297+        });
298+    }).catch(console.error);
299+
300+    function checkUpdate() {
301+        if (activated && updated) {
302+            window.location.reload();
303+        }
304+    }
305+})();
306+
307 render(html`<${App}/>`, document.body);
308diff --git a/package-lock.json b/package-lock.json
309index 940446d..2179582 100644
310--- a/package-lock.json
311+++ b/package-lock.json
312@@ -7,6 +7,7 @@
313       "name": "gamja",
314       "dependencies": {
315         "htm": "^3.0.4",
316+        "idb-keyval": "^6.2.1",
317         "linkifyjs": "^3.0.2",
318         "preact": "^10.5.9"
319       },
320@@ -2663,6 +2664,11 @@
321         "entities": "^3.0.1"
322       }
323     },
324+    "node_modules/idb-keyval": {
325+      "version": "6.2.1",
326+      "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
327+      "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg=="
328+    },
329     "node_modules/import-fresh": {
330       "version": "3.3.0",
331       "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
332diff --git a/package.json b/package.json
333index 3628469..fe89f53 100644
334--- a/package.json
335+++ b/package.json
336@@ -3,6 +3,7 @@
337   "type": "module",
338   "dependencies": {
339     "htm": "^3.0.4",
340+    "idb-keyval": "^6.2.1",
341     "linkifyjs": "^3.0.2",
342     "preact": "^10.5.9"
343   },
344diff --git a/service-worker.js b/service-worker.js
345new file mode 100644
346index 0000000..7cdeb56
347--- /dev/null
348+++ b/service-worker.js
349@@ -0,0 +1,70 @@
350+import * as irc from "./lib/irc.js";
351+import { idbkv } from "./lib/index.js";
352+
353+self.addEventListener("push", async (event) => {
354+	let title, body, parsedMessage;
355+
356+	try {
357+		parsedMessage = irc.parseMessage(event.data.text());
358+
359+		switch(parsedMessage.command) {
360+			case "PRIVMSG":
361+				title = `${parsedMessage.prefix.name} (${parsedMessage.params[0]}) says:`;
362+				body = parsedMessage.params[1];
363+				break;
364+			case "NOTE":
365+				if (parsedMessage.params.length > 1 && parsedMessage.params[0] == "WEBPUSH" && parsedMessage.params[1] == "REGISTERED") {
366+					title = "Push notifications enabled";
367+					body = "Successfully registered webpush token"
368+					break;
369+				}
370+				title = parsedMessage.command;
371+				body = parsedMessage.params.join(" ");
372+				break;
373+			default:
374+				title = parsedMessage.command;
375+				body = JSON.stringify(parsedMessage);
376+				break;
377+		}
378+	} catch (err) {
379+		title = event.data.text()
380+		body = event.data.text()
381+		console.error("Error parsing irc message in service worker:", err);
382+	}
383+
384+	const data = {
385+		message: parsedMessage
386+	};
387+
388+	event.waitUntil(idbkv.set("notification", data).then(() => {
389+		self.registration.showNotification(title, {
390+			body,
391+			data,
392+		})
393+	}));
394+});
395+
396+self.addEventListener('notificationclick', (event) => {
397+	event.notification.close();
398+
399+	event.waitUntil(clients.matchAll({
400+		includeUncontrolled: true,
401+	}).then(async (clientList) => {
402+		for (const client of clientList) {
403+			if ("focus" in client) {
404+				return client.focus();
405+			}
406+			return client;
407+		}
408+		if ("openWindow" in clients) {
409+			return clients.openWindow("/");
410+		}
411+	}).then((client) => {
412+		if (client) {
413+			client.postMessage(event.notification.data);
414+		}
415+	}));
416+});
417+self.addEventListener('install', () => { });
418+self.addEventListener('activate', () => { });
419+self.addEventListener('fetch', () => { });