Fix the subscribe dialog crashing before the topic list has loaded

The subscriptions come from an IndexedDB live query and are undefined until it
returns. If the database is slow or blocked, the splash hides after 3s anyway,
and opening the subscribe dialog crashed on props.subscriptions.map.

Fixes #2014
This commit is contained in:
binwiederhier committed 2026-10-16 02:03:54 +00:00
1 parent 36e2b7561a
commit b4fd16a3e9
3 files changed
+15 -2

No files matched your search

+1
View File
@@ -2180,6 +2180,7 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
* Web app: Fix the wrong topic opening when subscribed to the same topic on two different servers ([#858](https://github.com/binwiederhier/ntfy/issues/858))
* Web app: Fix the "Upgrade to ntfy Pro" banner covering the bottom of a long topic list ([#1007](https://github.com/binwiederhier/ntfy/issues/1007))
* Web app: The "Notifications are disabled" and "Notifications are blocked" warnings can now be closed ([#1220](https://github.com/binwiederhier/ntfy/issues/1220))
* Web app: Fix a crash when opening the subscribe dialog before the topic list has loaded, e.g. while another tab holds the browser database ([#2014](https://github.com/binwiederhier/ntfy/issues/2014), [#1881](https://github.com/binwiederhier/ntfy/issues/1881))
### ntfy iOS app v1.8.0 (UNRELEASED)
+11
View File
@@ -20,6 +20,17 @@ test("generate a topic name", async ({ page }) => {
await expect(page.getByRole("button", { name: topic, exact: true })).toBeVisible();
});
test("the subscribe dialog opens before the topic list has loaded", async ({ page }) => {
// #2014: if IndexedDB is slow or blocked (e.g. another tab holds it during an upgrade), the
// splash hides after 3s without the topic list, and opening the dialog used to crash the app
await page.addInitScript(() => {
indexedDB.open = () => new Proxy({}, { get: () => undefined, set: () => true }); // Never opens
});
await page.goto("/app");
await page.getByRole("button", { name: "Subscribe to topic" }).click();
await expect(page.getByRole("dialog").getByLabel(/topic name/i)).toBeVisible();
});
test("visiting a topic URL subscribes to it", async ({ page }) => {
const topic = unique("visit");
await page.goto(`/${topic}`);
+3 -2
View File
@@ -91,8 +91,9 @@ const SubscribePage = (props) => {
const [everyone, setEveryone] = useState(Permission.DENY_ALL);
const baseUrl = anotherServerVisible ? props.baseUrl : config.base_url;
const { topic } = props;
const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic));
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter(
const subscriptions = props.subscriptions ?? []; // Undefined until loaded from IndexedDB, which can be slow or blocked
const existingTopicUrls = subscriptions.map((s) => topicUrl(s.baseUrl, s.topic));
const existingBaseUrls = Array.from(new Set([publicBaseUrl, ...subscriptions.map((s) => s.baseUrl)])).filter(
(s) => s !== config.base_url,
);
const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account);