Compare commits

...

14 Commits

Author SHA1 Message Date
binwiederhier
5784b07f14 Bump 2023-06-28 20:23:10 -04:00
binwiederhier
8e1e0b3740 Overflow auto 2023-06-28 20:17:49 -04:00
binwiederhier
3f42e0e945 Merge branch 'main' into fix-permission-handling 2023-06-28 20:05:26 -04:00
binwiederhier
9146e439d2 Merge branch 'main' of github.com:binwiederhier/ntfy 2023-06-28 20:03:36 -04:00
binwiederhier
7a14a0b81f Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web 2023-06-28 20:03:32 -04:00
Philipp C. Heckel
9247475ab2 Merge pull request #793 from nimbleghost/pwa-action-bar
Make action bar match theme colour when run as PWA
2023-06-28 19:58:41 -04:00
nimbleghost
6b4c04c390 Make action bar match theme colour when run as PWA 2023-06-29 00:22:58 +02:00
nimbleghost
e8216ae9e7 Fix resubscribing when notifications are re-granted
(case: from denied to granted)
2023-06-29 00:02:18 +02:00
nimbleghost
365a0b2832 Fix preferences warnings 2023-06-28 23:38:57 +02:00
Philipp C. Heckel
f78389b6ef Merge pull request #792 from nimbleghost/fix-ntfy-banner
Fix ntfy upgrade banner in dark mode
2023-06-28 15:53:54 -04:00
nimbleghost
0d231d8bd9 Fix snackbars in dark mode 2023-06-28 21:18:04 +02:00
nimbleghost
d838790b8f Fix ntfy upgrade banner in dark mode 2023-06-28 20:43:42 +02:00
nimbleghost
9ce3545901 Fix refreshing things when permission is granted
We refreshed some things but not everything, this makes it more
responsive if you have the settings page open when granting permissions,
for example.
2023-06-28 20:26:54 +02:00
怪盗kidou
8db569e8a5 Translated using Weblate (Chinese (Simplified))
Currently translated at 94.5% (361 of 382 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/zh_Hans/
2023-06-28 16:52:23 +02:00
17 changed files with 198 additions and 142 deletions

View File

@@ -29,37 +29,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_x86_64.tar.gz
tar zxvf ntfy_2.6.0_linux_x86_64.tar.gz
sudo cp -a ntfy_2.6.0_linux_x86_64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_x86_64.tar.gz
tar zxvf ntfy_2.6.1_linux_x86_64.tar.gz
sudo cp -a ntfy_2.6.1_linux_x86_64/ntfy /usr/local/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.1_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv6.tar.gz
tar zxvf ntfy_2.6.0_linux_armv6.tar.gz
sudo cp -a ntfy_2.6.0_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.0_linux_armv6/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv6.tar.gz
tar zxvf ntfy_2.6.1_linux_armv6.tar.gz
sudo cp -a ntfy_2.6.1_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.1_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv7.tar.gz
tar zxvf ntfy_2.6.0_linux_armv7.tar.gz
sudo cp -a ntfy_2.6.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv7.tar.gz
tar zxvf ntfy_2.6.1_linux_armv7.tar.gz
sudo cp -a ntfy_2.6.1_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.1_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_arm64.tar.gz
tar zxvf ntfy_2.6.0_linux_arm64.tar.gz
sudo cp -a ntfy_2.6.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_arm64.tar.gz
tar zxvf ntfy_2.6.1_linux_arm64.tar.gz
sudo cp -a ntfy_2.6.1_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_2.6.1_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -109,7 +109,7 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -117,7 +117,7 @@ Manually installing the .deb file:
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv6.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -125,7 +125,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -133,7 +133,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -143,28 +143,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv6.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -192,18 +192,18 @@ NixOS also supports [declarative setup of the ntfy server](https://search.nixos.
## macOS
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.6.0/ntfy_2.6.0_macOS_all.tar.gz),
To install, please [download the tarball](https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_macOS_all.tar.gz),
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
`~/Library/Application Support/ntfy/client.yml` (sample included in the tarball).
```bash
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_macOS_all.tar.gz > ntfy_2.6.0_macOS_all.tar.gz
tar zxvf ntfy_2.6.0_macOS_all.tar.gz
sudo cp -a ntfy_2.6.0_macOS_all/ntfy /usr/local/bin/ntfy
curl -L https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_macOS_all.tar.gz > ntfy_2.6.1_macOS_all.tar.gz
tar zxvf ntfy_2.6.1_macOS_all.tar.gz
sudo cp -a ntfy_2.6.1_macOS_all/ntfy /usr/local/bin/ntfy
mkdir ~/Library/Application\ Support/ntfy
cp ntfy_2.6.0_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
cp ntfy_2.6.1_macOS_all/client/client.yml ~/Library/Application\ Support/ntfy/client.yml
ntfy --help
```
@@ -221,7 +221,7 @@ brew install ntfy
## Windows
The [ntfy CLI](subscribe/cli.md) (`ntfy publish` and `ntfy subscribe` only) is supported on Windows as well.
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.6.0/ntfy_2.6.0_windows_x86_64.zip),
To install, please [download the latest ZIP](https://github.com/binwiederhier/ntfy/releases/download/v2.6.1/ntfy_2.6.1_windows_x86_64.zip),
extract it and place the `ntfy.exe` binary somewhere in your `%Path%`.
The default path for the client config file is at `%AppData%\ntfy\client.yml` (not created automatically, sample in the ZIP file).

View File

@@ -2,7 +2,7 @@
Binaries for all releases can be found on the GitHub releases pages for the [ntfy server](https://github.com/binwiederhier/ntfy/releases)
and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/releases).
### ntfy server v2.6.0
### ntfy server v2.6.1
Released June 28, 2023
With this release, the ntfy web app now contains a **[progressive web app](https://docs.ntfy.sh/subscribe/pwa/) (PWA)
@@ -30,6 +30,7 @@ if you use promo code `MYTOPIC`). ntfy will always remain open source.
* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
* Newly created access tokens are now lowercase only to fully support `<topic>+<token>@<domain>` email syntax ([#773](https://github.com/binwiederhier/ntfy/issues/773), thanks to gingervitiz for reporting)
* The .1 release fixes a few visual issues with dark mode, and other web app updates ([#791](https://github.com/binwiederhier/ntfy/pull/791), [#793](https://github.com/binwiederhier/ntfy/pull/793), [#792](https://github.com/binwiederhier/ntfy/pull/792), thanks to [@nimbleghost](https://github.com/nimbleghost))
**Maintenance:**

8
go.mod
View File

@@ -4,7 +4,7 @@ go 1.18
require (
cloud.google.com/go/firestore v1.11.0 // indirect
cloud.google.com/go/storage v1.30.1 // indirect
cloud.google.com/go/storage v1.31.0 // indirect
github.com/BurntSushi/toml v1.3.2 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/emersion/go-smtp v0.16.0
@@ -69,9 +69,9 @@ require (
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/appengine/v2 v2.0.3 // indirect
google.golang.org/genproto v0.0.0-20230626202813-9b080da550b3 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230626202813-9b080da550b3 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230626202813-9b080da550b3 // indirect
google.golang.org/genproto v0.0.0-20230628200519-e449d1ea0e82 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230628200519-e449d1ea0e82 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82 // indirect
google.golang.org/grpc v1.56.1 // indirect
google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

8
go.sum
View File

@@ -14,6 +14,8 @@ cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tE
cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc=
cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM=
cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E=
cloud.google.com/go/storage v1.31.0 h1:+S3LjjEN2zZ+L5hOwj4+1OkGCsLVe0NzpXKQ1pSdTCI=
cloud.google.com/go/storage v1.31.0/go.mod h1:81ams1PrhW16L4kF7qg+4mTq7SRs5HsbDTM0bWvrwJ0=
firebase.google.com/go/v4 v4.11.0 h1:szjBoiF33A2FavRLIDZjW1mw+OsW/XAtHoYNIqWOjRk=
firebase.google.com/go/v4 v4.11.0/go.mod h1:60c36dWLK4+j05Vw5XMllek3b3PCynU3BfI46OSwsUE=
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
@@ -244,10 +246,16 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
google.golang.org/genproto v0.0.0-20230626202813-9b080da550b3 h1:Yofj1/U0xc/Zi5KEpoIxm51I2f85X+eGyY4YzAujRdw=
google.golang.org/genproto v0.0.0-20230626202813-9b080da550b3/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
google.golang.org/genproto v0.0.0-20230628200519-e449d1ea0e82 h1:Wdfp5Hc1bqGCWYZNrir4A1Jb+SmVaV2j1DL/pbMMTGI=
google.golang.org/genproto v0.0.0-20230628200519-e449d1ea0e82/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64=
google.golang.org/genproto/googleapis/api v0.0.0-20230626202813-9b080da550b3 h1:wl7z+A0jkB3Rl8Hz74SqGDlnnn5VlL2CV+9UTdZOo00=
google.golang.org/genproto/googleapis/api v0.0.0-20230626202813-9b080da550b3/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/api v0.0.0-20230628200519-e449d1ea0e82 h1:iI5Fmsfz4zDINYxJLxn2YChI//ypkHM/KuVSvlN7ZXk=
google.golang.org/genproto/googleapis/api v0.0.0-20230628200519-e449d1ea0e82/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230626202813-9b080da550b3 h1:QJuqz7YzNTyKDspkp2lrzqtq4lf2AhUSpXTsGP5SbLw=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230626202813-9b080da550b3/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82 h1:6b+zGQBiXFlAMpQr+cCarAdrZD4QgXSG7uUZadYysgg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20230628200519-e449d1ea0e82/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=

6
web/package-lock.json generated
View File

@@ -3441,9 +3441,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.442",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.442.tgz",
"integrity": "sha512-RkrZF//Ya+0aJq2NM3OdisNh5ZodZq1rdXOS96G8DdDgpDKqKE81yTbbQ3F/4CKm1JBPsGu1Lp/akkna2xO06Q==",
"version": "1.4.444",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.444.tgz",
"integrity": "sha512-/AjkL4syRvOpowXWy3N4OHmVbFdWQpfSKHh0sCVYipDeEAtMce3rLjMJi/27Ia9jNIbw6P1JuPN32pSWybXXEQ==",
"dev": true
},
"node_modules/emoji-regex": {

View File

@@ -55,14 +55,14 @@
"nav_upgrade_banner_label": "Upgrade to ntfy Pro",
"nav_upgrade_banner_description": "Reserve topics, more messages & emails, and larger attachments",
"alert_notification_permission_required_title": "Notifications are disabled",
"alert_notification_permission_required_description": "Grant your browser permission to display desktop notifications.",
"alert_notification_permission_required_description": "Grant your browser permission to display desktop notifications",
"alert_notification_permission_required_button": "Grant now",
"alert_notification_permission_denied_title": "Notifications are blocked",
"alert_notification_permission_denied_description": "Please re-enable them in your browser and refresh the page to receive notifications",
"alert_notification_permission_denied_description": "Please re-enable them in your browser",
"alert_notification_ios_install_required_title": "iOS install required",
"alert_notification_ios_install_required_description": "Click on the Share icon and Add to Home Screen to enable notifications on iOS",
"alert_not_supported_title": "Notifications not supported",
"alert_not_supported_description": "Notifications are not supported in your browser.",
"alert_not_supported_description": "Notifications are not supported in your browser",
"alert_not_supported_context_description": "Notifications are only supported over HTTPS. This is a limitation of the <mdnLink>Notifications API</mdnLink>.",
"notifications_list": "Notifications list",
"notifications_list_item": "Notification",

View File

@@ -352,5 +352,12 @@
"account_upgrade_dialog_tier_price_billed_monthly": "{{price}} 每年。按月计费。",
"account_upgrade_dialog_tier_price_billed_yearly": "{{价格}} 按年计费。节省 {{save}}。",
"account_upgrade_dialog_billing_contact_email": "有关账单问题,请直接<Link>联系我们 </Link>。",
"account_upgrade_dialog_billing_contact_website": "有关账单问题,请参考我们的<Link>网站 </Link>。"
"account_upgrade_dialog_billing_contact_website": "有关账单问题,请参考我们的<Link>网站 </Link>。",
"publish_dialog_call_item": "拨打电话 {{number}}",
"publish_dialog_call_label": "拨号",
"publish_dialog_chip_call_label": "拨号",
"publish_dialog_chip_call_no_verified_numbers_tooltip": "未验证的手机号",
"account_basics_phone_numbers_title": "电话号码",
"account_basics_phone_numbers_description": "电话通知",
"account_basics_phone_numbers_dialog_description": "要使用来电通知功能,您需要添加并验证至少一个电话号码。可以通过短信或电话进行验证。"
}

View File

@@ -44,9 +44,6 @@ class Notifier {
}
async webPushSubscription(hasWebPushTopics) {
if (!this.pushPossible()) {
throw new Error("Unsupported or denied");
}
const pushManager = await this.pushManager();
const existingSubscription = await pushManager.getSubscription();
if (existingSubscription) {

View File

@@ -27,7 +27,7 @@ class SubscriptionManager {
* It is important to note that "mutedUntil" must be part of the where() query, otherwise the Dexie live query
* will not react to it, and the Web Push topics will not be updated when the user mutes a topic.
*/
async webPushTopics(pushPossible = notifier.pushPossible()) {
async webPushTopics(pushPossible) {
if (!pushPossible) {
return [];
}
@@ -120,13 +120,14 @@ class SubscriptionManager {
);
}
async updateWebPushSubscriptions(presetTopics) {
const topics = presetTopics ?? (await this.webPushTopics());
async updateWebPushSubscriptions(topics) {
const hasWebPushTopics = topics.length > 0;
const browserSubscription = await notifier.webPushSubscription(hasWebPushTopics);
if (!browserSubscription) {
console.log("[SubscriptionManager] No browser subscription currently exists, so web push was never enabled. Skipping.");
console.log(
"[SubscriptionManager] No browser subscription currently exists, so web push was never enabled or the notification permission was removed. Skipping."
);
return;
}

View File

@@ -787,7 +787,7 @@ const Tokens = () => {
}}
/>
</Paragraph>
<div style={{ width: "100%", overflowX: "scroll" }}>{tokens?.length > 0 && <TokensTable tokens={tokens} />}</div>
<div style={{ width: "100%", overflowX: "auto" }}>{tokens?.length > 0 && <TokensTable tokens={tokens} />}</div>
</CardContent>
<CardActions>
<Button onClick={handleCreateClick}>{t("account_tokens_table_create_token_button")}</Button>

View File

@@ -19,11 +19,14 @@ import Navigation from "./Navigation";
import accountApi from "../app/AccountApi";
import PopupMenu from "./PopupMenu";
import { SubscriptionPopup } from "./SubscriptionPopup";
import { useIsLaunchedPWA } from "./hooks";
const ActionBar = (props) => {
const theme = useTheme();
const { t } = useTranslation();
const location = useLocation();
const isLaunchedPWA = useIsLaunchedPWA();
let title = "ntfy";
if (props.selected) {
title = topicDisplayName(props.selected);
@@ -32,6 +35,22 @@ const ActionBar = (props) => {
} else if (location.pathname === routes.account) {
title = t("action_bar_account");
}
const getActionBarBackground = () => {
if (isLaunchedPWA) {
return "#317f6f";
}
switch (theme.palette.mode) {
case "dark":
return "linear-gradient(150deg, #203631 0%, #2a6e60 100%)";
case "light":
default:
return "linear-gradient(150deg, #338574 0%, #56bda8 100%)";
}
};
return (
<AppBar
position="fixed"
@@ -44,7 +63,7 @@ const ActionBar = (props) => {
<Toolbar
sx={{
pr: "24px",
background: theme.palette.actionBarBackground,
background: getActionBarBackground(),
}}
>
<IconButton

View File

@@ -5,10 +5,9 @@ import { ThemeProvider, createTheme } from "@mui/material/styles";
import { useLiveQuery } from "dexie-react-hooks";
import { BrowserRouter, Outlet, Route, Routes, useParams } from "react-router-dom";
import { AllSubscriptions, SingleSubscription } from "./Notifications";
import themeOptions, { darkPalette, lightPalette } from "./theme";
import { darkTheme, lightTheme } from "./theme";
import Navigation from "./Navigation";
import ActionBar from "./ActionBar";
import notifier from "../app/Notifier";
import Preferences from "./Preferences";
import subscriptionManager from "../app/SubscriptionManager";
import userManager from "../app/UserManager";
@@ -46,13 +45,7 @@ const App = () => {
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
const themePreference = useLiveQuery(() => prefs.theme());
const theme = React.useMemo(
() =>
createTheme({
...themeOptions,
palette: {
...(darkModeEnabled(prefersDarkMode, themePreference) ? darkPalette : lightPalette),
},
}),
() => createTheme(darkModeEnabled(prefersDarkMode, themePreference) ? darkTheme : lightTheme),
[prefersDarkMode, themePreference]
);
@@ -91,7 +84,6 @@ const Layout = () => {
const params = useParams();
const { account, setAccount } = useContext(AccountContext);
const [mobileDrawerOpen, setMobileDrawerOpen] = useState(false);
const [notificationsGranted, setNotificationsGranted] = useState(notifier.granted());
const [sendDialogOpenMode, setSendDialogOpenMode] = useState("");
const users = useLiveQuery(() => userManager.all());
const subscriptions = useLiveQuery(() => subscriptionManager.all());
@@ -115,10 +107,8 @@ const Layout = () => {
<Navigation
subscriptions={subscriptionsWithoutInternal}
selectedSubscription={selected}
notificationsGranted={notificationsGranted}
mobileDrawerOpen={mobileDrawerOpen}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
onNotificationGranted={setNotificationsGranted}
onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
/>
<Main>

View File

@@ -18,6 +18,7 @@ import {
Box,
IconButton,
Button,
useTheme,
} from "@mui/material";
import * as React from "react";
import { useContext, useState } from "react";
@@ -43,6 +44,7 @@ import UpgradeDialog from "./UpgradeDialog";
import { AccountContext } from "./App";
import { PermissionDenyAll, PermissionRead, PermissionReadWrite, PermissionWrite } from "./ReserveIcons";
import { SubscriptionPopup } from "./SubscriptionPopup";
import { useNotificationPermissionListener } from "./hooks";
const navWidth = 280;
@@ -82,6 +84,7 @@ const Navigation = (props) => {
Navigation.width = navWidth;
const NavList = (props) => {
const theme = useTheme();
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
@@ -109,17 +112,12 @@ const NavList = (props) => {
const isPaid = account?.billing?.subscription;
const showUpgradeBanner = config.enable_payments && !isAdmin && !isPaid;
const showSubscriptionsList = props.subscriptions?.length > 0;
const [showNotificationPermissionRequired, setShowNotificationPermissionRequired] = useState(notifier.notRequested());
const [showNotificationPermissionDenied, setShowNotificationPermissionDenied] = useState(notifier.denied());
const showNotificationPermissionRequired = useNotificationPermissionListener(() => notifier.notRequested());
const showNotificationPermissionDenied = useNotificationPermissionListener(() => notifier.denied());
const showNotificationIOSInstallRequired = notifier.iosSupportedButInstallRequired();
const showNotificationBrowserNotSupportedBox = !showNotificationIOSInstallRequired && !notifier.browserSupported();
const showNotificationContextNotSupportedBox = notifier.browserSupported() && !notifier.contextSupported(); // Only show if notifications are generally supported in the browser
const refreshPermissions = () => {
setShowNotificationPermissionRequired(notifier.notRequested());
setShowNotificationPermissionDenied(notifier.denied());
};
const alertVisible =
showNotificationPermissionRequired ||
showNotificationPermissionDenied ||
@@ -130,8 +128,8 @@ const NavList = (props) => {
return (
<>
<Toolbar sx={{ display: { xs: "none", sm: "block" } }} />
<List component="nav" sx={{ paddingTop: alertVisible ? "0" : "" }}>
{showNotificationPermissionRequired && <NotificationPermissionRequired refreshPermissions={refreshPermissions} />}
<List component="nav" sx={{ paddingTop: { xs: 0, sm: alertVisible ? 0 : "" } }}>
{showNotificationPermissionRequired && <NotificationPermissionRequired />}
{showNotificationPermissionDenied && <NotificationPermissionDeniedAlert />}
{showNotificationBrowserNotSupportedBox && <NotificationBrowserNotSupportedAlert />}
{showNotificationContextNotSupportedBox && <NotificationContextNotSupportedAlert />}
@@ -190,7 +188,11 @@ const NavList = (props) => {
</ListItemIcon>
<ListItemText primary={t("nav_button_subscribe")} />
</ListItemButton>
{showUpgradeBanner && <UpgradeBanner />}
{showUpgradeBanner && (
// The text background gradient didn't seem to do well with switching between light/dark mode,
// So adding a `key` forces React to replace the entire component when the theme changes
<UpgradeBanner key={`upgrade-banner-${theme.palette.mode}`} mode={theme.palette.mode} />
)}
</List>
<SubscribeDialog
key={`subscribeDialog${subscribeDialogKey}`} // Resets dialog when canceled/closed
@@ -203,7 +205,7 @@ const NavList = (props) => {
);
};
const UpgradeBanner = () => {
const UpgradeBanner = ({ mode }) => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
@@ -220,13 +222,16 @@ const UpgradeBanner = () => {
width: `${Navigation.width - 1}px`,
bottom: 0,
mt: "auto",
background: "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)",
background:
mode === "light"
? "linear-gradient(150deg, rgba(196, 228, 221, 0.46) 0%, rgb(255, 255, 255) 100%)"
: "linear-gradient(150deg, #203631 0%, #2a6e60 100%)",
}}
>
<Divider />
<ListItemButton onClick={handleClick} sx={{ pt: 2, pb: 2 }}>
<ListItemIcon>
<CelebrationIcon sx={{ color: "#55b86e" }} fontSize="large" />
<CelebrationIcon sx={{ color: mode === "light" ? "#55b86e" : "#00ff95" }} fontSize="large" />
</ListItemIcon>
<ListItemText
sx={{ ml: 1 }}
@@ -236,7 +241,10 @@ const UpgradeBanner = () => {
style: {
fontWeight: 500,
fontSize: "1.1rem",
background: "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)",
background:
mode === "light"
? "-webkit-linear-gradient(45deg, #09009f, #00ff95 80%)"
: "-webkit-linear-gradient(45deg,rgb(255, 255, 255), #00ff95 80%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
},
@@ -354,11 +362,10 @@ const SubscriptionItem = (props) => {
);
};
const NotificationPermissionRequired = ({ refreshPermissions }) => {
const NotificationPermissionRequired = () => {
const { t } = useTranslation();
const requestPermission = async () => {
await notifier.maybeRequestPermission();
refreshPermissions();
};
return (
<Alert severity="warning" sx={{ paddingTop: 2 }}>

View File

@@ -11,14 +11,14 @@ const PrefRow = styled("div")`
display: flex;
flex-direction: row;
> :first-child {
> div:first-of-type {
flex: 1 0 40%;
display: flex;
flex-direction: column;
justify-content: ${(props) => (props.alignTop ? "normal" : "center")};
}
> :last-child {
> div:last-of-type {
flex: 1 0 calc(60% - 50px);
display: flex;
flex-direction: column;
@@ -29,12 +29,12 @@ const PrefRow = styled("div")`
flex-direction: column;
gap: 10px;
> :first-child,
> :last-child {
> :div:first-of-type,
> :div:last-of-type {
flex: unset;
}
> :last-child {
> div:last-of-type {
.MuiFormControl-root {
margin: 0;
}

View File

@@ -49,7 +49,7 @@ import { ReserveAddDialog, ReserveDeleteDialog, ReserveEditDialog } from "./Rese
import { UnauthorizedError } from "../app/errors";
import { subscribeTopic } from "./SubscribeDialog";
import notifier from "../app/Notifier";
import { useIsLaunchedPWA } from "./hooks";
import { useIsLaunchedPWA, useNotificationPermissionListener } from "./hooks";
const maybeUpdateAccountSettings = async (payload) => {
if (!session.exists()) {
@@ -79,6 +79,7 @@ const Preferences = () => (
const Notifications = () => {
const { t } = useTranslation();
const isLaunchedPWA = useIsLaunchedPWA();
const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible());
return (
<Card sx={{ p: 3 }} aria-label={t("prefs_notifications_title")}>
@@ -89,7 +90,7 @@ const Notifications = () => {
<Sound />
<MinPriority />
<DeleteAfter />
{!isLaunchedPWA && notifier.pushPossible() && <WebPushEnabled />}
{!isLaunchedPWA && pushPossible && <WebPushEnabled />}
</PrefGroup>
</Card>
);
@@ -240,7 +241,7 @@ const DeleteAfter = () => {
const Theme = () => {
const { t } = useTranslation();
const labelId = "prefTheme";
const enabled = useLiveQuery(async () => prefs.theme());
const theme = useLiveQuery(async () => prefs.theme());
const handleChange = async (ev) => {
await prefs.setTheme(ev.target.value);
};
@@ -248,7 +249,7 @@ const Theme = () => {
return (
<Pref labelId={labelId} title={t("prefs_appearance_theme_title")}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={enabled ?? false} onChange={handleChange} aria-labelledby={labelId}>
<Select value={theme ?? THEME.SYSTEM} onChange={handleChange} aria-labelledby={labelId}>
<MenuItem value={THEME.SYSTEM}>{t("prefs_appearance_theme_system")}</MenuItem>
<MenuItem value={THEME.DARK}>{t("prefs_appearance_theme_dark")}</MenuItem>
<MenuItem value={THEME.LIGHT}>{t("prefs_appearance_theme_light")}</MenuItem>

View File

@@ -136,8 +136,31 @@ export const useAutoSubscribe = (subscriptions, selected) => {
};
const webPushBroadcastChannel = new BroadcastChannel("web-push-broadcast");
const matchMedia = window.matchMedia("(display-mode: standalone)");
const isIOSStandalone = window.navigator.standalone === true;
/**
* Hook to return a value that's refreshed when the notification permission changes
*/
export const useNotificationPermissionListener = (query) => {
const [result, setResult] = useState(query());
useEffect(() => {
const handler = () => {
setResult(query());
};
if ("permissions" in navigator) {
navigator.permissions.query({ name: "notifications" }).then((permission) => {
permission.addEventListener("change", handler);
return () => {
permission.removeEventListener("change", handler);
};
});
}
}, []);
return result;
};
/**
* Updates the Web Push subscriptions when the list of topics changes,
@@ -145,11 +168,12 @@ const isIOSStandalone = window.navigator.standalone === true;
* the service worker, since the service worker cannot play sounds.
*/
const useWebPushListener = (topics) => {
const [lastTopics, setLastTopics] = useState();
const [prevUpdate, setPrevUpdate] = useState();
const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible());
useEffect(() => {
const topicsChanged = JSON.stringify(topics) !== JSON.stringify(lastTopics);
if (!notifier.pushPossible() || !topicsChanged) {
const nextUpdate = JSON.stringify({ topics, pushPossible });
if (topics === undefined || nextUpdate === prevUpdate) {
return;
}
@@ -157,12 +181,12 @@ const useWebPushListener = (topics) => {
try {
console.log("[useWebPushListener] Refreshing web push subscriptions", topics);
await subscriptionManager.updateWebPushSubscriptions(topics);
setLastTopics(topics);
setPrevUpdate(nextUpdate);
} catch (e) {
console.error("[useWebPushListener] Error refreshing web push subscriptions", e);
}
})();
}, [topics, lastTopics]);
}, [topics, pushPossible, prevUpdate]);
useEffect(() => {
const onMessage = () => {
@@ -183,25 +207,7 @@ const useWebPushListener = (topics) => {
* automatically.
*/
export const useWebPushTopics = () => {
const [pushPossible, setPushPossible] = useState(notifier.pushPossible());
useEffect(() => {
const handler = () => {
const newPushPossible = notifier.pushPossible();
console.log(`[useWebPushTopics] Notification Permission changed`, { pushPossible: newPushPossible });
setPushPossible(newPushPossible);
};
if ("permissions" in navigator) {
navigator.permissions.query({ name: "notifications" }).then((permission) => {
permission.addEventListener("change", handler);
return () => {
permission.removeEventListener("change", handler);
};
});
}
});
const pushPossible = useNotificationPermissionListener(() => notifier.pushPossible());
const topics = useLiveQuery(
async () => subscriptionManager.webPushTopics(pushPossible),
@@ -214,6 +220,9 @@ export const useWebPushTopics = () => {
return topics;
};
const matchMedia = window.matchMedia("(display-mode: standalone)");
const isIOSStandalone = window.navigator.standalone === true;
/*
* Watches the "display-mode" to detect if the app is running as a standalone app (PWA).
*/

View File

@@ -1,5 +1,5 @@
/** @type {import("@mui/material").ThemeOptions} */
const themeOptions = {
const baseThemeOptions = {
components: {
MuiListItemIcon: {
styleOverrides: {
@@ -22,37 +22,53 @@ const themeOptions = {
// https://github.com/binwiederhier/ntfy-android/blob/main/app/src/main/res/values/colors.xml
/** @type {import("@mui/material").ThemeOptions['palette']} */
export const lightPalette = {
mode: "light",
primary: {
main: "#338574",
/** @type {import("@mui/material").ThemeOptions} */
export const lightTheme = {
...baseThemeOptions,
components: {
...baseThemeOptions.components,
},
secondary: {
main: "#6cead0",
palette: {
mode: "light",
primary: {
main: "#338574",
},
secondary: {
main: "#6cead0",
},
error: {
main: "#c30000",
},
},
error: {
main: "#c30000",
},
actionBarBackground: "linear-gradient(150deg, #338574 0%, #56bda8 100%)",
};
/** @type {import("@mui/material").ThemeOptions['palette']} */
export const darkPalette = {
mode: "dark",
background: {
paper: "#1b2124",
/** @type {import("@mui/material").ThemeOptions} */
export const darkTheme = {
...baseThemeOptions,
components: {
...baseThemeOptions.components,
MuiSnackbarContent: {
styleOverrides: {
root: {
color: "#000",
backgroundColor: "#aeaeae",
},
},
},
},
primary: {
main: "#65b5a3",
palette: {
mode: "dark",
background: {
paper: "#1b2124",
},
primary: {
main: "#65b5a3",
},
secondary: {
main: "#6cead0",
},
error: {
main: "#fe4d2e",
},
},
secondary: {
main: "#6cead0",
},
error: {
main: "#fe4d2e",
},
actionBarBackground: "linear-gradient(150deg, #203631 0%, #2a6e60 100%)",
};
export default themeOptions;