This commit is contained in:
binwiederhier
2026-07-08 22:42:42 +02:00
parent 08d81a3645
commit 6869d166ae
18 changed files with 50 additions and 42 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
+12 -4
View File
@@ -5,7 +5,9 @@
font-family: "Roboto"; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 300; font-weight: 300;
src: local(""), url("../fonts/roboto-v29-latin-300.woff2") format("woff2"); src:
local(""),
url("../fonts/roboto-v29-latin-300.woff2") format("woff2");
} }
/* roboto-regular - latin */ /* roboto-regular - latin */
@@ -13,7 +15,9 @@
font-family: "Roboto"; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 400; font-weight: 400;
src: local(""), url("../fonts/roboto-v29-latin-regular.woff2") format("woff2"); src:
local(""),
url("../fonts/roboto-v29-latin-regular.woff2") format("woff2");
} }
/* roboto-500 - latin */ /* roboto-500 - latin */
@@ -21,7 +25,9 @@
font-family: "Roboto"; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 500; font-weight: 500;
src: local(""), url("../fonts/roboto-v29-latin-500.woff2") format("woff2"); src:
local(""),
url("../fonts/roboto-v29-latin-500.woff2") format("woff2");
} }
/* roboto-700 - latin */ /* roboto-700 - latin */
@@ -29,5 +35,7 @@
font-family: "Roboto"; font-family: "Roboto";
font-style: normal; font-style: normal;
font-weight: 700; font-weight: 700;
src: local(""), url("../fonts/roboto-v29-latin-700.woff2") format("woff2"); src:
local(""),
url("../fonts/roboto-v29-latin-700.woff2") format("woff2");
} }
+3 -3
View File
@@ -61,7 +61,7 @@ const handlePushMessage = async (data) => {
topicRoute: new URL(message.topic, self.location.origin).toString(), topicRoute: new URL(message.topic, self.location.origin).toString(),
baseUrl: subscription.baseUrl, baseUrl: subscription.baseUrl,
topic: subscription.topic, topic: subscription.topic,
}) }),
); );
// Delete existing notification with same sequence ID (if any) // Delete existing notification with same sequence ID (if any)
@@ -417,7 +417,7 @@ self.addEventListener("notificationclick", (event) => {
// [{"revision":"aaabbbcccdddeeefff12345","url":"/index.html"},...] // [{"revision":"aaabbbcccdddeeefff12345","url":"/index.html"},...]
precacheAndRoute( precacheAndRoute(
// eslint-disable-next-line no-underscore-dangle // eslint-disable-next-line no-underscore-dangle
self.__WB_MANIFEST self.__WB_MANIFEST,
); );
// Claim all open windows // Claim all open windows
@@ -441,7 +441,7 @@ if (!import.meta.env.DEV) {
// the app root itself, could be /, or not // the app root itself, could be /, or not
new RegExp(`^${config.app_root}$`), new RegExp(`^${config.app_root}$`),
], ],
}) }),
); );
// the manifest excludes config.js (see vite.config.js) since the dist-file differs from the // the manifest excludes config.js (see vite.config.js) since the dist-file differs from the
+2 -2
View File
@@ -65,7 +65,7 @@ describe("AccountApi.create", () => {
await accountApi.create("phil", "pw"); await accountApi.create("phil", "pw");
expect(fetchMock).toHaveBeenCalledWith( expect(fetchMock).toHaveBeenCalledWith(
"https://ntfy.sh/v1/account", "https://ntfy.sh/v1/account",
expect.objectContaining({ method: "POST", body: JSON.stringify({ username: "phil", password: "pw", email: "" }) }) expect.objectContaining({ method: "POST", body: JSON.stringify({ username: "phil", password: "pw", email: "" }) }),
); );
}); });
}); });
@@ -195,7 +195,7 @@ describe("AccountApi.sync", () => {
notification: { sound: "ding", delete_after: 3600, min_priority: 3 }, notification: { sound: "ding", delete_after: 3600, min_priority: 3 },
subscriptions: [{ topic: "t" }], subscriptions: [{ topic: "t" }],
reservations: [{ topic: "t" }], reservations: [{ topic: "t" }],
}) }),
); );
await accountApi.sync(); await accountApi.sync();
+3 -3
View File
@@ -57,7 +57,7 @@ describe("Api.publish", () => {
expect.objectContaining({ expect.objectContaining({
method: "PUT", method: "PUT",
body: JSON.stringify({ topic: "mytopic", message: "Hello", priority: 5, tags: ["warning"] }), body: JSON.stringify({ topic: "mytopic", message: "Hello", priority: 5, tags: ["warning"] }),
}) }),
); );
}); });
@@ -109,7 +109,7 @@ describe("Api web push", () => {
expect.objectContaining({ expect.objectContaining({
method: "POST", method: "POST",
body: JSON.stringify({ endpoint: "https://push.example/abc", auth: "AUTH", p256dh: "P256", topics: ["topicA", "topicB"] }), body: JSON.stringify({ endpoint: "https://push.example/abc", auth: "AUTH", p256dh: "P256", topics: ["topicA", "topicB"] }),
}) }),
); );
}); });
@@ -121,7 +121,7 @@ describe("Api web push", () => {
expect.objectContaining({ expect.objectContaining({
method: "DELETE", method: "DELETE",
body: JSON.stringify({ endpoint: "https://push.example/abc" }), body: JSON.stringify({ endpoint: "https://push.example/abc" }),
}) }),
); );
}); });
}); });
+1 -1
View File
@@ -67,7 +67,7 @@ class Connection {
this.ws.onclose = (event) => { this.ws.onclose = (event) => {
if (event.wasClean) { if (event.wasClean) {
console.log( console.log(
`[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}` `[Connection, ${this.shortUrl}, ${this.connectionId}] Connection closed cleanly, code=${event.code} reason=${event.reason}`,
); );
this.ws = null; this.ws = null;
} else { } else {
+2 -2
View File
@@ -70,13 +70,13 @@ class ConnectionManager {
user, user,
since, since,
(subId, notification) => this.notificationReceived(subId, notification), (subId, notification) => this.notificationReceived(subId, notification),
(subId, state) => this.stateChanged(subId, state) (subId, state) => this.stateChanged(subId, state),
); );
this.connections.set(connectionId, connection); this.connections.set(connectionId, connection);
console.log( console.log(
`[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${ `[ConnectionManager] Starting new connection ${connectionId} (subscription ${subscriptionId} with user ${
user ? user.username : "anonymous" user ? user.username : "anonymous"
})` })`,
); );
connection.start(); connection.start();
} }
+1 -1
View File
@@ -30,7 +30,7 @@ class Notifier {
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(), topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
baseUrl: subscription.baseUrl, baseUrl: subscription.baseUrl,
topic: subscription.topic, topic: subscription.topic,
}) }),
); );
} }
+2 -2
View File
@@ -35,7 +35,7 @@ class Poller {
} catch (e) { } catch (e) {
console.log(`[Poller] Error polling ${s.id}`, e); console.log(`[Poller] Error polling ${s.id}`, e);
} }
}) }),
); );
} }
@@ -61,7 +61,7 @@ class Poller {
if (deletedSequenceIds.length > 0) { if (deletedSequenceIds.length > 0) {
console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds); console.log(`[Poller] Deleting notifications with deleted sequence IDs for ${subscription.id}`, deletedSequenceIds);
await Promise.all( await Promise.all(
deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId)) deletedSequenceIds.map((sequenceId) => subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId)),
); );
} }
+4 -4
View File
@@ -18,7 +18,7 @@ export class SubscriptionManager {
subscriptions.map(async (s) => ({ subscriptions.map(async (s) => ({
...s, ...s,
new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(), new: await this.db.notifications.where({ subscriptionId: s.id, new: 1 }).count(),
})) })),
); );
} }
@@ -122,7 +122,7 @@ export class SubscriptionManager {
}); });
return local.id; return local.id;
}) }),
); );
// Remove local subscriptions that do not exist remotely // Remove local subscriptions that do not exist remotely
@@ -134,7 +134,7 @@ export class SubscriptionManager {
if (!local.internal && !remoteExists) { if (!local.internal && !remoteExists) {
await this.remove(local); await this.remove(local);
} }
}) }),
); );
} }
@@ -144,7 +144,7 @@ export class SubscriptionManager {
if (!browserSubscription) { if (!browserSubscription) {
console.log( console.log(
"[SubscriptionManager] No browser subscription currently exists, so web push was never enabled or the notification permission was removed. Skipping." "[SubscriptionManager] No browser subscription currently exists, so web push was never enabled or the notification permission was removed. Skipping.",
); );
return; return;
} }
+3 -3
View File
@@ -56,7 +56,7 @@ describe("throwAppError", () => {
await expect(throwAppError(fakeResponse(409, { code: UserExistsError.CODE }))).rejects.toBeInstanceOf(UserExistsError); await expect(throwAppError(fakeResponse(409, { code: UserExistsError.CODE }))).rejects.toBeInstanceOf(UserExistsError);
await expect(throwAppError(fakeResponse(409, { code: TopicReservedError.CODE }))).rejects.toBeInstanceOf(TopicReservedError); await expect(throwAppError(fakeResponse(409, { code: TopicReservedError.CODE }))).rejects.toBeInstanceOf(TopicReservedError);
await expect(throwAppError(fakeResponse(429, { code: AccountActionLimitReachedError.CODE }))).rejects.toBeInstanceOf( await expect(throwAppError(fakeResponse(429, { code: AccountActionLimitReachedError.CODE }))).rejects.toBeInstanceOf(
AccountActionLimitReachedError AccountActionLimitReachedError,
); );
}); });
@@ -78,7 +78,7 @@ describe("fetchOrThrow", () => {
const response = { status: 200 }; const response = { status: 200 };
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn(async () => response) vi.fn(async () => response),
); );
await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).resolves.toBe(response); await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).resolves.toBe(response);
}); });
@@ -86,7 +86,7 @@ describe("fetchOrThrow", () => {
it("throws on a non-200 response", async () => { it("throws on a non-200 response", async () => {
vi.stubGlobal( vi.stubGlobal(
"fetch", "fetch",
vi.fn(async () => fakeResponse(401)) vi.fn(async () => fakeResponse(401)),
); );
await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).rejects.toBeInstanceOf(UnauthorizedError); await expect(fetchOrThrow("https://ntfy.sh/mytopic/json")).rejects.toBeInstanceOf(UnauthorizedError);
}); });
+1 -1
View File
@@ -130,7 +130,7 @@ const Layout = () => {
const [selected] = (subscriptionsWithoutInternal || []).filter( const [selected] = (subscriptionsWithoutInternal || []).filter(
(s) => (s) =>
(params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) || (params.baseUrl && expandUrl(params.baseUrl).includes(s.baseUrl) && params.topic === s.topic) ||
(config.base_url === s.baseUrl && params.topic === s.topic) (config.base_url === s.baseUrl && params.topic === s.topic),
); );
useConnectionListeners(account, subscriptions, users, webPushTopics); useConnectionListeners(account, subscriptions, users, webPushTopics);
+3 -3
View File
@@ -80,7 +80,7 @@ const SingleSubscriptionList = (props) => {
// getAllNotifications() filtered by id), so topic switches are instant. // getAllNotifications() filtered by id), so topic switches are instant.
const notifications = useMemo( const notifications = useMemo(
() => allNotifications.filter((notification) => notification.subscriptionId === subscription.id), () => allNotifications.filter((notification) => notification.subscriptionId === subscription.id),
[allNotifications, subscription.id] [allNotifications, subscription.id],
); );
if (notifications.length === 0) { if (notifications.length === 0) {
return <NoNotifications subscription={subscription} />; return <NoNotifications subscription={subscription} />;
@@ -104,7 +104,7 @@ const NotificationList = (props) => {
main.scrollTo(0, 0); main.scrollTo(0, 0);
} }
}, },
[props.id] [props.id],
); );
return ( return (
@@ -319,7 +319,7 @@ const Attachment = (props) => {
infos.push( infos.push(
t("notifications_attachment_link_expires", { t("notifications_attachment_link_expires", {
date: formatDateTime(attachment.expires, dateFormat, timeFormat), date: formatDateTime(attachment.expires, dateFormat, timeFormat),
}) }),
); );
} }
if (expired) { if (expired) {
+4 -4
View File
@@ -168,7 +168,7 @@ const PublishDialog = (props) => {
loaded: formatBytes(ev.loaded), loaded: formatBytes(ev.loaded),
total: formatBytes(ev.total), total: formatBytes(ev.total),
percent: Math.round((ev.loaded * 100.0) / ev.total), percent: Math.round((ev.loaded * 100.0) / ev.total),
}) }),
); );
} else { } else {
setStatus(t("publish_dialog_progress_uploading")); setStatus(t("publish_dialog_progress_uploading"));
@@ -201,19 +201,19 @@ const PublishDialog = (props) => {
t("publish_dialog_attachment_limits_file_and_quota_reached", { t("publish_dialog_attachment_limits_file_and_quota_reached", {
fileSizeLimit: formatBytes(fileSizeLimit), fileSizeLimit: formatBytes(fileSizeLimit),
remainingBytes: formatBytes(remainingBytes), remainingBytes: formatBytes(remainingBytes),
}) }),
); );
} else if (fileSizeLimitReached) { } else if (fileSizeLimitReached) {
setAttachFileError( setAttachFileError(
t("publish_dialog_attachment_limits_file_reached", { t("publish_dialog_attachment_limits_file_reached", {
fileSizeLimit: formatBytes(fileSizeLimit), fileSizeLimit: formatBytes(fileSizeLimit),
}) }),
); );
} else if (quotaReached) { } else if (quotaReached) {
setAttachFileError( setAttachFileError(
t("publish_dialog_attachment_limits_quota_reached", { t("publish_dialog_attachment_limits_quota_reached", {
remainingBytes: formatBytes(remainingBytes), remainingBytes: formatBytes(remainingBytes),
}) }),
); );
} else { } else {
setAttachFileError(""); setAttachFileError("");
+2 -2
View File
@@ -93,7 +93,7 @@ const SubscribePage = (props) => {
const { topic } = props; const { topic } = props;
const existingTopicUrls = props.subscriptions.map((s) => topicUrl(s.baseUrl, s.topic)); 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 existingBaseUrls = Array.from(new Set([publicBaseUrl, ...props.subscriptions.map((s) => s.baseUrl)])).filter(
(s) => s !== config.base_url (s) => s !== config.base_url,
); );
const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account); const showReserveTopicCheckbox = config.enable_reservations && !anotherServerVisible && (config.enable_payments || account);
const reserveTopicEnabled = const reserveTopicEnabled =
@@ -113,7 +113,7 @@ const SubscribePage = (props) => {
setError( setError(
t("subscribe_dialog_error_user_not_authorized", { t("subscribe_dialog_error_user_not_authorized", {
username, username,
}) }),
); );
return; return;
} }
+2 -2
View File
@@ -120,14 +120,14 @@ export const SubscriptionPopup = (props) => {
`Hello friend, this is a test notification from ntfy web. It's ${formatDateTime( `Hello friend, this is a test notification from ntfy web. It's ${formatDateTime(
nowSeconds, nowSeconds,
dateFormat, dateFormat,
timeFormat timeFormat,
)} right now. Is that early or late?`, )} right now. Is that early or late?`,
`So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`, `So I heard you like ntfy? If that's true, go to GitHub and star it, or to the Play store and rate it. Thanks! Oh yeah, this is a test notification.`,
`It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`, `It's almost like you want to hear what I have to say. I'm not even a machine. I'm just a sentence that Phil typed on a random Thursday.`,
`Alright then, it's ${formatDateTime( `Alright then, it's ${formatDateTime(
nowSeconds, nowSeconds,
dateFormat, dateFormat,
timeFormat timeFormat,
)} already. Boy oh boy, where did the time go? I hope you're alright, friend.`, )} already. Boy oh boy, where did the time go? I hope you're alright, friend.`,
`There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`, `There are nine million bicycles in Beijing That's a fact; It's a thing we can't deny. I wonder if that's true ...`,
`I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`, `I'm really excited that you're trying out ntfy. Did you know that there are a few public topics, such as ntfy.sh/stats and ntfy.sh/announcements.`,
+3 -3
View File
@@ -30,7 +30,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
// wsSubscriptions should stay stable unless the list of subscription IDs changes. Without the memo, the connection // wsSubscriptions should stay stable unless the list of subscription IDs changes. Without the memo, the connection
// listener calls a refresh for no reason. This isn't a problem due to the makeConnectionId, but it triggers an // listener calls a refresh for no reason. This isn't a problem due to the makeConnectionId, but it triggers an
// unnecessary recomputation for every received message. // unnecessary recomputation for every received message.
[JSON.stringify({ subscriptions: subscriptions?.map(({ id }) => id), webPushTopics })] [JSON.stringify({ subscriptions: subscriptions?.map(({ id }) => id), webPushTopics })],
); );
// Register listeners for incoming messages, and connection state changes // Register listeners for incoming messages, and connection state changes
@@ -103,7 +103,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
}, },
// We have to disable dep checking for "navigate". This is fine, it never changes. // We have to disable dep checking for "navigate". This is fine, it never changes.
[] [],
); );
// Sync topic listener: For accounts with sync_topic, subscribe to an internal topic // Sync topic listener: For accounts with sync_topic, subscribe to an internal topic
@@ -233,7 +233,7 @@ export const useWebPushTopics = () => {
const topics = useLiveQuery( const topics = useLiveQuery(
async () => subscriptionManager.webPushTopics(pushPossible), async () => subscriptionManager.webPushTopics(pushPossible),
// invalidate (reload) query when these values change // invalidate (reload) query when these values change
[pushPossible] [pushPossible],
); );
useWebPushListener(topics); useWebPushListener(topics);
+1 -1
View File
@@ -39,7 +39,7 @@ export default defineConfig(({ mode }) => ({
...entry, ...entry,
url: "app.html", url: "app.html",
} }
: entry : entry,
), ),
}), }),
], ],