Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions public/sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,121 @@ self.addEventListener("fetch", (event) => {
return;
}
});

// --- soju.im/webpush ---------------------------------------------------
// The server (via the hosted-backend) sends exactly one IRC message as
// the encrypted push payload: no trailing CRLF, tags dropped except
// msgid. We parse the minimum needed to render a notification.

function parseIrcLine(line) {
let rest = line;
let msgid = null;
if (rest.startsWith("@")) {
const sp = rest.indexOf(" ");
if (sp === -1) return null;
const tags = rest.slice(1, sp);
for (const t of tags.split(";")) {
const eq = t.indexOf("=");
if (eq !== -1 && t.slice(0, eq) === "msgid") msgid = t.slice(eq + 1);
}
rest = rest.slice(sp + 1);
}
if (!rest.startsWith(":")) return null;
const sp = rest.indexOf(" ");
if (sp === -1) return null;
const nick = rest.slice(1, sp).split("!")[0];
rest = rest.slice(sp + 1);
const trailingIdx = rest.indexOf(" :");
let head = rest;
let text = "";
if (trailingIdx !== -1) {
head = rest.slice(0, trailingIdx);
text = rest.slice(trailingIdx + 2);
}
const parts = head.split(" ");
return { msgid, nick, command: parts[0], target: parts[1] || "", text };
}

// Network name for the notification context. The hosted build is
// single-network, so the manifest's name is the network ("Obby").
// Cached after first read; the SW may be killed between pushes, so the
// fetch is cheap and falls back to "" on failure.
let cachedNetworkName = null;
async function getNetworkName() {
if (cachedNetworkName !== null) return cachedNetworkName;
try {
const res = await fetch("/manifest.webmanifest", { cache: "force-cache" });
const m = await res.json();
cachedNetworkName = m.name || m.short_name || "";
} catch {
cachedNetworkName = "";
}
return cachedNetworkName;
}

function isChannelTarget(target) {
return !!target && "#&^$".includes(target.charAt(0));
}

self.addEventListener("push", (event) => {
if (!event.data) return;
let raw;
try {
raw = event.data.text();
} catch {
return;
}
event.waitUntil(
(async () => {
const parsed = parseIrcLine(raw);
if (!parsed || !parsed.nick) return;

// If a client window is focused, the in-app notifier already
// surfaces this message -- don't double-notify.
const wins = await self.clients.matchAll({
type: "window",
includeUncontrolled: true,
});
if (wins.some((c) => c.focused)) return;

const network = await getNetworkName();
const channel = isChannelTarget(parsed.target);

// Channel highlight: "alice in #weather"; DM: "alice".
// The network name is appended as context when known.
let title = channel
? `${parsed.nick} in ${parsed.target}`
: parsed.nick;
if (network) title += ` · ${network}`;

await self.registration.showNotification(title, {
body: parsed.text || "",
icon: "/pwa/icon-192.png",
badge: "/pwa/icon-192.png",
// Group by conversation: per-channel for highlights, per-sender
// for DMs -- so repeats collapse instead of stacking endlessly.
tag: channel ? `hl-${parsed.target}` : `pm-${parsed.nick}`,
renotify: true,
data: {
nick: parsed.nick,
target: parsed.target,
network,
},
});
})(),
);
});

self.addEventListener("notificationclick", (event) => {
event.notification.close();
event.waitUntil(
self.clients
.matchAll({ type: "window", includeUncontrolled: true })
.then((wins) => {
for (const c of wins) {
if ("focus" in c) return c.focus();
}
if (self.clients.openWindow) return self.clients.openWindow("/");
}),
);
});
26 changes: 26 additions & 0 deletions src/components/ui/UserSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ import {
import { useMediaQuery } from "../../hooks/useMediaQuery";
import { useModalBehavior } from "../../hooks/useModalBehavior";
import ircClient from "../../lib/ircClient";
import { requestNotificationPermission } from "../../lib/notifications";
import { openExternalUrl } from "../../lib/openUrl";
import { isTauri } from "../../lib/platformUtils";
import { settingsRegistry } from "../../lib/settings";
import type { SettingValue } from "../../lib/settings/types";
import { registerWebPush, unregisterWebPush } from "../../lib/webpush";
import useStore, {
type GlobalSettings,
loadSavedServers,
Expand Down Expand Up @@ -695,6 +697,29 @@ export const UserSettings: React.FC = React.memo(() => {
quitMessage,
});

// The notifications toggle is the user gesture we use to manage
// soju.im/webpush. Turning it on prompts for browser permission and,
// if granted, subscribes this device on every connected server that
// supports push; turning it off tears the subscription down.
const wantNotifications = (settings as Partial<GlobalSettings>)
.enableNotifications;
if (wantNotifications) {
const permission = await requestNotificationPermission();
if (permission === "granted") {
for (const srv of servers) {
if (srv.vapidKey && srv.capabilities?.includes("soju.im/webpush")) {
void registerWebPush(srv.id, srv.vapidKey);
}
}
}
} else if (wantNotifications === false) {
for (const srv of servers) {
if (srv.capabilities?.includes("soju.im/webpush")) {
void unregisterWebPush(srv.id);
}
}
Comment on lines +706 to +720
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Handle push subscription calls as awaited tasks on connected servers only.

This currently dispatches registration/unregistration as fire-and-forget across all servers, which can fail silently and attempt work against disconnected servers.

🛠️ Proposed fix
+    const pushServers = servers.filter(
+      (srv) =>
+        srv.isConnected && srv.capabilities?.includes("soju.im/webpush"),
+    );
+
     if (wantNotifications) {
       const permission = await requestNotificationPermission();
       if (permission === "granted") {
-        for (const srv of servers) {
-          if (srv.vapidKey && srv.capabilities?.includes("soju.im/webpush")) {
-            void registerWebPush(srv.id, srv.vapidKey);
-          }
-        }
+        const tasks = pushServers
+          .filter((srv) => !!srv.vapidKey)
+          .map((srv) => registerWebPush(srv.id, srv.vapidKey!));
+        await Promise.allSettled(tasks);
       }
     } else if (wantNotifications === false) {
-      for (const srv of servers) {
-        if (srv.capabilities?.includes("soju.im/webpush")) {
-          void unregisterWebPush(srv.id);
-        }
-      }
+      const tasks = pushServers.map((srv) => unregisterWebPush(srv.id));
+      await Promise.allSettled(tasks);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/ui/UserSettings.tsx` around lines 706 - 720, The current code
fires registerWebPush/unregisterWebPush without awaiting and runs them for all
servers (which may be disconnected); change the logic to only operate on
connected servers and await the operations (either sequentially with await
inside the for..of or gather promises and await Promise.all). Specifically, in
the wantNotifications branch, after permission === "granted", filter servers by
a connection flag (e.g., srv.connected or srv.isConnected) and by srv.vapidKey
and capabilities before calling registerWebPush(srv.id, srv.vapidKey), and await
those calls; similarly, when wantNotifications === false, filter by the same
connection flag and capabilities then await unregisterWebPush(srv.id). Ensure
you handle errors from each awaited call (try/catch or Promise.allSettled) so
failures don’t fail silently.

}

// Save notification sound file
if (notificationSoundFile) {
const reader = new FileReader();
Expand Down Expand Up @@ -750,6 +775,7 @@ export const UserSettings: React.FC = React.memo(() => {
originalValues?.homepage,
originalValues?.pronouns,
originalValues?.status,
servers,
]);

// Handle close
Expand Down
4 changes: 4 additions & 0 deletions src/lib/irc/IRCClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,10 @@ export class IRCClient implements IRCClientContext {
"labeled-response",
"draft/read-marker",
"obsidianirc/cmdslist",
// soju.im/webpush: lets us register a browser PushManager
// subscription with the server so DMs/highlights wake the device
// via the platform push service even when the tab is closed.
"soju.im/webpush",
// Note: unrealircd.org/link-security is informational only, don't request it
];

Expand Down
20 changes: 20 additions & 0 deletions src/lib/settings/definitions/allSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,26 @@ const profileSettings: SettingDefinition[] = [
* Notification Settings
*/
const notificationSettings: SettingDefinition[] = [
{
id: "notifications.enable",
key: "enableNotifications",
category: "notifications",
subcategory: "General",
title: msg`Enable Notifications`,
description: msg`Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed.`,
type: "toggle",
defaultValue: false,
searchKeywords: [
"notification",
"push",
"desktop",
"alert",
"mention",
"dm",
"webpush",
],
priority: 0,
},
{
id: "notifications.enableSounds",
key: "enableNotificationSounds",
Expand Down
Binary file added src/lib/webpush.ts
Binary file not shown.
2 changes: 1 addition & 1 deletion src/locales/cs/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/cs/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Povolit zvuky upozornění"
msgid "Enable notifications"
msgstr "Povolit upozornění"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you are just skipping adding any translation everywhere though? 🤔 I only noticed now @ValwareIRC


#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Zadejte název účtu..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Zobrazit komentáře"
msgid "Show comments ({commentCount})"
msgstr "Zobrazit komentáře ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Zobrazit připojení/odchody"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/de/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/de/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Benachrichtigungstöne aktivieren"
msgid "Enable notifications"
msgstr "Benachrichtigungen aktivieren"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Kontoname eingeben..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Kommentare anzeigen"
msgid "Show comments ({commentCount})"
msgstr "Kommentare anzeigen ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Beitritte/Abgänge anzeigen"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/en/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,10 @@ msgstr "Enable Notification Sounds"
msgid "Enable notifications"
msgstr "Enable notifications"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr "Enable Notifications"
Comment on lines +876 to +878
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Pipeline failure: i18n catalog check failed.

The GitHub Actions i18n check is failing with: "Untranslated strings detected in source." This indicates the i18n extraction and compilation process may not have completed correctly.

Please run:

npm run i18n:extract && npm run i18n:compile

and commit any changes to the src/locales/ files to resolve the pipeline failure.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/locales/en/messages.po` around lines 876 - 878, The i18n pipeline failed
due to untranslated strings (specifically the msgid "Enable Notifications"); run
the extraction and compilation scripts (npm run i18n:extract && npm run
i18n:compile) locally to regenerate the locale files, verify the msgid "Enable
Notifications" is present and translated in the generated locale files, and
commit the updated files so the i18n catalog check passes.


#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Enter account name..."
Expand Down Expand Up @@ -2236,6 +2240,10 @@ msgstr "Show comments"
msgid "Show comments ({commentCount})"
msgstr "Show comments ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Show Joins/Parts"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/es/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/es/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Activar sonidos de notificación"
msgid "Enable notifications"
msgstr "Activar notificaciones"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Ingresa nombre de cuenta..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Mostrar comentarios"
msgid "Show comments ({commentCount})"
msgstr "Mostrar comentarios ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Mostrar entradas/salidas"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/fi/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/fi/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Ota ilmoitusäänet käyttöön"
msgid "Enable notifications"
msgstr "Ota ilmoitukset käyttöön"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Syötä tilin nimi..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Näytä kommentit"
msgid "Show comments ({commentCount})"
msgstr "Näytä kommentit ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Näytä liittymiset/poistumiset"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/fr/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/fr/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Activer les sons de notification"
msgid "Enable notifications"
msgstr "Activer les notifications"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Entrez le nom du compte..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Afficher les commentaires"
msgid "Show comments ({commentCount})"
msgstr "Afficher les commentaires ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Afficher les entrées/sorties"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/it/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/it/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "Abilita suoni di notifica"
msgid "Enable notifications"
msgstr "Abilita notifiche"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "Inserisci nome account..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "Mostra commenti"
msgid "Show comments ({commentCount})"
msgstr "Mostra commenti ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "Mostra entrate/uscite"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/ja/messages.mjs

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions src/locales/ja/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -874,6 +874,10 @@ msgstr "通知音を有効にする"
msgid "Enable notifications"
msgstr "通知を有効にする"

#: src/lib/settings/definitions/allSettings.ts
msgid "Enable Notifications"
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Enter account name..."
msgstr "アカウント名を入力..."
Expand Down Expand Up @@ -2237,6 +2241,10 @@ msgstr "コメントを表示"
msgid "Show comments ({commentCount})"
msgstr "コメントを表示 ({commentCount})"

#: src/lib/settings/definitions/allSettings.ts
msgid "Show desktop notifications for mentions and DMs. On servers that support push (soju.im/webpush) this also wakes this device when the app is closed."
msgstr ""

#: src/lib/settings/definitions/allSettings.ts
msgid "Show Joins/Parts"
msgstr "参加/退出を表示"
Expand Down
2 changes: 1 addition & 1 deletion src/locales/ko/messages.mjs

Large diffs are not rendered by default.

Loading
Loading