mirror of
https://github.com/multipleof4/ntfy.git
synced 2026-10-08 21:05:21 +00:00
Fix topic reservation icon in nav bar
This commit is contained in:
@@ -1974,6 +1974,7 @@ since I do have to reset emails on a regular basis.
|
|||||||
* Update web app dependencies, including major-version upgrades to Vite (6 -> 8, now Rolldown-based), Material UI (5 -> 9), and Dexie (3 -> 4) ([#1800](https://github.com/binwiederhier/ntfy/pull/1800), [#1764](https://github.com/binwiederhier/ntfy/pull/1764), [#1767](https://github.com/binwiederhier/ntfy/pull/1767), [#1762](https://github.com/binwiederhier/ntfy/pull/1762), [#1766](https://github.com/binwiederhier/ntfy/pull/1766), [#1765](https://github.com/binwiederhier/ntfy/pull/1765), thanks Dependabot)
|
* Update web app dependencies, including major-version upgrades to Vite (6 -> 8, now Rolldown-based), Material UI (5 -> 9), and Dexie (3 -> 4) ([#1800](https://github.com/binwiederhier/ntfy/pull/1800), [#1764](https://github.com/binwiederhier/ntfy/pull/1764), [#1767](https://github.com/binwiederhier/ntfy/pull/1767), [#1762](https://github.com/binwiederhier/ntfy/pull/1762), [#1766](https://github.com/binwiederhier/ntfy/pull/1766), [#1765](https://github.com/binwiederhier/ntfy/pull/1765), thanks Dependabot)
|
||||||
* Play notification sounds in the web app even when the Notification API is unavailable, e.g. over plain HTTP or in browsers without notification support ([#1772](https://github.com/binwiederhier/ntfy/pull/1772), thanks to [@mitya12342](https://github.com/mitya12342) for the contribution)
|
* Play notification sounds in the web app even when the Notification API is unavailable, e.g. over plain HTTP or in browsers without notification support ([#1772](https://github.com/binwiederhier/ntfy/pull/1772), thanks to [@mitya12342](https://github.com/mitya12342) for the contribution)
|
||||||
* Stop escaping `<`, `>`, and `&` as `\u003c`/`\u003e`/`\u0026` in JSON responses ([#1511](https://github.com/binwiederhier/ntfy/issues/1511), [#1512](https://github.com/binwiederhier/ntfy/pull/1512), thanks to [@wunter8](https://github.com/wunter8) for the contribution)
|
* Stop escaping `<`, `>`, and `&` as `\u003c`/`\u003e`/`\u0026` in JSON responses ([#1511](https://github.com/binwiederhier/ntfy/issues/1511), [#1512](https://github.com/binwiederhier/ntfy/pull/1512), thanks to [@wunter8](https://github.com/wunter8) for the contribution)
|
||||||
|
* Fix the web app navbar not reflecting a topic reservation (lock icon, and "Reserve topic" -> "Change reservation"/"Remove reservation" menu) until a page reload, by persisting reservation and display-name changes onto already-subscribed topics during account sync
|
||||||
|
|
||||||
### ntfy Android v1.25.x (UNRELEASED)
|
### ntfy Android v1.25.x (UNRELEASED)
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { topicUrl } from "./utils";
|
|||||||
import { messageWithSequenceId } from "./notificationUtils";
|
import { messageWithSequenceId } from "./notificationUtils";
|
||||||
import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE } from "./events";
|
import { EVENT_MESSAGE, EVENT_MESSAGE_CLEAR, EVENT_MESSAGE_DELETE } from "./events";
|
||||||
|
|
||||||
class SubscriptionManager {
|
export class SubscriptionManager {
|
||||||
constructor(dbImpl) {
|
constructor(dbImpl) {
|
||||||
this.db = dbImpl;
|
this.db = dbImpl;
|
||||||
}
|
}
|
||||||
@@ -65,23 +65,36 @@ class SubscriptionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Upsert a subscription: create it if it doesn't exist yet, or merge the given fields into the
|
||||||
|
* existing one. Merging matters for account sync, which passes the remote display name and
|
||||||
|
* reservation -- without it, reserving/unreserving a topic you're already subscribed to would
|
||||||
|
* never be reflected locally until the database is recreated (e.g. a fresh login). Local-only
|
||||||
|
* state such as mutedUntil and last is preserved on merge.
|
||||||
|
*
|
||||||
* @param {string} baseUrl
|
* @param {string} baseUrl
|
||||||
* @param {string} topic
|
* @param {string} topic
|
||||||
* @param {object} opts
|
* @param {object} opts
|
||||||
* @param {boolean} opts.internal
|
* @param {boolean} opts.internal
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
async add(baseUrl, topic, opts = {}) {
|
async upsert(baseUrl, topic, opts = {}) {
|
||||||
const id = topicUrl(baseUrl, topic);
|
const id = topicUrl(baseUrl, topic);
|
||||||
|
|
||||||
const existingSubscription = await this.get(id);
|
const existingSubscription = await this.get(id);
|
||||||
if (existingSubscription) {
|
if (existingSubscription) {
|
||||||
|
// Avoid a needless write (and the resulting Dexie live-query churn) when nothing changed.
|
||||||
|
const changed = Object.keys(opts).some((key) => existingSubscription[key] !== opts[key]);
|
||||||
|
if (!changed) {
|
||||||
return existingSubscription;
|
return existingSubscription;
|
||||||
}
|
}
|
||||||
|
const updatedSubscription = { ...existingSubscription, ...opts };
|
||||||
|
await this.db.subscriptions.put(updatedSubscription);
|
||||||
|
return updatedSubscription;
|
||||||
|
}
|
||||||
|
|
||||||
const subscription = {
|
const subscription = {
|
||||||
...opts,
|
...opts,
|
||||||
id: topicUrl(baseUrl, topic),
|
id,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
topic,
|
topic,
|
||||||
mutedUntil: 0,
|
mutedUntil: 0,
|
||||||
@@ -101,7 +114,9 @@ class SubscriptionManager {
|
|||||||
remoteSubscriptions.map(async (remote) => {
|
remoteSubscriptions.map(async (remote) => {
|
||||||
const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
|
const reservation = remoteReservations?.find((r) => remote.base_url === config.base_url && remote.topic === r.topic) || null;
|
||||||
|
|
||||||
const local = await this.add(remote.base_url, remote.topic, {
|
// upsert(): for topics that already exist locally this merges in the latest remote
|
||||||
|
// display name and reservation (see upsert() for why this matters).
|
||||||
|
const local = await this.upsert(remote.base_url, remote.topic, {
|
||||||
displayName: remote.display_name, // May be undefined
|
displayName: remote.display_name, // May be undefined
|
||||||
reservation, // May be null!
|
reservation, // May be null!
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
// SubscriptionManager pulls in a handful of browser/Dexie-heavy singletons at import time. Mock
|
||||||
|
// them so the module imports cleanly under the node test environment; the tests construct their
|
||||||
|
// own SubscriptionManager with an in-memory fake db, so the real db singleton is never used.
|
||||||
|
vi.mock("./Api", () => ({ default: {} }));
|
||||||
|
vi.mock("./Notifier", () => ({ default: {} }));
|
||||||
|
vi.mock("./Prefs", () => ({ default: {} }));
|
||||||
|
vi.mock("./db", () => ({ default: () => ({}) }));
|
||||||
|
|
||||||
|
const { SubscriptionManager } = await import("./SubscriptionManager");
|
||||||
|
|
||||||
|
// Minimal in-memory stand-in for the Dexie "subscriptions" table, implementing just the surface
|
||||||
|
// that syncFromRemote() (and the upsert/remove/update helpers it calls) touches.
|
||||||
|
const fakeDb = () => {
|
||||||
|
const rows = new Map();
|
||||||
|
return {
|
||||||
|
rows,
|
||||||
|
subscriptions: {
|
||||||
|
get: async (id) => rows.get(id),
|
||||||
|
put: async (sub) => {
|
||||||
|
rows.set(sub.id, sub);
|
||||||
|
},
|
||||||
|
update: async (id, changes) => {
|
||||||
|
const existing = rows.get(id);
|
||||||
|
if (existing) {
|
||||||
|
rows.set(id, { ...existing, ...changes });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
delete: async (id) => {
|
||||||
|
rows.delete(id);
|
||||||
|
},
|
||||||
|
toArray: async () => Array.from(rows.values()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const baseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.spyOn(console, "log").mockImplementation(() => {});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SubscriptionManager.upsert", () => {
|
||||||
|
it("merges fields into an existing subscription without clobbering local-only state", async () => {
|
||||||
|
const db = fakeDb();
|
||||||
|
const manager = new SubscriptionManager(db);
|
||||||
|
|
||||||
|
await manager.upsert(baseUrl, "mytopic");
|
||||||
|
await manager.setMutedUntil("https://ntfy.sh/mytopic", 123);
|
||||||
|
|
||||||
|
const reservation = { topic: "mytopic", everyone: "deny-all" };
|
||||||
|
await manager.upsert(baseUrl, "mytopic", { displayName: "My Topic", reservation });
|
||||||
|
|
||||||
|
const stored = db.rows.get("https://ntfy.sh/mytopic");
|
||||||
|
expect(stored.reservation).toEqual(reservation);
|
||||||
|
expect(stored.displayName).toBe("My Topic");
|
||||||
|
expect(stored.mutedUntil).toBe(123); // local-only state preserved
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not write when an existing subscription would not change", async () => {
|
||||||
|
const db = fakeDb();
|
||||||
|
const manager = new SubscriptionManager(db);
|
||||||
|
|
||||||
|
await manager.upsert(baseUrl, "mytopic", { internal: true });
|
||||||
|
const putSpy = vi.spyOn(db.subscriptions, "put");
|
||||||
|
|
||||||
|
const result = await manager.upsert(baseUrl, "mytopic", { internal: true });
|
||||||
|
|
||||||
|
expect(putSpy).not.toHaveBeenCalled();
|
||||||
|
expect(result.topic).toBe("mytopic");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("SubscriptionManager.syncFromRemote", () => {
|
||||||
|
it("persists a reservation onto a subscription that already exists locally", async () => {
|
||||||
|
const db = fakeDb();
|
||||||
|
const manager = new SubscriptionManager(db);
|
||||||
|
|
||||||
|
// Topic was subscribed to before it was reserved, so it already exists locally without a
|
||||||
|
// reservation -- exactly the state when a user clicks "Reserve topic" in the navbar.
|
||||||
|
await manager.upsert(baseUrl, "mytopic");
|
||||||
|
expect(db.rows.get("https://ntfy.sh/mytopic").reservation).toBeFalsy();
|
||||||
|
|
||||||
|
const reservation = { topic: "mytopic", everyone: "deny-all" };
|
||||||
|
await manager.syncFromRemote([{ base_url: baseUrl, topic: "mytopic" }], [reservation]);
|
||||||
|
|
||||||
|
expect(db.rows.get("https://ntfy.sh/mytopic").reservation).toEqual(reservation);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears the reservation when the remote no longer reports one", async () => {
|
||||||
|
const db = fakeDb();
|
||||||
|
const manager = new SubscriptionManager(db);
|
||||||
|
|
||||||
|
await manager.upsert(baseUrl, "mytopic", { reservation: { topic: "mytopic", everyone: "deny-all" } });
|
||||||
|
|
||||||
|
await manager.syncFromRemote([{ base_url: baseUrl, topic: "mytopic" }], []);
|
||||||
|
|
||||||
|
expect(db.rows.get("https://ntfy.sh/mytopic").reservation).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("updates the display name on an existing subscription", async () => {
|
||||||
|
const db = fakeDb();
|
||||||
|
const manager = new SubscriptionManager(db);
|
||||||
|
|
||||||
|
await manager.upsert(baseUrl, "mytopic");
|
||||||
|
await manager.syncFromRemote([{ base_url: baseUrl, topic: "mytopic", display_name: "My Topic" }], []);
|
||||||
|
|
||||||
|
expect(db.rows.get("https://ntfy.sh/mytopic").displayName).toBe("My Topic");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -34,7 +34,7 @@ import prefs from "../app/Prefs";
|
|||||||
const publicBaseUrl = "https://ntfy.sh";
|
const publicBaseUrl = "https://ntfy.sh";
|
||||||
|
|
||||||
export const subscribeTopic = async (baseUrl, topic, opts) => {
|
export const subscribeTopic = async (baseUrl, topic, opts) => {
|
||||||
const subscription = await subscriptionManager.add(baseUrl, topic, opts);
|
const subscription = await subscriptionManager.upsert(baseUrl, topic, opts);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
try {
|
try {
|
||||||
await accountApi.addSubscription(baseUrl, topic);
|
await accountApi.addSubscription(baseUrl, topic);
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
if (!account || !account.sync_topic) {
|
if (!account || !account.sync_topic) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
subscriptionManager.add(config.base_url, account.sync_topic, { internal: true }); // Dangle!
|
subscriptionManager.upsert(config.base_url, account.sync_topic, { internal: true }); // Dangle!
|
||||||
}, [account]);
|
}, [account]);
|
||||||
|
|
||||||
// When subscriptions or users change, refresh the connections
|
// When subscriptions or users change, refresh the connections
|
||||||
@@ -139,7 +139,7 @@ export const useAutoSubscribe = (subscriptions, selected) => {
|
|||||||
const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url;
|
const baseUrl = params.baseUrl ? expandSecureUrl(params.baseUrl) : config.base_url;
|
||||||
console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
|
console.log(`[Hooks] Auto-subscribing to ${topicUrl(baseUrl, params.topic)}`);
|
||||||
(async () => {
|
(async () => {
|
||||||
const subscription = await subscriptionManager.add(baseUrl, params.topic);
|
const subscription = await subscriptionManager.upsert(baseUrl, params.topic);
|
||||||
if (session.exists()) {
|
if (session.exists()) {
|
||||||
try {
|
try {
|
||||||
await accountApi.addSubscription(baseUrl, params.topic);
|
await accountApi.addSubscription(baseUrl, params.topic);
|
||||||
|
|||||||
Reference in New Issue
Block a user