Compare commits
6 Commits
cancel-sch
...
v2.16.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b474a89b7 | ||
|
|
5ba1c71140 | ||
|
|
de81865c27 | ||
|
|
ed9c1bcb78 | ||
|
|
190d12cd54 | ||
|
|
63bf82e915 |
@@ -30,37 +30,37 @@ deb/rpm packages.
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.tar.gz
|
||||||
tar zxvf ntfy_2.15.0_linux_amd64.tar.gz
|
tar zxvf ntfy_2.16.0_linux_amd64.tar.gz
|
||||||
sudo cp -a ntfy_2.15.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_amd64/ntfy /usr/local/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_amd64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.tar.gz
|
||||||
tar zxvf ntfy_2.15.0_linux_armv6.tar.gz
|
tar zxvf ntfy_2.16.0_linux_armv6.tar.gz
|
||||||
sudo cp -a ntfy_2.15.0_linux_armv6/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_armv6/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_armv6/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.tar.gz
|
||||||
tar zxvf ntfy_2.15.0_linux_armv7.tar.gz
|
tar zxvf ntfy_2.16.0_linux_armv7.tar.gz
|
||||||
sudo cp -a ntfy_2.15.0_linux_armv7/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_armv7/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_armv7/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.tar.gz
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.tar.gz
|
||||||
tar zxvf ntfy_2.15.0_linux_arm64.tar.gz
|
tar zxvf ntfy_2.16.0_linux_arm64.tar.gz
|
||||||
sudo cp -a ntfy_2.15.0_linux_arm64/ntfy /usr/bin/ntfy
|
sudo cp -a ntfy_2.16.0_linux_arm64/ntfy /usr/bin/ntfy
|
||||||
sudo mkdir /etc/ntfy && sudo cp ntfy_2.15.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
sudo mkdir /etc/ntfy && sudo cp ntfy_2.16.0_linux_arm64/{client,server}/*.yml /etc/ntfy
|
||||||
sudo ntfy serve
|
sudo ntfy serve
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -116,7 +116,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -124,7 +124,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -132,7 +132,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -140,7 +140,7 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
wget https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.deb
|
wget https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.deb
|
||||||
sudo dpkg -i ntfy_*.deb
|
sudo dpkg -i ntfy_*.deb
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
@@ -150,28 +150,28 @@ Manually installing the .deb file:
|
|||||||
|
|
||||||
=== "x86_64/amd64"
|
=== "x86_64/amd64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_amd64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_amd64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv6"
|
=== "armv6"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv6.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv6.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "armv7/armhf"
|
=== "armv7/armhf"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_armv7.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_armv7.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
|
|
||||||
=== "arm64"
|
=== "arm64"
|
||||||
```bash
|
```bash
|
||||||
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_linux_arm64.rpm
|
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_linux_arm64.rpm
|
||||||
sudo systemctl enable ntfy
|
sudo systemctl enable ntfy
|
||||||
sudo systemctl start ntfy
|
sudo systemctl start ntfy
|
||||||
```
|
```
|
||||||
@@ -201,18 +201,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
|
|||||||
|
|
||||||
## macOS
|
## macOS
|
||||||
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on macOS as well.
|
||||||
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz),
|
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_darwin_all.tar.gz),
|
||||||
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
extract it and place it somewhere in your `PATH` (e.g. `/usr/local/bin/ntfy`).
|
||||||
|
|
||||||
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
If run as `root`, ntfy will look for its config at `/etc/ntfy/client.yml`. For all other users, it'll look for it at
|
||||||
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_darwin_all.tar.gz > ntfy_2.15.0_darwin_all.tar.gz
|
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_darwin_all.tar.gz > ntfy_2.16.0_darwin_all.tar.gz
|
||||||
tar zxvf ntfy_2.15.0_darwin_all.tar.gz
|
tar zxvf ntfy_2.16.0_darwin_all.tar.gz
|
||||||
sudo cp -a ntfy_2.15.0_darwin_all/ntfy /usr/local/bin/ntfy
|
sudo cp -a ntfy_2.16.0_darwin_all/ntfy /usr/local/bin/ntfy
|
||||||
mkdir ~/Library/Application\ Support/ntfy
|
mkdir ~/Library/Application\ Support/ntfy
|
||||||
cp ntfy_2.15.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
cp ntfy_2.16.0_darwin_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
|
||||||
ntfy --help
|
ntfy --help
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -231,7 +231,7 @@ brew install ntfy
|
|||||||
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
The ntfy server and CLI are fully supported on Windows. You can run the ntfy server directly or as a Windows service.
|
||||||
To install, you can either
|
To install, you can either
|
||||||
|
|
||||||
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.15.0/ntfy_2.15.0_windows_amd64.zip),
|
* [Download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.16.0/ntfy_2.16.0_windows_amd64.zip),
|
||||||
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
|
||||||
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
* Or install ntfy from the [Scoop](https://scoop.sh) main repository via `scoop install ntfy`
|
||||||
|
|
||||||
|
|||||||
@@ -2392,18 +2392,20 @@ Here are a few examples (assuming today's date is **12/10/2021, 9am, Eastern Tim
|
|||||||
</tr></table>
|
</tr></table>
|
||||||
|
|
||||||
### Updating scheduled notifications
|
### Updating scheduled notifications
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
**This feature is not yet released.** It will be available in ntfy v2.16.x and later and ntfy Android v1.22.x and later.
|
||||||
|
|
||||||
You can update or replace a scheduled message before it is delivered by publishing a new message with the same
|
You can update or replace a scheduled message before it is delivered by publishing a new message with the same
|
||||||
[sequence ID](#updating-deleting-notifications). When you do this, the **original scheduled message is deleted**
|
[sequence ID](#updating-deleting-notifications). When you do this, the **original scheduled message is deleted**
|
||||||
from the server and replaced with the new one. This is different from [updating notifications](#updating-notifications)
|
from the server and replaced with the new one. This is different from [updating notifications](#updating-notifications)
|
||||||
after delivery, where both messages are kept in the cache.
|
after delivery, where both messages are kept in the cache.
|
||||||
|
|
||||||
This is particularly useful for implementing a **watchdog that triggers when your script stops sending heartbeat messages**.
|
This is particularly useful for implementing a **watchdog that triggers when your script stops sending heartbeat messages**.
|
||||||
This mechanism is also called a [dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch). The idea is to have
|
This mechanism is also called a [dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch).
|
||||||
a mechanism that triggers an alert if it's not periodically reset.
|
|
||||||
|
|
||||||
For example, you could schedule a message to be
|
For example, you could schedule a message to be delivered in 5 minutes, but continuously update it every minute to push
|
||||||
delivered in 5 minutes, but continuously update it every minute to push the delivery time further into the future.
|
the delivery time further into the future. If your script or system stops running, the message will eventually be delivered as an alert.
|
||||||
If your script or system stops running, the message will eventually be delivered as an alert.
|
|
||||||
|
|
||||||
Here's an example of a dead man's switch that sends an alert if the script stops running for more than 5 minutes:
|
Here's an example of a dead man's switch that sends an alert if the script stops running for more than 5 minutes:
|
||||||
|
|
||||||
@@ -2515,6 +2517,10 @@ Here's an example of a dead man's switch that sends an alert if the script stops
|
|||||||
```
|
```
|
||||||
|
|
||||||
### Canceling scheduled notifications
|
### Canceling scheduled notifications
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
**This feature is not yet released.** It will be available in ntfy v2.16.x and later and ntfy Android v1.22.x and later.
|
||||||
|
|
||||||
You can cancel a scheduled message before it is delivered by sending a DELETE request to the
|
You can cancel a scheduled message before it is delivered by sending a DELETE request to the
|
||||||
`/<topic>/<sequence_id>` endpoint, just like [deleting notifications](#deleting-notifications). This will remove the
|
`/<topic>/<sequence_id>` endpoint, just like [deleting notifications](#deleting-notifications). This will remove the
|
||||||
scheduled message from the server so it will never be delivered, and emit a `message_delete` event to any subscribers.
|
scheduled message from the server so it will never be delivered, and emit a `message_delete` event to any subscribers.
|
||||||
|
|||||||
@@ -6,12 +6,34 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
| Component | Version | Release date |
|
| Component | Version | Release date |
|
||||||
|------------------|---------|--------------|
|
|------------------|---------|--------------|
|
||||||
| ntfy server | v2.15.0 | Nov 16, 2025 |
|
| ntfy server | v2.16.0 | Jan 19, 2026 |
|
||||||
| ntfy Android app | v1.21.1 | Jan 6, 2025 |
|
| ntfy Android app | v1.21.1 | Jan 6, 2025 |
|
||||||
| ntfy iOS app | v1.3 | Nov 26, 2023 |
|
| ntfy iOS app | v1.3 | Nov 26, 2023 |
|
||||||
|
|
||||||
Please check out the release notes for [upcoming releases](#not-released-yet) below.
|
Please check out the release notes for [upcoming releases](#not-released-yet) below.
|
||||||
|
|
||||||
|
## ntfy server v2.16.0
|
||||||
|
Released January 19, 2026
|
||||||
|
|
||||||
|
This release adds support for updating and deleting notifications, heartbeat-style / dead man's switch notifications,
|
||||||
|
custom Twilio call formats, and makes `ntfy serve` work on Windows. It also adds a "New version available" banner to the web app.
|
||||||
|
|
||||||
|
This one is very exciting, as it brings a lot of highly requested features to ntfy.
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
|
||||||
|
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
||||||
|
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
||||||
|
* Support for heartbeat-style / [dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch) notifications aka
|
||||||
|
[updating and deleting scheduled notifications](publish.md#scheduled-delivery) ([#1556](https://github.com/binwiederhier/ntfy/pull/1556),
|
||||||
|
[#1142](https://github.com/binwiederhier/ntfy/pull/1142), [#954](https://github.com/binwiederhier/ntfy/issues/954),
|
||||||
|
thanks to [@GamerGirlandCo](https://github.com/GamerGirlandCo) for the initial implementation)
|
||||||
|
* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
||||||
|
* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1104](https://github.com/binwiederhier/ntfy/issues/1104),
|
||||||
|
[#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328),
|
||||||
|
thanks to [@wtf911](https://github.com/wtf911))
|
||||||
|
* Web app: "New version available" banner ([#1554](https://github.com/binwiederhier/ntfy/pull/1554))
|
||||||
|
|
||||||
## ntfy Android app v1.21.1
|
## ntfy Android app v1.21.1
|
||||||
Released January 6, 2026
|
Released January 6, 2026
|
||||||
|
|
||||||
@@ -1599,22 +1621,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
|
|||||||
|
|
||||||
## Not released yet
|
## Not released yet
|
||||||
|
|
||||||
### ntfy server v2.16.x (UNRELEASED)
|
|
||||||
|
|
||||||
**Features:**
|
|
||||||
|
|
||||||
* Support for [updating and deleting notifications](publish.md#updating-deleting-notifications) ([#303](https://github.com/binwiederhier/ntfy/issues/303), [#1536](https://github.com/binwiederhier/ntfy/pull/1536),
|
|
||||||
[ntfy-android#151](https://github.com/binwiederhier/ntfy-android/pull/151), thanks to [@wunter8](https://github.com/wunter8) for the initial implementation)
|
|
||||||
* Support for heartbeat-style / [dead man's switch](https://en.wikipedia.org/wiki/Dead_man%27s_switch) notifications aka
|
|
||||||
[updating and deleting scheduled notifications](publish.md#scheduled-delivery) ([#1556](https://github.com/binwiederhier/ntfy/pull/1556),
|
|
||||||
[#1142](https://github.com/binwiederhier/ntfy/pull/1142), [#954](https://github.com/binwiederhier/ntfy/issues/954),
|
|
||||||
thanks to [@GamerGirlandCo](https://github.com/GamerGirlandCo) for the initial implementation)
|
|
||||||
* Configure [custom Twilio call format](config.md#phone-calls) for phone calls ([#1289](https://github.com/binwiederhier/ntfy/pull/1289), thanks to [@mmichaa](https://github.com/mmichaa) for the initial implementation)
|
|
||||||
* `ntfy serve` now works on Windows, including support for running it as a Windows service ([#1104](https://github.com/binwiederhier/ntfy/issues/1104),
|
|
||||||
[#1552](https://github.com/binwiederhier/ntfy/pull/1552), originally [#1328](https://github.com/binwiederhier/ntfy/pull/1328),
|
|
||||||
thanks to [@wtf911](https://github.com/wtf911))
|
|
||||||
* Web app: "New version available" banner ([#1554](https://github.com/binwiederhier/ntfy/pull/1554))
|
|
||||||
|
|
||||||
### ntfy Android app v1.22.x (UNRELEASED)
|
### ntfy Android app v1.22.x (UNRELEASED)
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -3823,9 +3823,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001764",
|
"version": "1.0.30001765",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz",
|
||||||
"integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==",
|
"integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { NavigationRoute, registerRoute } from "workbox-routing";
|
|||||||
import { NetworkFirst } from "workbox-strategies";
|
import { NetworkFirst } from "workbox-strategies";
|
||||||
import { clientsClaim } from "workbox-core";
|
import { clientsClaim } from "workbox-core";
|
||||||
import { dbAsync } from "../src/app/db";
|
import { dbAsync } from "../src/app/db";
|
||||||
import { badge, icon, messageWithSequenceId, toNotificationParams } from "../src/app/notificationUtils";
|
import { badge, icon, messageWithSequenceId, notificationTag, toNotificationParams } from "../src/app/notificationUtils";
|
||||||
import initI18n from "../src/app/i18n";
|
import initI18n from "../src/app/i18n";
|
||||||
import {
|
import {
|
||||||
EVENT_MESSAGE,
|
EVENT_MESSAGE,
|
||||||
@@ -38,6 +38,13 @@ const handlePushMessage = async (data) => {
|
|||||||
|
|
||||||
console.log("[ServiceWorker] Message received", data);
|
console.log("[ServiceWorker] Message received", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete existing notification with same sequence ID (if any)
|
// Delete existing notification with same sequence ID (if any)
|
||||||
const sequenceId = message.sequence_id || message.id;
|
const sequenceId = message.sequence_id || message.id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
@@ -65,10 +72,11 @@ const handlePushMessage = async (data) => {
|
|||||||
|
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
...toNotificationParams({
|
...toNotificationParams({
|
||||||
subscriptionId,
|
|
||||||
message,
|
message,
|
||||||
defaultTitle: message.topic,
|
defaultTitle: message.topic,
|
||||||
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
topicRoute: new URL(message.topic, self.location.origin).toString(),
|
||||||
|
baseUrl: subscription.baseUrl,
|
||||||
|
topic: subscription.topic,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -81,18 +89,23 @@ const handlePushMessageDelete = async (data) => {
|
|||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
console.log("[ServiceWorker] Deleting notification sequence", data);
|
console.log("[ServiceWorker] Deleting notification sequence", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Delete notification with the same sequence_id
|
// Delete notification with the same sequence_id
|
||||||
const sequenceId = message.sequence_id;
|
const sequenceId = message.sequence_id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
await db.notifications.where({ subscriptionId, sequenceId }).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close browser notification with matching tag
|
// Close browser notification with matching tag (scoped by topic)
|
||||||
const tag = message.sequence_id || message.id;
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, message.sequence_id || message.id);
|
||||||
if (tag) {
|
const notifications = await self.registration.getNotifications({ tag });
|
||||||
const notifications = await self.registration.getNotifications({ tag });
|
notifications.forEach((notification) => notification.close());
|
||||||
notifications.forEach((notification) => notification.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update subscription last message id (for ?since=... queries)
|
// Update subscription last message id (for ?since=... queries)
|
||||||
await db.subscriptions.update(subscriptionId, {
|
await db.subscriptions.update(subscriptionId, {
|
||||||
@@ -108,18 +121,23 @@ const handlePushMessageClear = async (data) => {
|
|||||||
const db = await dbAsync();
|
const db = await dbAsync();
|
||||||
console.log("[ServiceWorker] Marking notification as read", data);
|
console.log("[ServiceWorker] Marking notification as read", data);
|
||||||
|
|
||||||
|
// Look up subscription for baseUrl and topic
|
||||||
|
const subscription = await db.subscriptions.get(subscriptionId);
|
||||||
|
if (!subscription) {
|
||||||
|
console.log("[ServiceWorker] Subscription not found", subscriptionId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Mark notification as read (set new = 0)
|
// Mark notification as read (set new = 0)
|
||||||
const sequenceId = message.sequence_id;
|
const sequenceId = message.sequence_id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
await db.notifications.where({ subscriptionId, sequenceId }).modify({ new: 0 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close browser notification with matching tag
|
// Close browser notification with matching tag (scoped by topic)
|
||||||
const tag = message.sequence_id || message.id;
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, message.sequence_id || message.id);
|
||||||
if (tag) {
|
const notifications = await self.registration.getNotifications({ tag });
|
||||||
const notifications = await self.registration.getNotifications({ tag });
|
notifications.forEach((notification) => notification.close());
|
||||||
notifications.forEach((notification) => notification.close());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update subscription last message id (for ?since=... queries)
|
// Update subscription last message id (for ?since=... queries)
|
||||||
await db.subscriptions.update(subscriptionId, {
|
await db.subscriptions.update(subscriptionId, {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
import { playSound, topicDisplayName, topicShortUrl, urlB64ToUint8Array } from "./utils";
|
||||||
import { toNotificationParams } from "./notificationUtils";
|
import { notificationTag, toNotificationParams } from "./notificationUtils";
|
||||||
import prefs from "./Prefs";
|
import prefs from "./Prefs";
|
||||||
import routes from "../components/routes";
|
import routes from "../components/routes";
|
||||||
|
|
||||||
@@ -23,21 +23,23 @@ class Notifier {
|
|||||||
const registration = await this.serviceWorkerRegistration();
|
const registration = await this.serviceWorkerRegistration();
|
||||||
await registration.showNotification(
|
await registration.showNotification(
|
||||||
...toNotificationParams({
|
...toNotificationParams({
|
||||||
subscriptionId: subscription.id,
|
|
||||||
message: notification,
|
message: notification,
|
||||||
defaultTitle,
|
defaultTitle,
|
||||||
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
|
topicRoute: new URL(routes.forSubscription(subscription), window.location.origin).toString(),
|
||||||
|
baseUrl: subscription.baseUrl,
|
||||||
|
topic: subscription.topic,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel(notification) {
|
async cancel(subscription, notification) {
|
||||||
if (!this.supported()) {
|
if (!this.supported()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const tag = notification.sequence_id || notification.id;
|
const sequenceId = notification.sequence_id || notification.id;
|
||||||
console.log(`[Notifier] Cancelling notification with ${tag}`);
|
const tag = notificationTag(subscription.baseUrl, subscription.topic, sequenceId);
|
||||||
|
console.log(`[Notifier] Cancelling notification with tag ${tag}`);
|
||||||
const registration = await this.serviceWorkerRegistration();
|
const registration = await this.serviceWorkerRegistration();
|
||||||
const notifications = await registration.getNotifications({ tag });
|
const notifications = await registration.getNotifications({ tag });
|
||||||
notifications.forEach((n) => n.close());
|
notifications.forEach((n) => n.close());
|
||||||
|
|||||||
@@ -50,8 +50,16 @@ export const isImage = (attachment) => {
|
|||||||
export const icon = "/static/images/ntfy.png";
|
export const icon = "/static/images/ntfy.png";
|
||||||
export const badge = "/static/images/mask-icon.svg";
|
export const badge = "/static/images/mask-icon.svg";
|
||||||
|
|
||||||
export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
/**
|
||||||
|
* Computes a unique notification tag scoped by baseUrl, topic, and sequence ID.
|
||||||
|
* This ensures notifications from different topics with the same sequence ID don't collide.
|
||||||
|
*/
|
||||||
|
export const notificationTag = (baseUrl, topic, sequenceId) => `${baseUrl}/${topic}/${sequenceId}`;
|
||||||
|
|
||||||
|
export const toNotificationParams = ({ message, defaultTitle, topicRoute, baseUrl, topic }) => {
|
||||||
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
const image = isImage(message.attachment) ? message.attachment.url : undefined;
|
||||||
|
const sequenceId = message.sequence_id || message.id;
|
||||||
|
const tag = notificationTag(baseUrl, topic, sequenceId);
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
// https://developer.mozilla.org/en-US/docs/Web/API/Notifications_API
|
||||||
return [
|
return [
|
||||||
@@ -62,7 +70,7 @@ export const toNotificationParams = ({ message, defaultTitle, topicRoute }) => {
|
|||||||
icon,
|
icon,
|
||||||
image,
|
image,
|
||||||
timestamp: message.time * 1000,
|
timestamp: message.time * 1000,
|
||||||
tag: message.sequence_id || message.id, // Update notification if there is a sequence ID
|
tag, // Scoped by baseUrl/topic/sequenceId to avoid cross-topic collisions
|
||||||
renotify: true,
|
renotify: true,
|
||||||
silent: false,
|
silent: false,
|
||||||
// This is used by the notification onclick event
|
// This is used by the notification onclick event
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNotification = async (subscriptionId, notification) => {
|
const handleNotification = async (subscription, notification) => {
|
||||||
// This logic is (partially) duplicated in
|
// This logic is (partially) duplicated in
|
||||||
// - Android: SubscriberService::onNotificationReceived()
|
// - Android: SubscriberService::onNotificationReceived()
|
||||||
// - Android: FirebaseService::onMessageReceived()
|
// - Android: FirebaseService::onMessageReceived()
|
||||||
@@ -59,20 +59,20 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
// - Web app: sw.js:handleMessage(), sw.js:handleMessageClear(), ...
|
||||||
|
|
||||||
if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) {
|
if (notification.event === EVENT_MESSAGE_DELETE && notification.sequence_id) {
|
||||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, notification.sequence_id);
|
await subscriptionManager.deleteNotificationBySequenceId(subscription.id, notification.sequence_id);
|
||||||
await notifier.cancel(notification);
|
await notifier.cancel(subscription, notification);
|
||||||
} else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) {
|
} else if (notification.event === EVENT_MESSAGE_CLEAR && notification.sequence_id) {
|
||||||
await subscriptionManager.markNotificationReadBySequenceId(subscriptionId, notification.sequence_id);
|
await subscriptionManager.markNotificationReadBySequenceId(subscription.id, notification.sequence_id);
|
||||||
await notifier.cancel(notification);
|
await notifier.cancel(subscription, notification);
|
||||||
} else {
|
} else {
|
||||||
// Regular message: delete existing and add new
|
// Regular message: delete existing and add new
|
||||||
const sequenceId = notification.sequence_id || notification.id;
|
const sequenceId = notification.sequence_id || notification.id;
|
||||||
if (sequenceId) {
|
if (sequenceId) {
|
||||||
await subscriptionManager.deleteNotificationBySequenceId(subscriptionId, sequenceId);
|
await subscriptionManager.deleteNotificationBySequenceId(subscription.id, sequenceId);
|
||||||
}
|
}
|
||||||
const added = await subscriptionManager.addNotification(subscriptionId, notification);
|
const added = await subscriptionManager.addNotification(subscription.id, notification);
|
||||||
if (added) {
|
if (added) {
|
||||||
await subscriptionManager.notify(subscriptionId, notification);
|
await subscriptionManager.notify(subscription.id, notification);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -89,7 +89,7 @@ export const useConnectionListeners = (account, subscriptions, users, webPushTop
|
|||||||
if (subscription.internal) {
|
if (subscription.internal) {
|
||||||
await handleInternalMessage(message);
|
await handleInternalMessage(message);
|
||||||
} else {
|
} else {
|
||||||
await handleNotification(subscriptionId, message);
|
await handleNotification(subscription, message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user