Compare commits

...

112 Commits

Author SHA1 Message Date
Philipp Heckel
66e46aaded Add Docker build for ARMv6, bump again 2022-04-24 22:25:34 -04:00
Philipp Heckel
ddf5d49895 Update install instructions, bump version 2022-04-24 22:09:58 -04:00
Philipp Heckel
899d895f29 Add ARMv6 install instructions 2022-04-24 21:53:08 -04:00
Philipp Heckel
27588b8a48 Makefile for ARMv6 2022-04-24 21:41:40 -04:00
Philipp Heckel
c3f4adb777 Revert urfave/cli, see https://github.com/urfave/cli/issues/1373 2022-04-24 21:20:49 -04:00
Philipp Heckel
3633503549 Bump version, update deps 2022-04-24 20:32:17 -04:00
Philipp Heckel
5494bcce88 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-24 20:19:03 -04:00
Tiago Esperança Triques
b824a1f17f Translated using Weblate (Portuguese (Brazil))
Currently translated at 23.6% (36 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/pt_BR/
2022-04-25 02:18:20 +02:00
Philipp Heckel
5a6b6dacc0 Changelog 2022-04-24 20:01:14 -04:00
Tiago Esperança Triques
0c6e9d4fca Added translation using Weblate (Portuguese (Brazil)) 2022-04-24 23:44:03 +02:00
Philipp Heckel
0d0ca188bf Fix changelog 2022-04-23 20:48:44 -04:00
Philipp Heckel
9c91ae2744 Make sure clear= values are checked 2022-04-23 15:23:18 -04:00
Philipp Heckel
882f027f6c Changelog 2022-04-23 13:45:26 -04:00
Philipp Heckel
b805d49cfd Disallow HEAD/GET requests with body 2022-04-23 13:40:26 -04:00
Philipp Heckel
12f85cceb1 Add clear=true|false support to actions 2022-04-22 23:07:35 -04:00
Philipp Heckel
8f4a1db1f0 Changelog, add tests 2022-04-22 14:51:44 -04:00
Philipp Heckel
58bde32bfb Fix docs 2022-04-22 13:53:32 -04:00
Philipp Heckel
26b3aa27ae Add proper screenshot 2022-04-21 20:35:25 -04:00
Philipp Heckel
26ebd23bfd Add user actions to web app 2022-04-21 16:33:49 -04:00
Philipp Heckel
12d347976c Docs docs docs 2022-04-21 13:57:42 -04:00
Philipp Heckel
a779434bab More docs 2022-04-21 09:58:28 -04:00
Philipp Heckel
c5ec3b48b4 Ahhh 2022-04-20 19:15:15 -04:00
Philipp Heckel
712c292183 More docs 2022-04-20 16:31:25 -04:00
Philipp Heckel
8900df27c9 Docs, still WIP 2022-04-19 23:26:46 -04:00
Philipp Heckel
0eb511c714 Merge branch 'main' into actions 2022-04-19 19:37:35 -04:00
Philipp Heckel
d48eec5e66 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-19 19:37:19 -04:00
Philipp Heckel
3a7fd7a620 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into actions 2022-04-19 19:37:13 -04:00
Philipp Heckel
85043b34a4 Merge branch 'main' into actions 2022-04-19 19:33:25 -04:00
Philipp Heckel
2df0e98749 Added Russian to changelog + web app 2022-04-19 19:31:50 -04:00
Philipp Heckel
3c3b2477af Docs (WIP), Firebase 2022-04-19 19:22:19 -04:00
Philipp Heckel
5a9b2122c2 Make simple actions parsing work 2022-04-19 09:14:32 -04:00
Ilya Mikheev
37e72e078d Translated using Weblate (Russian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-19 09:12:56 +02:00
Aleksej Muratov
a2dafc11f2 Translated using Weblate (Russian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-19 09:12:56 +02:00
Philipp Heckel
55869f551e Add ID 2022-04-17 14:29:43 -04:00
Philipp Heckel
967cde1fb5 JSON format 2022-04-16 20:12:01 -04:00
Philipp Heckel
26efd481e3 WIP Actions 2022-04-16 16:17:58 -04:00
Philipp Heckel
06fd7327de Docs for DND override 2022-04-15 15:39:58 -04:00
Philipp Heckel
a08d57ca0f Changelog 2022-04-15 12:19:48 -04:00
Philipp Heckel
c60c51871d Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-15 12:16:24 -04:00
Erik S
04c4150283 Translated using Weblate (Russian)
Currently translated at 42.7% (65 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ru/
2022-04-15 18:16:18 +02:00
109247019824
1feb038385 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-15 18:16:17 +02:00
Erik S
61a5b0dbe9 Added translation using Weblate (Russian) 2022-04-14 09:02:20 +02:00
Philipp Heckel
690cd683f0 Changelog 2022-04-13 21:38:56 -04:00
Philipp Heckel
c87c81f663 Add WebSockets support to docs/config 2022-04-13 14:06:20 -04:00
Philipp Heckel
8190d5b1f4 Added docs for "Share to topic" and "ntfy:// links" 2022-04-12 20:10:05 -04:00
Philipp Heckel
75c11371e6 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-11 22:00:14 -04:00
Rogelio Dominguez
ffa0bf05cd Translated using Weblate (Spanish)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-12 04:00:09 +02:00
Philipp Heckel
8e1c57af25 Added Norwegian 2022-04-11 20:18:18 -04:00
Shoshin Akamine
c62916a43c Translated using Weblate (Japanese)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2022-04-12 01:56:34 +02:00
Linerly
f5145ffaae Translated using Weblate (Indonesian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2022-04-12 01:56:34 +02:00
109247019824
0a6aba1ac7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-12 01:56:33 +02:00
Allan Nordhøy
5d30246c35 Translated using Weblate (Norwegian Bokmål)
Currently translated at 76.3% (116 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2022-04-12 01:56:33 +02:00
Oğuz Ersen
e9386ecfe3 Translated using Weblate (Turkish)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-12 01:56:31 +02:00
Christian Meis
04f5d4acb7 Translated using Weblate (German)
Currently translated at 100.0% (152 of 152 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-12 01:56:31 +02:00
Philipp Heckel
841c08fcb6 Adding Spanish translation 2022-04-10 15:21:13 -04:00
Philipp Heckel
2d7c354723 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-10 15:16:11 -04:00
Rogelio Dominguez
6fec79055e Translated using Weblate (Spanish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-10 21:16:06 +02:00
109247019824
f61a8f82a7 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-10 21:16:05 +02:00
Philipp Heckel
136883fd94 Additional descriptions for settings (#203), URL validation (#204) 2022-04-10 15:13:12 -04:00
Philipp Heckel
9c3f5929c7 Changelog 2022-04-09 15:12:03 -04:00
Philipp Heckel
39bd1fe164 Added Japanese + Indonesian to web app 2022-04-09 10:54:09 -04:00
Philipp Heckel
67ea467501 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-09 10:47:47 -04:00
Shoshin Akamine
ed946195e2 Translated using Weblate (Japanese)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/ja/
2022-04-09 16:47:41 +02:00
Linerly
84bf95fa85 Translated using Weblate (Indonesian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/id/
2022-04-09 16:47:39 +02:00
109247019824
cf9ba9b1f9 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-09 16:47:38 +02:00
Oğuz Ersen
1a18ce9e21 Translated using Weblate (Turkish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-09 16:47:37 +02:00
Philipp Heckel
044b717f86 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-09 10:46:00 -04:00
Shoshin Akamine
8777718afc Added translation using Weblate (Japanese) 2022-04-09 09:12:08 +02:00
Linerly
8e3910c76d Added translation using Weblate (Indonesian) 2022-04-09 04:39:31 +02:00
Philipp Heckel
448444eccf Show snack bar error message when publishing fails, closes #205 2022-04-08 20:24:11 -04:00
Philipp Heckel
65cd380527 Service URL 2022-04-08 19:31:50 -04:00
J. Lavoie
71a49ac1a6 Translated using Weblate (French)
Currently translated at 39.4% (54 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/
2022-04-09 01:30:55 +02:00
Rogelio Dominguez
1fba62276c Translated using Weblate (Spanish)
Currently translated at 13.1% (18 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-09 01:30:55 +02:00
Christian Meis
29f265be30 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-09 00:10:19 +02:00
Christian Meis
4c9011f391 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-09 00:01:15 +02:00
Christian Meis
155475422e Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:53:10 +02:00
Christian Meis
32353e0f02 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:51:11 +02:00
Christian Meis
69159b9aae Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:43:22 +02:00
J. Lavoie
b47d0ac240 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:43:22 +02:00
J. Lavoie
d14af78403 Translated using Weblate (French)
Currently translated at 16.0% (22 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/fr/
2022-04-08 23:29:05 +02:00
Rogelio Dominguez
9cb08036ef Translated using Weblate (Spanish)
Currently translated at 10.9% (15 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/es/
2022-04-08 23:29:04 +02:00
109247019824
e0da6b1302 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 23:29:04 +02:00
Christian Meis
fcb1f938b9 Translated using Weblate (German)
Currently translated at 98.5% (135 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 23:29:04 +02:00
J. Lavoie
9c094c1cc3 Added translation using Weblate (French) 2022-04-08 23:25:41 +02:00
Rogelio Dominguez
69c6f24d97 Added translation using Weblate (Spanish) 2022-04-08 21:57:48 +02:00
Philipp Heckel
e8b020ff45 Replace placeholders 2022-04-08 15:26:14 -04:00
Philipp Heckel
2ec9a7307e Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 15:21:54 -04:00
Philipp Heckel
738ee5cf35 Suggested fixes for delay string, widen priority dropdown, add German and Turkish 2022-04-08 15:21:22 -04:00
109247019824
8144d39e29 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 21:21:05 +02:00
Oğuz Ersen
788d5e9f9b Translated using Weblate (Turkish)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 21:21:05 +02:00
Philipp Heckel
d399d2fe1c Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 14:56:15 -04:00
109247019824
615b09a774 Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 20:56:03 +02:00
Oğuz Ersen
7a5e8cc44b Translated using Weblate (Turkish)
Currently translated at 7.2% (10 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 20:56:03 +02:00
Christian Meis
291b49488b Translated using Weblate (German)
Currently translated at 97.8% (134 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/de/
2022-04-08 20:56:02 +02:00
Philipp Heckel
aa58242551 Update language array to match finished languages 2022-04-08 12:54:53 -04:00
Philipp Heckel
b08ea2c416 Merge branch 'main' of https://hosted.weblate.org/git/ntfy/web into main 2022-04-08 12:47:28 -04:00
109247019824
98f02f78db Translated using Weblate (Bulgarian)
Currently translated at 100.0% (137 of 137 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/bg/
2022-04-08 18:47:19 +02:00
Philipp Heckel
d2f933e15f Fix English language strings, as per #203 2022-04-08 12:45:41 -04:00
109247019824
d672969840 Added translation using Weblate (Bulgarian) 2022-04-08 17:00:21 +02:00
Allan Nordhøy
8c4f0c1253 Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (1 of 1 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/nb_NO/
2022-04-08 16:48:46 +02:00
Allan Nordhøy
18c88e567c Added translation using Weblate (Norwegian Bokmål) 2022-04-08 16:48:46 +02:00
Oğuz Ersen
2c5505852e Translated using Weblate (Turkish)
Currently translated at 100.0% (1 of 1 strings)

Translation: ntfy/Web app
Translate-URL: https://hosted.weblate.org/projects/ntfy/web/tr/
2022-04-08 16:48:46 +02:00
Oğuz Ersen
bc8f245064 Added translation using Weblate (Turkish) 2022-04-08 16:48:46 +02:00
Philipp Heckel
30726144b8 Finish web app translation 2022-04-08 10:44:35 -04:00
Philipp Heckel
893701c07b Extracting translation strings 2022-04-07 21:46:33 -04:00
Philipp Heckel
0ec9a4c89b Changelog 2022-04-07 20:33:15 -04:00
Philipp Heckel
96fb7e2296 Working language switcher 2022-04-07 20:31:24 -04:00
Philipp Heckel
750e390b5d WIP: Translation of web app 2022-04-07 19:11:51 -04:00
Philipp Heckel
7a8cfb5f66 Changelog 2022-04-07 15:41:44 -04:00
Philipp Heckel
d761ce929c Merge branch 'armv6' into main 2022-04-07 13:35:13 -04:00
Philipp Heckel
29969582e9 Update go.sum 2022-04-06 20:50:36 -04:00
Philipp Heckel
e22ec2c505 ARMv6 2022-04-06 19:45:30 -04:00
54 changed files with 4973 additions and 1308 deletions

View File

@@ -16,6 +16,20 @@ builds:
hooks:
post:
- upx "{{ .Path }}" # apt install upx
-
id: ntfy_armv6
binary: ntfy
env:
- CGO_ENABLED=1 # required for go-sqlite3
- CC=arm-linux-gnueabi-gcc # apt install gcc-arm-linux-gnueabi
tags: [sqlite_omit_load_extension,osusergo,netgo]
ldflags:
- "-linkmode=external -extldflags=-static -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}}"
goos: [linux]
goarch: [arm]
goarm: [6]
# No "upx", since it causes random core dumps, see
# https://github.com/binwiederhier/ntfy/issues/191#issuecomment-1083406546
-
id: ntfy_armv7
binary: ntfy
@@ -124,14 +138,24 @@ dockers:
goarm: 7
build_flag_templates:
- "--platform=linux/arm/v7"
- image_templates:
- &armv6_image "binwiederhier/ntfy:{{ .Tag }}-armv6"
use: buildx
dockerfile: Dockerfile
goarch: arm
goarm: 6
build_flag_templates:
- "--platform=linux/arm/v6"
docker_manifests:
- name_template: "binwiederhier/ntfy:latest"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image
- name_template: "binwiederhier/ntfy:{{ .Tag }}"
image_templates:
- *amd64_image
- *arm64v8_image
- *armv7_image
- *armv6_image

View File

@@ -18,6 +18,7 @@ help:
@echo "Build server & client (not release version):"
@echo " make server - Build server & client (all architectures)"
@echo " make server-amd64 - Build server & client (amd64 only)"
@echo " make server-armv6 - Build server & client (armv6 only)"
@echo " make server-armv7 - Build server & client (armv7 only)"
@echo " make server-arm64 - Build server & client (arm64 only)"
@echo
@@ -51,9 +52,11 @@ help:
@echo
@echo "Install locally (requires sudo):"
@echo " make install-amd64 - Copy amd64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv6 - Copy armv6 binary from dist/ to /usr/bin/ntfy"
@echo " make install-armv7 - Copy armv7 binary from dist/ to /usr/bin/ntfy"
@echo " make install-arm64 - Copy arm64 binary from dist/ to /usr/bin/ntfy"
@echo " make install-deb-amd64 - Install .deb from dist/ (amd64 only)"
@echo " make install-deb-armv6 - Install .deb from dist/ (armv6 only)"
@echo " make install-deb-armv7 - Install .deb from dist/ (armv7 only)"
@echo " make install-deb-arm64 - Install .deb from dist/ (arm64 only)"
@@ -104,7 +107,10 @@ server: server-deps
server-amd64: server-deps-static-sites
goreleaser build --snapshot --rm-dist --debug --id ntfy_amd64
server-armv7: server-deps-static-sites server-deps-gcc-armv7
server-armv6: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv6
server-armv7: server-deps-static-sites server-deps-gcc-armv6-armv7
goreleaser build --snapshot --rm-dist --debug --id ntfy_armv7
server-arm64: server-deps-static-sites server-deps-gcc-arm64
@@ -112,7 +118,7 @@ server-arm64: server-deps-static-sites server-deps-gcc-arm64
server-deps: server-deps-static-sites server-deps-all server-deps-gcc
server-deps-gcc: server-deps-gcc-armv7 server-deps-gcc-arm64
server-deps-gcc: server-deps-gcc-armv6-armv7 server-deps-gcc-arm64
server-deps-static-sites:
mkdir -p server/docs server/site
@@ -121,8 +127,8 @@ server-deps-static-sites:
server-deps-all:
which upx || { echo "ERROR: upx not installed. On Ubuntu, run: apt install upx"; exit 1; }
server-deps-gcc-armv7:
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
server-deps-gcc-armv6-armv7:
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/ARMv7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
server-deps-gcc-arm64:
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
@@ -178,14 +184,12 @@ staticcheck: .PHONY
# Releasing targets
release: release-deps
release: clean server-deps release-check-tags docs web check
goreleaser release --rm-dist --debug
release-snapshot: release-deps
release-snapshot: clean server-deps docs web check
goreleaser release --snapshot --skip-publish --rm-dist --debug
release-deps: clean server-deps release-check-tags docs web check
release-check-tags:
$(eval LATEST_TAG := $(shell git describe --abbrev=0 --tags | cut -c2-))
if ! grep -q $(LATEST_TAG) docs/install.md; then\
@@ -201,10 +205,13 @@ release-check-tags:
# Installing targets
install-amd64: remove-binary
sudo cp -a dist/ntfy_amd64_linux_amd64/ntfy /usr/bin/ntfy
sudo cp -a dist/ntfy_amd64_linux_amd64_v1/ntfy /usr/bin/ntfy
install-armv6: remove-binary
sudo cp -a dist/ntfy_armv6_linux_arm_6/ntfy /usr/bin/ntfy
install-armv7: remove-binary
sudo cp -a dist/ntfy_armv7_linux_armv7/ntfy /usr/bin/ntfy
sudo cp -a dist/ntfy_armv7_linux_arm_7/ntfy /usr/bin/ntfy
install-arm64: remove-binary
sudo cp -a dist/ntfy_arm64_linux_arm64/ntfy /usr/bin/ntfy
@@ -215,6 +222,9 @@ remove-binary:
install-amd64-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_amd64.deb
install-armv6-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv6.deb
install-armv7-deb: purge-package
sudo dpkg -i dist/ntfy_*_linux_armv7.deb

View File

@@ -56,6 +56,12 @@ func WithClick(url string) PublishOption {
return WithHeader("X-Click", url)
}
// WithActions adds custom user actions to the notification. The value can be either a JSON array or the
// simple format definition. See https://ntfy.sh/docs/publish/#action-buttons for details.
func WithActions(value string) PublishOption {
return WithHeader("X-Actions", value)
}
// WithAttach sets a URL that will be used by the client to download an attachment
func WithAttach(attach string) PublishOption {
return WithHeader("X-Attach", attach)

View File

@@ -26,6 +26,7 @@ var cmdPublish = &cli.Command{
&cli.StringFlag{Name: "tags", Aliases: []string{"tag", "T"}, EnvVars: []string{"NTFY_TAGS"}, Usage: "comma separated list of tags and emojis"},
&cli.StringFlag{Name: "delay", Aliases: []string{"at", "in", "D"}, EnvVars: []string{"NTFY_DELAY"}, Usage: "delay/schedule message"},
&cli.StringFlag{Name: "click", Aliases: []string{"U"}, EnvVars: []string{"NTFY_CLICK"}, Usage: "URL to open when notification is clicked"},
&cli.StringFlag{Name: "actions", Aliases: []string{"A"}, EnvVars: []string{"NTFY_ACTIONS"}, Usage: "actions JSON array or simple definition"},
&cli.StringFlag{Name: "attach", Aliases: []string{"a"}, EnvVars: []string{"NTFY_ATTACH"}, Usage: "URL to send as an external attachment"},
&cli.StringFlag{Name: "filename", Aliases: []string{"name", "n"}, EnvVars: []string{"NTFY_FILENAME"}, Usage: "filename for the attachment"},
&cli.StringFlag{Name: "file", Aliases: []string{"f"}, EnvVars: []string{"NTFY_FILE"}, Usage: "file to upload as an attachment"},
@@ -72,6 +73,7 @@ func execPublish(c *cli.Context) error {
tags := c.String("tags")
delay := c.String("delay")
click := c.String("click")
actions := c.String("actions")
attach := c.String("attach")
filename := c.String("filename")
file := c.String("file")
@@ -112,6 +114,9 @@ func execPublish(c *cli.Context) error {
if click != "" {
options = append(options, client.WithClick(click))
}
if actions != "" {
options = append(options, client.WithActions(strings.ReplaceAll(actions, "\n", " ")))
}
if attach != "" {
options = append(options, client.WithAttach(attach))
}

View File

@@ -528,6 +528,11 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# WebSockets support
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix
@@ -552,6 +557,11 @@ or the root domain:
# Higher than the max message size of 4096 bytes
LimitRequestBody 102400
# WebSockets support
RewriteCond %{HTTP:Upgrade} websocket [NC]
RewriteCond %{HTTP:Connection} upgrade [NC]
RewriteRule ^/?(.*) "ws://127.0.0.1:2586/$1" [P,L]
# Redirect HTTP to HTTPS, but only for GET topic addresses, since we want
# it to work with curl without the annoying https:// prefix

View File

@@ -26,28 +26,37 @@ deb/rpm packages.
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_x86_64.tar.gz
tar zxvf ntfy_1.20.0_linux_x86_64.tar.gz
sudo cp -a ntfy_1.20.0_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.20.0_linux_x86_64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_x86_64.tar.gz
tar zxvf ntfy_1.21.2_linux_x86_64.tar.gz
sudo cp -a ntfy_1.21.2_linux_x86_64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_x86_64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.tar.gz
tar zxvf ntfy_1.21.2_linux_armv6.tar.gz
sudo cp -a ntfy_1.21.2_linux_armv6/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv6/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_armv7.tar.gz
tar zxvf ntfy_1.20.0_linux_armv7.tar.gz
sudo cp -a ntfy_1.20.0_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.20.0_linux_armv7/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.tar.gz
tar zxvf ntfy_1.21.2_linux_armv7.tar.gz
sudo cp -a ntfy_1.21.2_linux_armv7/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_armv7/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_arm64.tar.gz
tar zxvf ntfy_1.20.0_linux_arm64.tar.gz
sudo cp -a ntfy_1.20.0_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.20.0_linux_arm64/{client,server}/*.yml /etc/ntfy
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.tar.gz
tar zxvf ntfy_1.21.2_linux_arm64.tar.gz
sudo cp -a ntfy_1.21.2_linux_arm64/ntfy /usr/bin/ntfy
sudo mkdir /etc/ntfy && sudo cp ntfy_1.21.2_linux_arm64/{client,server}/*.yml /etc/ntfy
sudo ntfy serve
```
@@ -94,7 +103,15 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_amd64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -102,7 +119,7 @@ Manually installing the .deb file:
=== "armv7/armhf"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_armv7.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -110,7 +127,7 @@ Manually installing the .deb file:
=== "arm64"
```bash
wget https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_arm64.deb
wget https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.deb
sudo dpkg -i ntfy_*.deb
sudo systemctl enable ntfy
sudo systemctl start ntfy
@@ -120,21 +137,28 @@ Manually installing the .deb file:
=== "x86_64/amd64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_amd64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_amd64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv6"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv6.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "armv7/armhf"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_armv7.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_armv7.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
=== "arm64"
```bash
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.20.0/ntfy_1.20.0_linux_arm64.rpm
sudo rpm -ivh https://github.com/binwiederhier/ntfy/releases/download/v1.21.2/ntfy_1.21.2_linux_arm64.rpm
sudo systemctl enable ntfy
sudo systemctl start ntfy
```
@@ -153,8 +177,8 @@ makepkg -si
```
## Docker
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv7 and arm64. It should be pretty
straight forward to use.
The [ntfy image](https://hub.docker.com/r/binwiederhier/ntfy) is available for amd64, armv6, armv7 and arm64. It should
be pretty straight forward to use.
The server exposes its web UI and the API on port 80, so you need to expose that in Docker. To use the persistent
[message cache](config.md#message-cache), you also need to map a volume to `/var/cache/ntfy`. To change other settings,

File diff suppressed because it is too large Load Diff

View File

@@ -4,13 +4,93 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
<!--
## ntfy Android app v1.11.0 (UNRELEASED)
## ntfy Android app v1.12.0 (UNRELEASED)
**Features:**
* Custom notification [action buttons](https://ntfy.sh/docs/publish/#action-buttons) ([#134](https://github.com/binwiederhier/ntfy/issues/134),
thanks to [@mrherman](https://github.com/mrherman) for reporting)
* Support for [ntfy:// deep links](https://ntfy.sh/docs/subscribe/phone/#ntfy-links) ([#20](https://github.com/binwiederhier/ntfy/issues/20), thanks
to [@Copephobia](https://github.com/Copephobia) for reporting)
* [Fastlane metadata](https://hosted.weblate.org/projects/ntfy/android-fastlane/) can now be translated too ([#198](https://github.com/binwiederhier/ntfy/issues/198),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Channel settings option to configure DND override, sounds, etc. ([#91](https://github.com/binwiederhier/ntfy/issues/91))
**Bugs:**
* Validate URLs when changing default server and server in user management ([#193](https://github.com/binwiederhier/ntfy/issues/193),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* Error in sending test notification in different languages ([#209](https://github.com/binwiederhier/ntfy/issues/209),
thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov) for reporting)
* "[x] Instant delivery in doze mode" checkbox does not work properly ([#211](https://github.com/binwiederhier/ntfy/issues/211))
* Disallow "http" GET/HEAD actions with body ([#221](https://github.com/binwiederhier/ntfy/issues/221), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
* Action "view" with "clear=true" does not work on some phones ([#220](https://github.com/binwiederhier/ntfy/issues/220), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
* Do not group foreground service notification with others ([#219](https://github.com/binwiederhier/ntfy/issues/219), thanks to
[@s-h-a-r-d](https://github.com/s-h-a-r-d) for reporting)
**Additional translations:**
* Japanese (thanks to [@shak](https://hosted.weblate.org/user/shak/))
* Russian (thanks to [@flamey](https://hosted.weblate.org/user/flamey/) and [@ilya.mikheev.coder](https://hosted.weblate.org/user/ilya.mikheev.coder/))
**Thanks for testing:**
Thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d) (aka @Shard), [@cmeis](https://github.com/cmeis),
@poblabs, and everyone I forgot for testing.
-->
## ntfy server v1.21.2
Released Apr 24, 2022
**Features:**
* Custom notification [action buttons](https://ntfy.sh/docs/publish/#action-buttons) ([#134](https://github.com/binwiederhier/ntfy/issues/134),
thanks to [@mrherman](https://github.com/mrherman) for reporting)
* Added ARMv6 build ([#200](https://github.com/binwiederhier/ntfy/issues/200), thanks to [@jcrubioa](https://github.com/jcrubioa) for reporting)
* Web app internationalization support 🇧🇬 🇩🇪 🇺🇸 🌎 ([#189](https://github.com/binwiederhier/ntfy/issues/189))
**Bugs:**
* Web app: English language strings fixes, additional descriptions for settings ([#203](https://github.com/binwiederhier/ntfy/issues/203), thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Web app: Show error message snackbar when sending test notification fails ([#205](https://github.com/binwiederhier/ntfy/issues/205), thanks to [@cmeis](https://github.com/cmeis))
* Web app: basic URL validation in user management ([#204](https://github.com/binwiederhier/ntfy/issues/204), thanks to [@cmeis](https://github.com/cmeis))
* Disallow "http" GET/HEAD actions with body ([#221](https://github.com/binwiederhier/ntfy/issues/221), thanks to
[@cmeis](https://github.com/cmeis) for reporting)
**Translations (web app):**
* Bulgarian (thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* German (thanks to [@cmeis](https://github.com/cmeis))
* Indonesian (thanks to [@linerly](https://hosted.weblate.org/user/linerly/))
* Japanese (thanks to [@shak](https://hosted.weblate.org/user/shak/))
* Norwegian Bokmål (thanks to [@comradekingu](https://github.com/comradekingu))
* Russian (thanks to [@flamey](https://hosted.weblate.org/user/flamey/) and [@ilya.mikheev.coder](https://hosted.weblate.org/user/ilya.mikheev.coder/))
* Spanish (thanks to [@rogeliodh](https://github.com/rogeliodh))
* Turkish (thanks to [@ersen](https://ersen.moe/))
**Integrations:**
[Apprise](https://github.com/caronc/apprise) support was fully released in [v0.9.8.2](https://github.com/caronc/apprise/releases/tag/v0.9.8.2)
of Apprise. Thanks to [@particledecay](https://github.com/particledecay) and [@caronc](https://github.com/caronc) for their fantastic work.
You can try it yourself like this (detailed usage in the [Apprise wiki](https://github.com/caronc/apprise/wiki/Notify_ntfy)):
```
pip3 install apprise
apprise -b "Hi there" ntfys://mytopic
```
## ntfy Android app v1.11.0
Released Apr 7, 2022
**Features:**
* Download attachments to cache folder ([#181](https://github.com/binwiederhier/ntfy/issues/181))
* Regularly delete attachments for deleted notifications ([#142](https://github.com/binwiederhier/ntfy/issues/142))
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
* Translations to different languages ([#188](https://github.com/binwiederhier/ntfy/issues/188), thanks to
[@StoyanDimitrov](https://github.com/StoyanDimitrov) for initiating things)
**Bugs:**
@@ -23,14 +103,14 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
**Translations:**
* English language improvements (thanks to [@comradekingu](https://github.com/comradekingu))
* Bulgarian (thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Bulgarian (thanks to [@StoyanDimitrov](https://github.com/StoyanDimitrov))
* Chinese/Simplified (thanks to [@poi](https://hosted.weblate.org/user/poi) and [@PeterCxy](https://hosted.weblate.org/user/PeterCxy))
* Dutch (*incomplete*, thanks to [@diony](https://hosted.weblate.org/user/diony))
* French (thanks to [@Kusoneko](https://kusoneko.moe/) and [@mlcsthor](https://hosted.weblate.org/user/mlcsthor/))
* German (thanks to [@cmeis](https://github.com/cmeis))
* Italian (thanks to [@theTranslator](https://hosted.weblate.org/user/theTranslator/))
* Indonesian (thanks to [@linerly](https://hosted.weblate.org/user/linerly/))
* Norwegian (*incomplete*, thanks to [@comradekingu](https://github.com/comradekingu))
* Norwegian Bokmål (*incomplete*, thanks to [@comradekingu](https://github.com/comradekingu))
* Portuguese/Brazil (thanks to [@LW](https://hosted.weblate.org/user/LW/))
* Spanish (thanks to [@rogeliodh](https://github.com/rogeliodh))
* Turkish (thanks to [@ersen](https://ersen.moe/))
@@ -40,8 +120,6 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
* Many thanks to [@cmeis](https://github.com/cmeis), [@Fallenbagel](https://github.com/Fallenbagel), [@Joeharrison94](https://github.com/Joeharrison94),
and [@rogeliodh](https://github.com/rogeliodh) for input on the new attachment logic, and for testing the release
-->
## ntfy server v1.20.0
Released Apr 6, 2022
@@ -51,11 +129,11 @@ Released Apr 6, 2022
**Bugs:**
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@RasHas](https://github.com/RasHas))
* Added `EXPOSE 80/tcp` to Dockerfile to support auto-discovery in [Traefik](https://traefik.io/) ([#195](https://github.com/binwiederhier/ntfy/issues/195), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
**Documentation:**
* Added docker-compose example to [install instructions](install.md#docker) ([#194](https://github.com/binwiederhier/ntfy/pull/194), thanks to [@RasHas](https://github.com/RasHas))
* Added docker-compose example to [install instructions](install.md#docker) ([#194](https://github.com/binwiederhier/ntfy/pull/194), thanks to [@s-h-a-r-d](https://github.com/s-h-a-r-d))
**Integrations:**

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

View File

@@ -31,7 +31,7 @@ If those screenshots are still not enough, here's a video:
</figure>
## Message priority
When you [publish messages](../publish.md#message-priority) to a topic, you can define a priority. This priority defines
When you [publish messages](../publish.md#message-priority) to a topic, you can **define a priority**. This priority defines
how urgently Android will notify you about the notification, and whether they make a sound and/or vibrate.
By default, messages with default priority or higher (>= 3) will vibrate and make a sound. Messages with high or urgent
@@ -42,11 +42,19 @@ priority (>= 4) will also show as pop-over, like so:
<figcaption>High and urgent notifications show as pop-over</figcaption>
</figure>
You can change these settings in Android by long-pressing on the app, and tapping "Notifications". You can then configure
the settings (and custom sounds or vibration) for each of the priorities:
You can change these settings in Android by long-pressing on the app, and tapping "Notifications", or from the "Settings"
menu under "Channel settings". There is one notification channel for each priority:
<figure markdown>
![notification settings](../static/img/android-notification-settings.png){ width=500 }
![notification settings](../static/img/android-screenshot-notification-settings.png){ width=500 }
<figcaption>Per-priority channels</figcaption>
</figure>
Per notification channel, you can configure a **channel-specific sound**, whether to **override the Do Not Disturb (DND)**
setting, and other settings such as popover or notification dot:
<figure markdown>
![channel details](../static/img/android-screenshot-notification-details.jpg){ width=500 }
<figcaption>Per-priority sound/vibration settings</figcaption>
</figure>
@@ -80,6 +88,34 @@ notifications. Firebase is overall pretty bad at delivering messages in time, bu
The ntfy Android app uses Firebase only for the main host `ntfy.sh`, and only in the Google Play flavor of the app.
It won't use Firebase for any self-hosted servers, and not at all in the the F-Droid flavor.
## Share to topic
You can share files to a topic using Android's "Share" feature. This works in almost any app that supports sharing files
or text, and it's useful for sending yourself links, files or other things. The feature remembers a few of the last topics
you shared content to and lists them at the bottom.
The feature is pretty self-explanatory, and one picture says more than a thousand words. So here are two pictures:
<div id="share-to-topic-screenshots" class="screenshots">
<a href="../../static/img/android-screenshot-share-1.jpg"><img src="../../static/img/android-screenshot-share-1.jpg"/></a>
<a href="../../static/img/android-screenshot-share-2.jpg"><img src="../../static/img/android-screenshot-share-2.jpg"/></a>
</div>
## ntfy:// links
The ntfy Android app supports deep linking directly to topics. This is useful when integrating with [automation apps](#automation-apps)
such as [MacroDroid](https://play.google.com/store/apps/details?id=com.arlosoft.macrodroid) or [Tasker](https://play.google.com/store/apps/details?id=net.dinglisch.android.taskerm),
or to simply directly link to a topic from a mobile website.
!!! info
Android deep linking of http/https links is very brittle and limited, which is why something like `https://<host>/<topic>/subscribe` is
**not possible**, and instead `ntfy://` links have to be used. More details in [issue #20](https://github.com/binwiederhier/ntfy/issues/20).
**Supported link formats:**
| Link format | Example | Description |
|-------------------------------------------------------------------------------|-------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <span style="white-space: nowrap">`ntfy://<host>/<topic>`</span> | `ntfy://ntfy.sh/mytopic` | Directly opens the Android app detail view for the given topic and server. Subscribes to the topic if not already subscribed. This is equivalent to the web view `https://ntfy.sh/mytopic` (HTTPS!) |
| <span style="white-space: nowrap">`ntfy://<host>/<topic>?secure=false`</span> | `ntfy://example.com/mytopic?secure=false` | Same as above, except that this will use HTTP instead of HTTPS as topic URL. This is equivalent to the web view `http://example.com/mytopic` (HTTP!) |
## Integrations
### UnifiedPush

34
go.mod
View File

@@ -4,7 +4,7 @@ go 1.17
require (
cloud.google.com/go/firestore v1.6.1 // indirect
cloud.google.com/go/storage v1.21.0 // indirect
cloud.google.com/go/storage v1.22.0 // indirect
firebase.google.com/go v3.13.0+incompatible
github.com/BurntSushi/toml v1.1.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
@@ -14,19 +14,21 @@ require (
github.com/mattn/go-sqlite3 v1.14.12
github.com/olebedev/when v0.0.0-20211212231525-59bd4edcf9d6
github.com/stretchr/testify v1.7.0
github.com/urfave/cli/v2 v2.4.0
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a // indirect
github.com/urfave/cli/v2 v2.4.7
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
google.golang.org/api v0.74.0
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171
golang.org/x/time v0.0.0-20220411224347-583f2d630306
google.golang.org/api v0.75.0
gopkg.in/yaml.v2 v2.4.0
)
require github.com/pkg/errors v0.9.1 // indirect
require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
cloud.google.com/go v0.101.0 // indirect
cloud.google.com/go/compute v1.6.1 // indirect
cloud.google.com/go/iam v0.3.0 // indirect
github.com/AlekSi/pointer v1.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -34,18 +36,18 @@ require (
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-cmp v0.5.7 // indirect
github.com/googleapis/gax-go/v2 v2.2.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.23.0 // indirect
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b // indirect
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 // indirect
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf // indirect
google.golang.org/grpc v1.45.0 // indirect
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 // indirect
google.golang.org/grpc v1.46.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)

92
go.sum
View File

@@ -26,9 +26,9 @@ cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+Y
cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4=
cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc=
cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA=
cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U=
cloud.google.com/go v0.100.2 h1:t9Iw5QH5v4XtlEQaCtUY7x6sCABps8sW0acw7e2WQ6Y=
cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A=
cloud.google.com/go v0.101.0 h1:g+LL+JvpvdyGtcaD2xw2mSByE/6F9s471eJSoaysM84=
cloud.google.com/go v0.101.0/go.mod h1:hEiddgDb77jDQ+I80tURYNJEnuwPzFU8awCFFRLKjW0=
cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
@@ -36,15 +36,15 @@ cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUM
cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow=
cloud.google.com/go/compute v1.2.0/go.mod h1:xlogom/6gr8RJGBe7nT2eGsQYAFUbbv8dbC29qE3Xmw=
cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM=
cloud.google.com/go/compute v1.5.0 h1:b1zWmYuuHz7gO9kDcM/EpHGr06UgsYNRpNJzI2kFiLM=
cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M=
cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s=
cloud.google.com/go/compute v1.6.1 h1:2sMmt8prCn7DPaG4Pmh0N3Inmc8cT8ae5k1M6VJ9Wqc=
cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU=
cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
cloud.google.com/go/firestore v1.6.1 h1:8rBq3zRjnHx8UtBvaOWqBB1xq9jH6/wltfQLlTMh2Fw=
cloud.google.com/go/firestore v1.6.1/go.mod h1:asNXNOzBdyVQmEU+ggO8UPodTkEVFW5Qx+rwHnAz+EY=
cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw=
cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc=
cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY=
cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
@@ -56,8 +56,8 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo
cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
cloud.google.com/go/storage v1.21.0 h1:HwnT2u2D309SFDHQII6m18HlrCi3jAXhUMTLOWXYH14=
cloud.google.com/go/storage v1.21.0/go.mod h1:XmRlxkgPjlBONznT2dDUU/5XlpU2OjMnKuqnZI01LAA=
cloud.google.com/go/storage v1.22.0 h1:NUV0NNp9nkBuW66BFRLuMgldN60C57ET3dhbwLIYio8=
cloud.google.com/go/storage v1.22.0/go.mod h1:GbaLEoMqbVm6sx3Z0R++gSiBlgMv6yUi2q1DeGFKQgE=
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
firebase.google.com/go v3.13.0+incompatible h1:3TdYC3DDi6aHn20qoRkxwGqNgdjtblwVAyRLQwGn/+4=
firebase.google.com/go v3.13.0+incompatible/go.mod h1:xlah6XbEyW6tbfSklcfe5FHJIwjt8toICdV5Wh9ptHs=
@@ -65,8 +65,6 @@ github.com/AlekSi/pointer v1.0.0/go.mod h1:1kjywbfcPFCmncIxtk6fIEub6LKrfMz3gc5QK
github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w=
github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU=
github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
@@ -86,6 +84,7 @@ github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XP
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU=
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
@@ -105,6 +104,7 @@ github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.m
github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ=
github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0=
github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/gabriel-vasile/mimetype v1.4.0 h1:Cn9dkdYsMIu56tGho+fqzh7XmvY2YyGU0FnbhiOsEro=
github.com/gabriel-vasile/mimetype v1.4.0/go.mod h1:fA8fi6KUiG7MgQQ+mEWotXoEOvmxRtOJlERCzSmRvr8=
@@ -166,8 +166,9 @@ github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPg
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.2.1 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ=
github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw=
github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk=
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
@@ -188,8 +189,11 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0=
github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM=
github.com/googleapis/gax-go/v2 v2.2.0 h1:s7jOdKSaksJVOxE0Y/S32otcfiP+UQ0cL8/GTKaONwE=
github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM=
github.com/googleapis/gax-go/v2 v2.3.0 h1:nRJtk3y8Fm770D42QV6T90ZnvFZyk7agSo3Q+Z9p3WI=
github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM=
github.com/googleapis/go-type-adapters v1.0.0 h1:9XdMn+d/G57qq1s8dNc5IesGCXHf6V2HZ2JwRxfA2tA=
github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
@@ -227,8 +231,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/urfave/cli/v2 v2.4.0 h1:m2pxjjDFgDxSPtO8WSdbndj17Wu2y8vOT86wE/tjr+I=
github.com/urfave/cli/v2 v2.4.0/go.mod h1:NX9W0zmTvedE5oDoOMs2RTC8RvdK98NTYZE5LbaEYPg=
github.com/urfave/cli/v2 v2.4.7 h1:nUgKLTC/InVYwUx26HZUBGIBZaptiW97W8vVlhuYawo=
github.com/urfave/cli/v2 v2.4.7/go.mod h1:oDzoM7pVwz6wHn5ogWgFUU1s4VJayeQS+aEZDqXIEJs=
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
@@ -248,10 +252,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064 h1:S25/rfnfsMVgORT4/J61MJ7rdyseOZOyvLIrZEZ7s6s=
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -325,11 +327,11 @@ golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b h1:vI32FkLJNAWtGD4BwkThwEy6XS7ZLLMHkSkYfF8M0W0=
golang.org/x/net v0.0.0-20220403103023-749bd193bc2b/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861 h1:yssD99+7tqHWO5Gwh81phT+67hg+KttniBr6UnEXOY8=
golang.org/x/net v0.0.0-20220421235706-1d1ef9303861/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -348,8 +350,9 @@ golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -412,19 +415,17 @@ golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12 h1:QyVthZKMsyaQwBTJE04jdNN0Pp5Fn9Qga0mrgxyERQM=
golang.org/x/sys v0.0.0-20220406163625-3f8b81556e12/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171 h1:EH1Deb8WZJ0xc0WK//leUHXcX9aLE5SymusoTmMZye8=
golang.org/x/term v0.0.0-20220411215600-e5f449aeb171/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -438,8 +439,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65 h1:M73Iuj3xbbb9Uk1DYhzydthsj6oOd6l9bpuFcNoUvTs=
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w=
golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
@@ -494,8 +495,9 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f h1:GGU+dLjvlC3qDwqYgL6UgRmHXhOOgns0bZu2Ty5mm6U=
golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
@@ -528,16 +530,12 @@ google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdr
google.golang.org/api v0.59.0/go.mod h1:sT2boj7M9YJxZzgeZqXogmhfmRWDtPzT31xkieUbuZU=
google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I=
google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo=
google.golang.org/api v0.64.0/go.mod h1:931CdxA8Rm4t6zqTFGSsgwbAEZ2+GMYurbndwSimebM=
google.golang.org/api v0.66.0/go.mod h1:I1dmXYpX7HGwz/ejRxwQp2qj5bFAz93HiCU1C1oYd9M=
google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g=
google.golang.org/api v0.69.0/go.mod h1:boanBiw+h5c3s+tBPgEzLDRHfFLWV0qXxRHz3ws7C80=
google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA=
google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8=
google.golang.org/api v0.73.0 h1:O9bThUh35K1rvUrQwTUQ1eqLC/IYyzUpWavYIO2EXvo=
google.golang.org/api v0.73.0/go.mod h1:lbd/q6BRFJbdpV6OUCXstVeiI5mL/d3/WifG7iNKnjI=
google.golang.org/api v0.74.0 h1:ExR2D+5TYIrMphWgs5JCgwRhEDlPDXXrLwHHMgPHTXE=
google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs=
google.golang.org/api v0.75.0 h1:0AYh/ae6l9TDUvIQrDw5QRpM100P6oHgD+o3dYHMzJg=
google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
@@ -585,6 +583,7 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D
google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A=
google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
@@ -608,23 +607,20 @@ google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ6
google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20211223182754-3ac035c7e7cb/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220111164026-67b88f271998/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220114231437-d2e6a121cae0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220201184016-50beb8ab5c44/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc=
google.golang.org/genproto v0.0.0-20220211171837-173942840c17/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220216160803-4663080d8bc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI=
google.golang.org/genproto v0.0.0-20220322021311-435b647f9ef2 h1:3n0D2NdPGm0g0wrVJzXJWW5CBOoqgGBkDX9cRMJHZAY=
google.golang.org/genproto v0.0.0-20220322021311-435b647f9ef2/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E=
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf h1:JTjwKJX9erVpsw17w+OIPP7iAgEkN/r8urhWSunEDTs=
google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731 h1:nquqdM9+ps0JZcIiI70+tqoaIFS5Ql4ZuK8UXnz3HfE=
google.golang.org/genproto v0.0.0-20220422154200-b37d22cd5731/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
@@ -652,8 +648,9 @@ google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnD
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU=
google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M=
google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ=
google.golang.org/grpc v1.46.0 h1:oCjezcn6g6A75TGoKYBPgKmVBLexhYLM6MebdrPApP8=
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
@@ -676,7 +673,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=

View File

@@ -2,6 +2,7 @@ package server
import (
"encoding/json"
"fmt"
"net/http"
)
@@ -22,6 +23,15 @@ func (e errHTTP) JSON() string {
return string(b)
}
func wrapErrHTTP(err *errHTTP, message string, args ...interface{}) *errHTTP {
return &errHTTP{
Code: err.Code,
HTTPCode: err.HTTPCode,
Message: fmt.Sprintf("%s, %s", err.Message, fmt.Sprintf(message, args...)),
Link: err.Link,
}
}
var (
errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications"}
errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", ""}
@@ -39,6 +49,7 @@ var (
errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"}
errHTTPBadRequestWebSocketsUpgradeHeaderMissing = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets"}
errHTTPBadRequestJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"}
errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"}

View File

@@ -2,6 +2,7 @@ package server
import (
"database/sql"
"encoding/json"
"errors"
"fmt"
_ "github.com/mattn/go-sqlite3" // SQLite driver
@@ -29,6 +30,7 @@ const (
priority INT NOT NULL,
tags TEXT NOT NULL,
click TEXT NOT NULL,
actions TEXT NOT NULL,
attachment_name TEXT NOT NULL,
attachment_type TEXT NOT NULL,
attachment_size INT NOT NULL,
@@ -43,37 +45,37 @@ const (
COMMIT;
`
insertMessageQuery = `
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT INTO messages (mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding, published)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`
pruneMessagesQuery = `DELETE FROM messages WHERE time < ? AND published = 1`
selectRowIDFromMessageID = `SELECT id FROM messages WHERE topic = ? AND mid = ?`
selectMessagesSinceTimeQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND time >= ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceTimeIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND time >= ?
ORDER BY time, id
`
selectMessagesSinceIDQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND id > ? AND published = 1
ORDER BY time, id
`
selectMessagesSinceIDIncludeScheduledQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE topic = ? AND (id > ? OR published = 0)
ORDER BY time, id
`
selectMessagesDueQuery = `
SELECT mid, time, topic, message, title, priority, tags, click, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
SELECT mid, time, topic, message, title, priority, tags, click, actions, attachment_name, attachment_type, attachment_size, attachment_expires, attachment_url, attachment_owner, encoding
FROM messages
WHERE time <= ? AND published = 0
ORDER BY time, id
@@ -88,7 +90,7 @@ const (
// Schema management queries
const (
currentSchemaVersion = 5
currentSchemaVersion = 6
createSchemaVersionTableQuery = `
CREATE TABLE IF NOT EXISTS schemaVersion (
id INT PRIMARY KEY,
@@ -166,6 +168,11 @@ const (
ALTER TABLE messages_new RENAME TO messages;
COMMIT;
`
// 5 -> 6
migrate5To6AlterMessagesTableQuery = `
ALTER TABLE messages ADD COLUMN actions TEXT NOT NULL DEFAULT('');
`
)
type messageCache struct {
@@ -228,6 +235,14 @@ func (c *messageCache) AddMessage(m *message) error {
attachmentURL = m.Attachment.URL
attachmentOwner = m.Attachment.Owner
}
var actionsStr string
if len(m.Actions) > 0 {
actionsBytes, err := json.Marshal(m.Actions)
if err != nil {
return err
}
actionsStr = string(actionsBytes)
}
_, err := c.db.Exec(
insertMessageQuery,
m.ID,
@@ -238,6 +253,7 @@ func (c *messageCache) AddMessage(m *message) error {
m.Priority,
tags,
m.Click,
actionsStr,
attachmentName,
attachmentType,
attachmentSize,
@@ -399,7 +415,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
for rows.Next() {
var timestamp, attachmentSize, attachmentExpires int64
var priority int
var id, topic, msg, title, tagsStr, click, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string
var id, topic, msg, title, tagsStr, click, actionsStr, attachmentName, attachmentType, attachmentURL, attachmentOwner, encoding string
err := rows.Scan(
&id,
&timestamp,
@@ -409,6 +425,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
&priority,
&tagsStr,
&click,
&actionsStr,
&attachmentName,
&attachmentType,
&attachmentSize,
@@ -424,6 +441,12 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
if tagsStr != "" {
tags = strings.Split(tagsStr, ",")
}
var actions []*action
if actionsStr != "" {
if err := json.Unmarshal([]byte(actionsStr), &actions); err != nil {
return nil, err
}
}
var att *attachment
if attachmentName != "" && attachmentURL != "" {
att = &attachment{
@@ -445,6 +468,7 @@ func readMessages(rows *sql.Rows) ([]*message, error) {
Priority: priority,
Tags: tags,
Click: click,
Actions: actions,
Attachment: att,
Encoding: encoding,
})
@@ -490,6 +514,8 @@ func setupCacheDB(db *sql.DB) error {
return migrateFrom3(db)
} else if schemaVersion == 4 {
return migrateFrom4(db)
} else if schemaVersion == 5 {
return migrateFrom5(db)
}
return fmt.Errorf("unexpected schema version found: %d", schemaVersion)
}
@@ -562,5 +588,16 @@ func migrateFrom4(db *sql.DB) error {
if _, err := db.Exec(updateSchemaVersion, 5); err != nil {
return err
}
return migrateFrom5(db)
}
func migrateFrom5(db *sql.DB) error {
log.Print("Migrating cache database schema: from 5 to 6")
if _, err := db.Exec(migrate5To6AlterMessagesTableQuery); err != nil {
return err
}
if _, err := db.Exec(updateSchemaVersion, 6); err != nil {
return err
}
return nil // Update this when a new version is added
}

View File

@@ -535,6 +535,13 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca
}
m.Time = delay.Unix()
}
actionsStr := readParam(r, "x-actions", "actions", "action")
if actionsStr != "" {
m.Actions, err = parseActions(actionsStr)
if err != nil {
return false, false, "", false, err // wrapped errHTTPBadRequestActionsInvalid
}
}
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush {
firebase = false
@@ -1150,6 +1157,13 @@ func (s *Server) transformBodyJSON(next handleFunc) handleFunc {
if m.Click != "" {
r.Header.Set("X-Click", m.Click)
}
if len(m.Actions) > 0 {
actionsStr, err := json.Marshal(m.Actions)
if err != nil {
return errHTTPBadRequestJSONInvalid
}
r.Header.Set("X-Actions", string(actionsStr))
}
if m.Email != "" {
r.Header.Set("X-Email", m.Email)
}

View File

@@ -81,6 +81,13 @@ func toFirebaseMessage(m *message, auther auth.Auther) (*messaging.Message, erro
"message": m.Message,
"encoding": m.Encoding,
}
if len(m.Actions) > 0 {
actions, err := json.Marshal(m.Actions)
if err != nil {
return nil, err
}
data["actions"] = string(actions)
}
if m.Attachment != nil {
data["attachment_name"] = m.Attachment.Name
data["attachment_type"] = m.Attachment.Type

View File

@@ -876,6 +876,26 @@ func TestServer_PublishUnifiedPushText(t *testing.T) {
require.Equal(t, "this is a unifiedpush text message", m.Message)
}
func TestServer_PublishActions_AndPoll(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
response := request(t, s, "PUT", "/mytopic", "my message", map[string]string{
"Actions": "view, Open portal, https://home.nest.com/; http, Turn down, https://api.nest.com/device/XZ1D2, body=target_temp_f=65",
})
require.Equal(t, 200, response.Code)
response = request(t, s, "GET", "/mytopic/json?poll=1", "", nil)
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, 2, len(m.Actions))
require.Equal(t, "view", m.Actions[0].Action)
require.Equal(t, "Open portal", m.Actions[0].Label)
require.Equal(t, "https://home.nest.com/", m.Actions[0].URL)
require.Equal(t, "http", m.Actions[1].Action)
require.Equal(t, "Turn down", m.Actions[1].Label)
require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL)
require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
}
func TestServer_PublishAsJSON(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
body := `{"topic":"mytopic","message":"A message","title":"a title\nwith lines","tags":["tag1","tag 2"],` +
@@ -911,6 +931,41 @@ func TestServer_PublishAsJSON_WithEmail(t *testing.T) {
require.Equal(t, 1, mailer.Count())
}
func TestServer_PublishAsJSON_WithActions(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
body := `{
"topic":"mytopic",
"message":"A message",
"actions": [
{
"action": "view",
"label": "Open portal",
"url": "https://home.nest.com/"
},
{
"action": "http",
"label": "Turn down",
"url": "https://api.nest.com/device/XZ1D2",
"body": "target_temp_f=65"
}
]
}`
response := request(t, s, "POST", "/", body, nil)
require.Equal(t, 200, response.Code)
m := toMessage(t, response.Body.String())
require.Equal(t, "mytopic", m.Topic)
require.Equal(t, "A message", m.Message)
require.Equal(t, 2, len(m.Actions))
require.Equal(t, "view", m.Actions[0].Action)
require.Equal(t, "Open portal", m.Actions[0].Label)
require.Equal(t, "https://home.nest.com/", m.Actions[0].URL)
require.Equal(t, "http", m.Actions[1].Action)
require.Equal(t, "Turn down", m.Actions[1].Label)
require.Equal(t, "https://api.nest.com/device/XZ1D2", m.Actions[1].URL)
require.Equal(t, "target_temp_f=65", m.Actions[1].Body)
}
func TestServer_PublishAsJSON_Invalid(t *testing.T) {
s := newTestServer(t, newTestConfig(t))
body := `{"topic":"mytopic",INVALID`

View File

@@ -27,6 +27,7 @@ type message struct {
Priority int `json:"priority,omitempty"`
Tags []string `json:"tags,omitempty"`
Click string `json:"click,omitempty"`
Actions []*action `json:"actions,omitempty"`
Attachment *attachment `json:"attachment,omitempty"`
Title string `json:"title,omitempty"`
Message string `json:"message,omitempty"`
@@ -42,6 +43,19 @@ type attachment struct {
Owner string `json:"-"` // IP address of uploader, used for rate limiting
}
type action struct {
ID string `json:"id"`
Action string `json:"action"` // "view", "broadcast", or "http"
Label string `json:"label"` // action button label
Clear bool `json:"clear"` // clear notification after successful execution
URL string `json:"url,omitempty"` // used in "view" and "http" actions
Method string `json:"method,omitempty"` // used in "http" action, default is POST (!)
Headers map[string]string `json:"headers,omitempty"` // used in "http" action
Body string `json:"body,omitempty"` // used in "http" action
Intent string `json:"intent,omitempty"` // used in "broadcast" action
Extras map[string]string `json:"extras,omitempty"` // used in "broadcast" action
}
// publishMessage is used as input when publishing as JSON
type publishMessage struct {
Topic string `json:"topic"`
@@ -50,6 +64,7 @@ type publishMessage struct {
Priority int `json:"priority"`
Tags []string `json:"tags"`
Click string `json:"click"`
Actions []action `json:"actions"`
Attach string `json:"attach"`
Filename string `json:"filename"`
Email string `json:"email"`

View File

@@ -1,10 +1,17 @@
package server
import (
"encoding/json"
"heckel.io/ntfy/util"
"net/http"
"strings"
)
const (
actionIDLength = 10
actionsMax = 3
)
func readBoolParam(r *http.Request, defaultValue bool, names ...string) bool {
value := strings.ToLower(readParam(r, names...))
if value == "" {
@@ -40,3 +47,103 @@ func readQueryParam(r *http.Request, names ...string) string {
}
return ""
}
func parseActions(s string) (actions []*action, err error) {
// Parse JSON or simple format
s = strings.TrimSpace(s)
if strings.HasPrefix(s, "[") {
actions, err = parseActionsFromJSON(s)
} else {
actions, err = parseActionsFromSimple(s)
}
if err != nil {
return nil, err
}
// Add ID field, ensure correct uppercase/lowercase
for i := range actions {
actions[i].ID = util.RandomString(actionIDLength)
actions[i].Action = strings.ToLower(actions[i].Action)
actions[i].Method = strings.ToUpper(actions[i].Method)
}
// Validate
if len(actions) > actionsMax {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "only %d actions allowed", actionsMax)
}
for _, action := range actions {
if !util.InStringList([]string{"view", "broadcast", "http"}, action.Action) {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action '%s' unknown", action.Action)
} else if action.Label == "" {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'label' is required")
} else if util.InStringList([]string{"view", "http"}, action.Action) && action.URL == "" {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'url' is required for action '%s'", action.Action)
} else if action.Action == "http" && util.InStringList([]string{"GET", "HEAD"}, action.Method) && action.Body != "" {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'body' cannot be set if method is %s", action.Method)
}
}
return actions, nil
}
func parseActionsFromJSON(s string) ([]*action, error) {
actions := make([]*action, 0)
if err := json.Unmarshal([]byte(s), &actions); err != nil {
return nil, err
}
return actions, nil
}
func parseActionsFromSimple(s string) ([]*action, error) {
actions := make([]*action, 0)
rawActions := util.SplitNoEmpty(s, ";")
for _, rawAction := range rawActions {
newAction := &action{
Headers: make(map[string]string),
Extras: make(map[string]string),
}
parts := util.SplitNoEmpty(rawAction, ",")
if len(parts) < 3 {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action requires at least keys 'action', 'label' and one parameter: %s", rawAction)
}
for i, part := range parts {
key, value := util.SplitKV(part, "=")
if key == "" && i == 0 {
newAction.Action = value
} else if key == "" && i == 1 {
newAction.Label = value
} else if key == "" && util.InStringList([]string{"view", "http"}, newAction.Action) && i == 2 {
newAction.URL = value
} else if strings.HasPrefix(key, "headers.") {
newAction.Headers[strings.TrimPrefix(key, "headers.")] = value
} else if strings.HasPrefix(key, "extras.") {
newAction.Extras[strings.TrimPrefix(key, "extras.")] = value
} else if key != "" {
switch strings.ToLower(key) {
case "action":
newAction.Action = value
case "label":
newAction.Label = value
case "clear":
lvalue := strings.ToLower(value)
if !util.InStringList([]string{"true", "yes", "1", "false", "no", "0"}, lvalue) {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "'clear=%s' not allowed", value)
}
newAction.Clear = lvalue == "true" || lvalue == "yes" || lvalue == "1"
case "url":
newAction.URL = value
case "method":
newAction.Method = value
case "body":
newAction.Body = value
default:
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "key '%s' unknown", key)
}
} else {
return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "unknown term '%s'", part)
}
}
actions = append(actions, newAction)
}
return actions, nil
}

View File

@@ -27,3 +27,56 @@ func TestReadBoolParam(t *testing.T) {
require.Equal(t, false, up)
require.Equal(t, true, firebase)
}
func TestParseActions(t *testing.T) {
actions, err := parseActions("[]")
require.Nil(t, err)
require.Empty(t, actions)
actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open; view, Show portal, https://door.lan")
require.Nil(t, err)
require.Equal(t, 2, len(actions))
require.Equal(t, "http", actions[0].Action)
require.Equal(t, "Open door", actions[0].Label)
require.Equal(t, "https://door.lan/open", actions[0].URL)
require.Equal(t, "view", actions[1].Action)
require.Equal(t, "Show portal", actions[1].Label)
require.Equal(t, "https://door.lan", actions[1].URL)
actions, err = parseActions(`[{"action":"http","label":"Open door","url":"https://door.lan/open"}, {"action":"view","label":"Show portal","url":"https://door.lan"}]`)
require.Nil(t, err)
require.Equal(t, 2, len(actions))
require.Equal(t, "http", actions[0].Action)
require.Equal(t, "Open door", actions[0].Label)
require.Equal(t, "https://door.lan/open", actions[0].URL)
require.Equal(t, "view", actions[1].Action)
require.Equal(t, "Show portal", actions[1].Label)
require.Equal(t, "https://door.lan", actions[1].URL)
actions, err = parseActions("action=http, label=Open door, url=https://door.lan/open, body=this is a body, method=PUT")
require.Nil(t, err)
require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action)
require.Equal(t, "Open door", actions[0].Label)
require.Equal(t, "https://door.lan/open", actions[0].URL)
require.Equal(t, "PUT", actions[0].Method)
require.Equal(t, "this is a body", actions[0].Body)
actions, err = parseActions("action=broadcast, label=Do a thing, extras.command=some command, extras.some_param=a parameter")
require.Nil(t, err)
require.Equal(t, 1, len(actions))
require.Equal(t, "broadcast", actions[0].Action)
require.Equal(t, "Do a thing", actions[0].Label)
require.Equal(t, 2, len(actions[0].Extras))
require.Equal(t, "some command", actions[0].Extras["command"])
require.Equal(t, "a parameter", actions[0].Extras["some_param"])
actions, err = parseActions("action=http, label=Send request, url=http://example.com, method=GET, headers.Content-Type=application/json, headers.Authorization=Basic sdasffsf")
require.Nil(t, err)
require.Equal(t, 1, len(actions))
require.Equal(t, "http", actions[0].Action)
require.Equal(t, "Send request", actions[0].Label)
require.Equal(t, 2, len(actions[0].Headers))
require.Equal(t, "application/json", actions[0].Headers["Content-Type"])
require.Equal(t, "Basic sdasffsf", actions[0].Headers["Authorization"])
}

View File

@@ -77,6 +77,16 @@ func SplitNoEmpty(s string, sep string) []string {
return res
}
// SplitKV splits a string into a key/value pair using a separator, and trimming space. If the separator
// is not found, key is empty.
func SplitKV(s string, sep string) (key string, value string) {
kv := strings.SplitN(strings.TrimSpace(s), sep, 2)
if len(kv) == 2 {
return strings.TrimSpace(kv[0]), strings.TrimSpace(kv[1])
}
return "", strings.TrimSpace(kv[0])
}
// RandomString returns a random string with a given length
func RandomString(length int) string {
randomMutex.Lock() // Who would have thought that random.Intn() is not thread-safe?!

View File

@@ -152,3 +152,17 @@ func TestParseSize_FailureInvalid(t *testing.T) {
t.Fatalf("expected error, but got none")
}
}
func TestSplitKV(t *testing.T) {
key, value := SplitKV(" key = value ", "=")
require.Equal(t, "key", key)
require.Equal(t, "value", value)
key, value = SplitKV(" value ", "=")
require.Equal(t, "", key)
require.Equal(t, "value", value)
key, value = SplitKV("mykey=value=with=separator ", "=")
require.Equal(t, "mykey", key)
require.Equal(t, "value=with=separator", value)
}

2103
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,9 +15,13 @@
"@mui/material": "latest",
"dexie": "^3.2.1",
"dexie-react-hooks": "^1.1.1",
"i18next": "^21.6.14",
"i18next-browser-languagedetector": "^6.1.4",
"i18next-http-backend": "^1.4.0",
"js-base64": "^3.7.2",
"react": "latest",
"react-dom": "latest",
"react-i18next": "^11.16.2",
"react-infinite-scroll-component": "^6.1.0",
"react-router-dom": "^6.2.2",
"react-scripts": "^5.0.0",

View File

@@ -0,0 +1,154 @@
{
"action_bar_clear_notifications": "Премахване на известия",
"alert_grant_description": "Разрешете на мрежовия четец да показва известия.",
"notifications_attachment_copy_url_title": "Копиране на адреса на прикачения файл",
"notifications_example": "Пример",
"notifications_no_subscriptions_title": "Липсват абонаменти",
"nav_topics_title": "Абонаменти",
"action_bar_send_test_notification": "Пробно известие",
"action_bar_unsubscribe": "Отписване",
"nav_button_all_notifications": "Всички известия",
"action_bar_settings": "Настройки",
"publish_dialog_title_topic": "Публикуване в темата {{topic}}",
"publish_dialog_title_no_topic": "Изпращане",
"publish_dialog_progress_uploading": "Изпращане…",
"publish_dialog_progress_uploading_detail": "Изпращане {{loaded}}/{{total}} ({{percent}}%)…",
"publish_dialog_message_published": "Известието е публикувано",
"publish_dialog_attachment_limits_file_and_quota_reached": "надвишава ограничението и квотата от {{fileSizeLimit}}, оставащи {{remainingBytes}}",
"publish_dialog_message_label": "Съобщение",
"publish_dialog_message_placeholder": "Въведете съобщение",
"publish_dialog_other_features": "Други възможности:",
"publish_dialog_chip_click_label": "Адрес",
"publish_dialog_chip_email_label": "Препращане към ел. поща",
"publish_dialog_chip_attach_url_label": "Прикачване на файл от адрес",
"publish_dialog_chip_attach_file_label": "Прикачване местен файл",
"publish_dialog_chip_delay_label": "Забавяне на изпращането",
"publish_dialog_chip_topic_label": "Промяна на темата",
"publish_dialog_button_cancel_sending": "Отменяне на изпращането",
"publish_dialog_button_cancel": "Отказ",
"subscribe_dialog_error_user_anonymous": "анонимен",
"prefs_notifications_title": "Известия",
"prefs_notifications_sound_title": "Звук при получаване",
"prefs_notifications_sound_no_sound": "Без звук",
"prefs_notifications_min_priority_title": "Минимален приоритет",
"prefs_notifications_min_priority_any": "Всички",
"prefs_notifications_min_priority_low_and_higher": "Нисък приоритет и по-висок",
"prefs_notifications_min_priority_default_and_higher": "Подразбиран приоритет и по-висок",
"prefs_notifications_min_priority_high_and_higher": "Висок приоритет и по-висок",
"prefs_notifications_min_priority_max_only": "Само максимален приоритет",
"prefs_notifications_delete_after_never": "Никога",
"prefs_users_add_button": "Добавяне",
"prefs_users_dialog_password_label": "Парола",
"alert_not_supported_description": "Мрежовият четец не поддържа известия.",
"message_bar_type_message": "Въведете съобщение",
"message_bar_error_publishing": "Грешка при изпращане на известието",
"notifications_copied_to_clipboard": "Копирано в междинната памет",
"notifications_attachment_link_expired": "препратката за изтегляне е невалидна",
"nav_button_settings": "Настройки",
"nav_button_documentation": "Ръководство",
"nav_button_subscribe": "Абониране за тема",
"alert_grant_title": "Известията са изключени",
"alert_grant_button": "Разрешаване",
"notifications_tags": "Етикети",
"nav_button_publish_message": "Изпращане",
"alert_not_supported_title": "Не се поддържат известия",
"notifications_attachment_open_title": "Към {{url}}",
"notifications_attachment_copy_url_button": "Копиране на адреса",
"notifications_attachment_open_button": "Отваряне на прикачения файл",
"notifications_attachment_link_expires": "препратката изтича на {{date}}",
"notifications_actions_open_url_title": "Към {{url}}",
"notifications_click_copy_url_button": "Копиране на препратка",
"notifications_click_open_button": "Отваряне",
"notifications_click_copy_url_title": "Копира препратката в междинната памет",
"notifications_none_for_topic_title": "Липсват известия в темата",
"notifications_none_for_any_title": "Липсват известия",
"notifications_none_for_topic_description": "За да изпратите известия в тази тема, просто изпратете PUT или POST към адреса ѝ.",
"notifications_none_for_any_description": "За да изпратите известия в тема, просто изпратете PUT или POST към адреса ѝ. Ето пример с една от вашите теми.",
"notifications_no_subscriptions_description": "Щракнете върху „{{linktext}}“, за да създадете тема или да се абонирате. След това като изпратите съобщения чрез метода PUT или POST ще ги получавате тук.",
"notifications_more_details": "За допълнителна информация посетете <websiteLink>страницата</websiteLink> или <docsLink>документацията</docsLink>.",
"publish_dialog_priority_min": "Мин. приоритет",
"publish_dialog_attachment_limits_file_reached": "надвишава ограничението от {{fileSizeLimit}}",
"publish_dialog_base_url_label": "Адрес на услугата",
"publish_dialog_base_url_placeholder": "Адрес на услугата, напр. https://example.com",
"publish_dialog_topic_placeholder": "Име на темата, напр. phils_alerts",
"publish_dialog_priority_low": "Нисък приоритет",
"publish_dialog_attachment_limits_quota_reached": "надвишава ограничението, оставащи {{remainingBytes}}",
"publish_dialog_priority_high": "Висок приоритет",
"publish_dialog_priority_default": "Подразбиран приоритет",
"publish_dialog_title_placeholder": "Заглавие на известието, напр. Предупреждение за диска",
"publish_dialog_tags_label": "Етикети",
"publish_dialog_email_label": "Адрес на електронна поща",
"publish_dialog_priority_max": "Макс. приоритет",
"publish_dialog_tags_placeholder": "Разделени със запетая етикети, напр. внимание, диск",
"publish_dialog_click_label": "Адрес",
"publish_dialog_topic_label": "Име на темата",
"publish_dialog_title_label": "Заглавие",
"publish_dialog_priority_label": "Приоритет",
"publish_dialog_click_placeholder": "Адрес, който се отваря при щракване върху известието",
"publish_dialog_email_placeholder": "Поща, на която да се препрати известието, напр. phil@example.com",
"publish_dialog_attach_label": "Адрес на прикачения файл",
"publish_dialog_filename_placeholder": "Име на прикачения файл",
"publish_dialog_attach_placeholder": "Прикачете файл от адрес, напр. https://f-droid.org/F-Droid.apk",
"prefs_notifications_delete_after_three_hours": "След три часа",
"publish_dialog_filename_label": "Име на файла",
"publish_dialog_delay_label": "Забавяне",
"publish_dialog_details_examples_description": "За примери и подробно описание на всички възможности при изпращане, вижте <docsLink>документацията</docsLink>.",
"publish_dialog_button_send": "Изпращане",
"publish_dialog_checkbox_publish_another": "Изпращане на повече",
"publish_dialog_attached_file_title": "Прикачен файл:",
"publish_dialog_attached_file_filename_placeholder": "Име на прикачения файл",
"publish_dialog_drop_file_here": "Пуснете файла тук",
"subscribe_dialog_subscribe_description": "Възможно е темите да не са защитени с парола, затова изберете име, което е трудно за отгатване. След като се абонирате, можете да изпращате известия по PUT или POST.",
"emoji_picker_search_placeholder": "Търсете емоция",
"subscribe_dialog_subscribe_title": "Абониране за тема",
"subscribe_dialog_subscribe_topic_placeholder": "Име на темата, напр. phils_alerts",
"subscribe_dialog_subscribe_use_another_label": "Използване на друг сървър",
"subscribe_dialog_login_username_label": "Потребител, напр. phil",
"subscribe_dialog_login_button_back": "Назад",
"subscribe_dialog_subscribe_button_cancel": "Отказ",
"subscribe_dialog_login_description": "Темата е защитена. За да се абонирате въведете потребител и парола.",
"subscribe_dialog_subscribe_button_subscribe": "Абониране",
"subscribe_dialog_login_title": "Изисква се вход",
"prefs_notifications_delete_after_title": "Автоматично премахване",
"prefs_notifications_delete_after_one_day": "След един ден",
"prefs_users_table_user_header": "Потребител",
"prefs_users_dialog_title_edit": "Промяна на потребител",
"prefs_users_dialog_base_url_label": "Адрес на услугата, e.g. https://ntfy.sh",
"prefs_users_dialog_button_cancel": "Отказ",
"prefs_users_dialog_button_save": "Запазване",
"prefs_appearance_language_title": "Език",
"subscribe_dialog_login_password_label": "Парола",
"subscribe_dialog_login_button_login": "Вход",
"subscribe_dialog_error_user_not_authorized": "Потребителят {{username}} няма достъп",
"prefs_appearance_title": "Външен вид",
"publish_dialog_delay_placeholder": "Забавяне на изпращането, {{unixTimestamp}}, {{relativeTime}} или „{{naturalLanguage}}“ (на английски)",
"prefs_notifications_delete_after_one_week": "След една седмица",
"prefs_users_title": "Управление на потребители",
"prefs_users_table_base_url_header": "Адрес на услугата",
"prefs_users_dialog_title_add": "Добавяне на потребител",
"prefs_notifications_delete_after_one_month": "След един месец",
"prefs_users_dialog_username_label": "Потребител, напр. phil",
"prefs_users_dialog_button_add": "Добавяне",
"error_boundary_title": "О, не, ntfy се срина",
"error_boundary_description": "Това очевидно не трябва да се случва. Много съжаляваме!<br/>Ако имате минута, <githubLink>докладвайте в GitHub</githubLink>, или ни уведомете в <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
"error_boundary_stack_trace": "Следа от стека",
"error_boundary_gathering_info": "Събиране на допълнителна информация…",
"notifications_loading": "Зареждане на известия…",
"error_boundary_button_copy_stack_trace": "Копиране на следата от стека",
"prefs_users_description": "Добавяйте и премахвайте потребители за защитените теми. Имайте предвид, че потребителското име и паролата се съхраняват в местната памет на мрежовия четец.",
"prefs_notifications_sound_description_none": "Известията не са съпроводени със звук",
"prefs_notifications_sound_description_some": "Известията са съпроводени със звука „{{sound}}“",
"prefs_notifications_delete_after_never_description": "Известията никога не се премахват автоматично",
"prefs_notifications_delete_after_three_hours_description": "Известията се премахват автоматично след три часа",
"priority_min": "минимален",
"priority_low": "нисък",
"priority_high": "висок",
"priority_max": "максимален",
"priority_default": "подразбиран",
"prefs_notifications_delete_after_one_week_description": "Известията се премахват автоматично след една седмица",
"prefs_notifications_delete_after_one_day_description": "Известията се премахват автоматично след един ден",
"prefs_notifications_min_priority_description_max": "Показват се известията с приоритет 5 (най-висок)",
"prefs_notifications_delete_after_one_month_description": "Известията се премахват автоматично след един месец",
"prefs_notifications_min_priority_description_any": "Показват се всички известия, независимо от приоритета им",
"prefs_notifications_min_priority_description_x_or_higher": "Показват се известията с приоритет {{number}} ({{name}}) или по-висок"
}

View File

@@ -0,0 +1,154 @@
{
"nav_topics_title": "Abonnierte Themen",
"nav_button_all_notifications": "Alle Benachrichtigungen",
"nav_button_settings": "Einstellungen",
"nav_button_documentation": "Dokumentation",
"nav_button_publish_message": "Benachrichtigung senden",
"nav_button_subscribe": "Thema abonnieren",
"alert_grant_title": "Benachrichtigungen sind deaktiviert",
"publish_dialog_base_url_label": "Service-URL",
"publish_dialog_details_examples_description": "Beispiele und ausführliche Informationen zu allen Optionen findest Du in der <docsLink>Dokumentation</docsLink>.",
"publish_dialog_attached_file_filename_placeholder": "Dateiname des Anhangs",
"subscribe_dialog_login_description": "Dieses Thema benötigt eine Anmeldung. Bitte gib Benutzernamen und Kennwort ein.",
"prefs_notifications_title": "Benachrichtigungen",
"prefs_notifications_sound_title": "Benachrichtigungston",
"prefs_notifications_min_priority_max_only": "Nur höchste Priorität",
"prefs_notifications_delete_after_never": "Nie",
"prefs_users_dialog_password_label": "Kennwort",
"prefs_users_dialog_button_cancel": "Abbrechen",
"prefs_users_dialog_button_add": "Hinzufügen",
"prefs_users_dialog_button_save": "Speichern",
"prefs_appearance_language_title": "Sprache",
"notifications_none_for_any_description": "Um Benachrichtigungen an ein Thema zu senden, schicke einen PUT/POST-Request an die Themen-URL. Hier ist ein Beispiel mit einem Deiner Themen.",
"publish_dialog_message_placeholder": "Gib hier eine Nachricht ein",
"notifications_attachment_link_expires": "Link läuft ab am/um {{date}}",
"notifications_click_copy_url_title": "Link-URL in Zwischenablage kopieren",
"publish_dialog_priority_low": "Niedrige Priorität",
"publish_dialog_message_label": "Nachricht",
"action_bar_unsubscribe": "Von Thema abmelden",
"notifications_copied_to_clipboard": "In Zwischenablage kopiert",
"notifications_loading": "Benachrichtigungen werden geladen …",
"notifications_attachment_open_title": "Gehe zu {{url}}",
"notifications_none_for_any_title": "Du hast keine Benachrichtigungen empfangen.",
"action_bar_send_test_notification": "Test-Benachrichtigung senden",
"alert_grant_description": "Dem Browser erlauben, Desktop-Benachrichtigungen anzuzeigen.",
"notifications_tags": "Tags",
"message_bar_type_message": "Gib hier eine Nachricht ein",
"message_bar_error_publishing": "Fehler beim Senden der Benachrichtigung",
"alert_not_supported_title": "Benachrichtigungen werden nicht unterstützt",
"alert_not_supported_description": "Benachrichtigungen werden von Deinem Browser nicht unterstützt.",
"action_bar_settings": "Einstellungen",
"action_bar_clear_notifications": "Alle Benachrichtigungen löschen",
"alert_grant_button": "Jetzt erlauben",
"notifications_none_for_topic_title": "Du hast für dieses Thema noch keine Benachrichtigungen empfangen.",
"notifications_click_open_button": "Link öffnen",
"notifications_more_details": "Ausführlichere Informationen findest Du auf der <websiteLink>Website</websiteLink> und in der <docsLink>Dokumentation</docsLink>.",
"notifications_attachment_copy_url_title": "URL des Anhangs in Zwischenablage kopieren",
"notifications_attachment_copy_url_button": "URL kopieren",
"notifications_attachment_open_button": "Anhang öffnen",
"notifications_attachment_link_expired": "Download-Link ist abgelaufen",
"notifications_click_copy_url_button": "Link kopieren",
"notifications_actions_open_url_title": "Gehe zu {{url}}",
"publish_dialog_other_features": "Andere Optionen:",
"notifications_none_for_topic_description": "Um Benachrichtigungen an dieses Thema zu senden, PUTe/POSTe an die Themen-URL.",
"notifications_no_subscriptions_title": "Anscheinend hast Du noch keine Themen abonniert.",
"notifications_no_subscriptions_description": "Klicke den „{{linktext}}“-Link um ein Thema zu erstellen oder zu abonnieren. Danach kannst Du Nachrichten per PUT oder POST senden und erhältst hier die Benachrichtigungen.",
"notifications_example": "Beispiel",
"publish_dialog_progress_uploading": "Wird hochgeladen …",
"publish_dialog_title_topic": "Senden an {{topic}}",
"publish_dialog_title_no_topic": "Benachrichtigung senden",
"publish_dialog_message_published": "Benachrichtigung gesendet",
"publish_dialog_attachment_limits_file_and_quota_reached": "überschreitet das Dateigrößen-Limit {{fileSizeLimit}} und die Quota, {{remainingBytes}} übrig",
"publish_dialog_progress_uploading_detail": "Hochladen {{loaded}}/{{total}} ({{percent}} %) …",
"publish_dialog_priority_max": "Max. Priorität",
"publish_dialog_topic_placeholder": "Thema, z.B. phil_alerts",
"publish_dialog_attachment_limits_file_reached": "überschreitet das Dateigrößen-Limit {{filesizeLimit}}",
"publish_dialog_topic_label": "Thema",
"publish_dialog_priority_default": "Standard-Priorität",
"publish_dialog_base_url_placeholder": "Service-URL, z.B. https://example.com",
"publish_dialog_attachment_limits_quota_reached": "überschreitet die Quota, {{remainingBytes}} übrig",
"publish_dialog_priority_min": "Min. Priorität",
"publish_dialog_priority_high": "Hohe Priorität",
"publish_dialog_title_label": "Titel",
"publish_dialog_tags_placeholder": "Komma-getrennte Liste von Tags, z.B. Warnung, srv1-Backup",
"publish_dialog_priority_label": "Priorität",
"publish_dialog_filename_label": "Dateiname",
"publish_dialog_title_placeholder": "Benachrichtigungs-Titel, z.B. CPU-Last-Warnung",
"publish_dialog_tags_label": "Tags",
"publish_dialog_click_label": "Klick-URL",
"publish_dialog_click_placeholder": "URL die geöffnet werden soll, wenn die Benachrichtigung angeklickt wird",
"publish_dialog_email_label": "E-Mail",
"publish_dialog_attach_label": "URL des Anhangs",
"publish_dialog_attach_placeholder": "Datei von URL anhängen, z.B. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_placeholder": "Dateiname des Anhangs",
"publish_dialog_delay_label": "Verzögerung",
"publish_dialog_email_placeholder": "Adresse, an die die Benachrichtigung gesendet werden soll, z.B. phil@beispiel.com",
"publish_dialog_chip_click_label": "Klick-URL",
"publish_dialog_button_cancel_sending": "Senden abbrechen",
"publish_dialog_drop_file_here": "Datei hierher ziehen",
"publish_dialog_chip_email_label": "An E-Mail weiterleiten",
"publish_dialog_button_cancel": "Abbrechen",
"publish_dialog_chip_attach_file_label": "Lokale Datei anhängen",
"prefs_notifications_min_priority_title": "Minimale Priorität",
"prefs_users_add_button": "Benutzer hinzufügen",
"publish_dialog_delay_placeholder": "Auslieferung verzögern, z.B. {{unixTimestamp}}, {{relativeTime}}, oder \"{{naturalLanguage}}\" (nur Englisch)",
"prefs_appearance_title": "Darstellung",
"subscribe_dialog_login_password_label": "Kennwort",
"subscribe_dialog_login_button_back": "Zurück",
"publish_dialog_chip_attach_url_label": "Datei von URL anhängen",
"publish_dialog_chip_delay_label": "Auslieferung verzögern",
"publish_dialog_chip_topic_label": "Thema ändern",
"subscribe_dialog_subscribe_title": "Thema abonnieren",
"subscribe_dialog_login_username_label": "Benutzername, z.B. phil",
"subscribe_dialog_login_button_login": "Anmelden",
"prefs_notifications_sound_no_sound": "Kein Ton",
"prefs_notifications_min_priority_default_and_higher": "Standard-Priorität und höher",
"subscribe_dialog_subscribe_topic_placeholder": "Thema, z.B. phil_alerts",
"publish_dialog_button_send": "Senden",
"publish_dialog_checkbox_publish_another": "Weitere Nachricht senden",
"publish_dialog_attached_file_title": "Dateianhang:",
"emoji_picker_search_placeholder": "Emoji suchen",
"subscribe_dialog_subscribe_description": "Themen sind evtl. nicht kennwort-geschützt, also wähle einen schwer zu erratenden Namen. Nach dem Abonnieren kannst Du Benachrichtigungen per POST/PUT senden.",
"subscribe_dialog_subscribe_use_another_label": "Anderen Server verwenden",
"subscribe_dialog_subscribe_button_cancel": "Abbrechen",
"subscribe_dialog_subscribe_button_subscribe": "Abonnieren",
"subscribe_dialog_login_title": "Anmeldung erforderlich",
"subscribe_dialog_error_user_anonymous": "anonym",
"subscribe_dialog_error_user_not_authorized": "Benutzer {{username}} hat keine Berechtigung",
"prefs_notifications_min_priority_any": "Alle Prioritäten",
"prefs_notifications_min_priority_low_and_higher": "Niedrige Priorität und höher",
"prefs_notifications_min_priority_high_and_higher": "Hohe Priorität und höher",
"prefs_notifications_delete_after_title": "Benachrichtigungen löschen",
"prefs_notifications_delete_after_three_hours": "Nach drei Stunden",
"prefs_users_dialog_title_edit": "Benutzer bearbeiten",
"prefs_notifications_delete_after_one_day": "Nach einem Tag",
"prefs_notifications_delete_after_one_week": "Nach einer Woche",
"prefs_notifications_delete_after_one_month": "Nach einem Monat",
"prefs_users_title": "Benutzer verwalten",
"prefs_users_table_user_header": "Benutzer",
"prefs_users_table_base_url_header": "Service-URL",
"prefs_users_dialog_base_url_label": "Service-URL, z.B. https://ntfy.sh",
"prefs_users_dialog_username_label": "Benutzername, z.B. phil",
"prefs_users_description": "Benutzer für kennwort-geschützte Themen hinzufügen/löschen. Achtung: Benutzername und Kennwort werden im lokalen Browser-Speicher abgelegt.",
"prefs_users_dialog_title_add": "Benutzer hinzufügen",
"error_boundary_title": "Oh nein, ntfy ist abgestürzt",
"error_boundary_description": "Das sollte offensichtlich nicht passieren. Sorry.<br/>Wenn möglich, <githubLink>melde den Fehler auf GitHub</githubLink> oder schreibe uns auf <discordLink>Discord</discordLink> oder <matrixLink>Matrix</matrixLink>.",
"error_boundary_stack_trace": "Stacktrace",
"error_boundary_gathering_info": "Weitere Informationen sammeln …",
"error_boundary_button_copy_stack_trace": "Stacktrace kopieren",
"prefs_notifications_delete_after_never_description": "Benachrichtigungen werden nie automatisch gelöscht",
"prefs_notifications_delete_after_one_month_description": "Benachrichtigungen werden nach einem Monat automatisch gelöscht",
"prefs_notifications_min_priority_description_any": "Alle Benachrichtigungen (aller Prioritäten) anzeigen",
"prefs_notifications_min_priority_description_max": "Zeige Benachrichtigungen wenn ihre Priorität5 (max) ist",
"priority_low": "niedrig",
"priority_default": "Standard",
"priority_high": "hoch",
"priority_max": "max",
"prefs_notifications_sound_description_none": "Kein Ton beim Empfang einer Benachrichtigung",
"prefs_notifications_sound_description_some": "Sound {{sound}} beim Eintreffen einer Benachrichtigung abspielen",
"prefs_notifications_min_priority_description_x_or_higher": "Zeige Benachrichtigungen wenn ihre Priorität {{number}} ({{name}}) oder höher ist",
"prefs_notifications_delete_after_three_hours_description": "Benachrichtigungen werden nach drei Stunden automatisch gelöscht",
"prefs_notifications_delete_after_one_day_description": "Benachrichtigungen werden nach einem Tag automatisch gelöscht",
"prefs_notifications_delete_after_one_week_description": "Benachrichtigungen werden nach einer Woche automatisch gelöscht",
"priority_min": "min"
}

View File

@@ -0,0 +1,156 @@
{
"action_bar_settings": "Settings",
"action_bar_send_test_notification": "Send test notification",
"action_bar_clear_notifications": "Clear all notifications",
"action_bar_unsubscribe": "Unsubscribe",
"message_bar_type_message": "Type a message here",
"message_bar_error_publishing": "Error publishing notification",
"nav_topics_title": "Subscribed topics",
"nav_button_all_notifications": "All notifications",
"nav_button_settings": "Settings",
"nav_button_documentation": "Documentation",
"nav_button_publish_message": "Publish notification",
"nav_button_subscribe": "Subscribe to topic",
"alert_grant_title": "Notifications are disabled",
"alert_grant_description": "Grant your browser permission to display desktop notifications.",
"alert_grant_button": "Grant now",
"alert_not_supported_title": "Notifications not supported",
"alert_not_supported_description": "Notifications are not supported in your browser.",
"notifications_copied_to_clipboard": "Copied to clipboard",
"notifications_tags": "Tags",
"notifications_attachment_copy_url_title": "Copy attachment URL to clipboard",
"notifications_attachment_copy_url_button": "Copy URL",
"notifications_attachment_open_title": "Go to {{url}}",
"notifications_attachment_open_button": "Open attachment",
"notifications_attachment_link_expires": "link expires {{date}}",
"notifications_attachment_link_expired": "download link expired",
"notifications_click_copy_url_title": "Copy link URL to clipboard",
"notifications_click_copy_url_button": "Copy link",
"notifications_click_open_button": "Open link",
"notifications_actions_open_url_title": "Go to {{url}}",
"notifications_actions_not_supported": "Action not supported in web app",
"notifications_actions_http_request_title": "Send HTTP {{method}} to {{url}}",
"notifications_none_for_topic_title": "You haven't received any notifications for this topic yet.",
"notifications_none_for_topic_description": "To send notifications to this topic, simply PUT or POST to the topic URL.",
"notifications_none_for_any_title": "You haven't received any notifications.",
"notifications_none_for_any_description": "To send notifications to a topic, simply PUT or POST to the topic URL. Here's an example using one of your topics.",
"notifications_no_subscriptions_title": "It looks like you don't have any subscriptions yet.",
"notifications_no_subscriptions_description": "Click the \"{{linktext}}\" link to create or subscribe to a topic. After that, you can send messages via PUT or POST and you'll receive notifications here.",
"notifications_example": "Example",
"notifications_more_details": "For more information, check out the <websiteLink>website</websiteLink> or <docsLink>documentation</docsLink>.",
"notifications_loading": "Loading notifications …",
"publish_dialog_title_topic": "Publish to {{topic}}",
"publish_dialog_title_no_topic": "Publish notification",
"publish_dialog_progress_uploading": "Uploading …",
"publish_dialog_progress_uploading_detail": "Uploading {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "Notification published",
"publish_dialog_attachment_limits_file_and_quota_reached": "exceeds {{fileSizeLimit}} file limit and quota, {{remainingBytes}} remaining",
"publish_dialog_attachment_limits_file_reached": "exceeds {{fileSizeLimit}} file limit",
"publish_dialog_attachment_limits_quota_reached": "exceeds quota, {{remainingBytes}} remaining",
"publish_dialog_priority_min": "Min. priority",
"publish_dialog_priority_low": "Low priority",
"publish_dialog_priority_default": "Default priority",
"publish_dialog_priority_high": "High priority",
"publish_dialog_priority_max": "Max. priority",
"publish_dialog_base_url_label": "Service URL",
"publish_dialog_base_url_placeholder": "Service URL, e.g. https://example.com",
"publish_dialog_topic_label": "Topic name",
"publish_dialog_topic_placeholder": "Topic name, e.g. phil_alerts",
"publish_dialog_title_label": "Title",
"publish_dialog_title_placeholder": "Notification title, e.g. Disk space alert",
"publish_dialog_message_label": "Message",
"publish_dialog_message_placeholder": "Type a message here",
"publish_dialog_tags_label": "Tags",
"publish_dialog_tags_placeholder": "Comma-separated list of tags, e.g. warning, srv1-backup",
"publish_dialog_priority_label": "Priority",
"publish_dialog_click_label": "Click URL",
"publish_dialog_click_placeholder": "URL that is opened when notification is clicked",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Address to forward the notification to, e.g. phil@example.com",
"publish_dialog_attach_label": "Attachment URL",
"publish_dialog_attach_placeholder": "Attach file by URL, e.g. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_label": "Filename",
"publish_dialog_filename_placeholder": "Attachment filename",
"publish_dialog_delay_label": "Delay",
"publish_dialog_delay_placeholder": "Delay delivery, e.g. {{unixTimestamp}}, {{relativeTime}}, or \"{{naturalLanguage}}\" (English only)",
"publish_dialog_other_features": "Other features:",
"publish_dialog_chip_click_label": "Click URL",
"publish_dialog_chip_email_label": "Forward to email",
"publish_dialog_chip_attach_url_label": "Attach file by URL",
"publish_dialog_chip_attach_file_label": "Attach local file",
"publish_dialog_chip_delay_label": "Delay delivery",
"publish_dialog_chip_topic_label": "Change topic",
"publish_dialog_details_examples_description": "For examples and a detailed description of all send features, please refer to the <docsLink>documentation</docsLink>.",
"publish_dialog_button_cancel_sending": "Cancel sending",
"publish_dialog_button_cancel": "Cancel",
"publish_dialog_button_send": "Send",
"publish_dialog_checkbox_publish_another": "Publish another",
"publish_dialog_attached_file_title": "Attached file:",
"publish_dialog_attached_file_filename_placeholder": "Attachment filename",
"publish_dialog_drop_file_here": "Drop file here",
"emoji_picker_search_placeholder": "Search emoji",
"subscribe_dialog_subscribe_title": "Subscribe to topic",
"subscribe_dialog_subscribe_description": "Topics may not be password-protected, so choose a name that's not easy to guess. Once subscribed, you can PUT/POST notifications.",
"subscribe_dialog_subscribe_topic_placeholder": "Topic name, e.g. phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Use another server",
"subscribe_dialog_subscribe_button_cancel": "Cancel",
"subscribe_dialog_subscribe_button_subscribe": "Subscribe",
"subscribe_dialog_login_title": "Login required",
"subscribe_dialog_login_description": "This topic is password-protected. Please enter username and password to subscribe.",
"subscribe_dialog_login_username_label": "Username, e.g. phil",
"subscribe_dialog_login_password_label": "Password",
"subscribe_dialog_login_button_back": "Back",
"subscribe_dialog_login_button_login": "Login",
"subscribe_dialog_error_user_not_authorized": "User {{username}} not authorized",
"subscribe_dialog_error_user_anonymous": "anonymous",
"prefs_notifications_title": "Notifications",
"prefs_notifications_sound_title": "Notification sound",
"prefs_notifications_sound_description_none": "Notifications do not play any sound when they arrive",
"prefs_notifications_sound_description_some": "Notifications play the {{sound}} sound when they arrive",
"prefs_notifications_sound_no_sound": "No sound",
"prefs_notifications_min_priority_title": "Minimum priority",
"prefs_notifications_min_priority_description_any": "Showing all notifications, regardless of priority",
"prefs_notifications_min_priority_description_x_or_higher": "Show notifications if priority is {{number}} ({{name}}) or above",
"prefs_notifications_min_priority_description_max": "Show notifications if priority is 5 (max)",
"prefs_notifications_min_priority_any": "Any priority",
"prefs_notifications_min_priority_low_and_higher": "Low priority and higher",
"prefs_notifications_min_priority_default_and_higher": "Default priority and higher",
"prefs_notifications_min_priority_high_and_higher": "High priority and higher",
"prefs_notifications_min_priority_max_only": "Only max priority",
"prefs_notifications_delete_after_title": "Delete notifications",
"prefs_notifications_delete_after_never": "Never",
"prefs_notifications_delete_after_three_hours": "After three hours",
"prefs_notifications_delete_after_one_day": "After one day",
"prefs_notifications_delete_after_one_week": "After one week",
"prefs_notifications_delete_after_one_month": "After one month",
"prefs_notifications_delete_after_never_description": "Notifications are never auto-deleted",
"prefs_notifications_delete_after_three_hours_description": "Notifications are auto-deleted after three hours",
"prefs_notifications_delete_after_one_day_description": "Notifications are auto-deleted after one day",
"prefs_notifications_delete_after_one_week_description": "Notifications are auto-deleted after one week",
"prefs_notifications_delete_after_one_month_description": "Notifications are auto-deleted after one month",
"prefs_users_title": "Manage users",
"prefs_users_description": "Add/remove users for your protected topics here. Please note that username and password are stored in the browser's local storage.",
"prefs_users_add_button": "Add user",
"prefs_users_table_user_header": "User",
"prefs_users_table_base_url_header": "Service URL",
"prefs_users_dialog_title_add": "Add user",
"prefs_users_dialog_title_edit": "Edit user",
"prefs_users_dialog_base_url_label": "Service URL, e.g. https://ntfy.sh",
"prefs_users_dialog_username_label": "Username, e.g. phil",
"prefs_users_dialog_password_label": "Password",
"prefs_users_dialog_button_cancel": "Cancel",
"prefs_users_dialog_button_add": "Add",
"prefs_users_dialog_button_save": "Save",
"prefs_appearance_title": "Appearance",
"prefs_appearance_language_title": "Language",
"priority_min": "min",
"priority_low": "low",
"priority_default": "default",
"priority_high": "high",
"priority_max": "max",
"error_boundary_title": "Oh no, ntfy crashed",
"error_boundary_description": "This should obviously not happen. Very sorry about this.<br/>If you have a minute, please <githubLink>report this on GitHub</githubLink>, or let us know via <discordLink>Discord</discordLink> or <matrixLink>Matrix</matrixLink>.",
"error_boundary_button_copy_stack_trace": "Copy stack trace",
"error_boundary_stack_trace": "Stack trace",
"error_boundary_gathering_info": "Gather more info …"
}

View File

@@ -0,0 +1,154 @@
{
"action_bar_settings": "Configuración",
"action_bar_send_test_notification": "Enviar notificación de prueba",
"action_bar_clear_notifications": "Borrar todas las notificaciones",
"nav_topics_title": "Tópicos suscritos",
"alert_grant_button": "Conceder ahora",
"action_bar_unsubscribe": "Cancelar la suscripción",
"message_bar_type_message": "Escriba un mensaje aquí",
"message_bar_error_publishing": "Error al publicar la notificación",
"alert_grant_title": "Las notificaciones están deshabilitadas",
"alert_grant_description": "Concede a tu navegador permiso para mostrar notificaciones en el escritorio.",
"nav_button_all_notifications": "Todas las notificaciones",
"nav_button_settings": "Ajustes",
"nav_button_subscribe": "Suscribirse al tópico",
"nav_button_documentation": "Documentación",
"nav_button_publish_message": "Publicar notificación",
"notifications_copied_to_clipboard": "Copiado al portapapeles",
"alert_not_supported_title": "Notificaciones no soportadas",
"alert_not_supported_description": "Las notificaciones no están soportadas por tu navegador.",
"notifications_tags": "Etiquetas",
"notifications_attachment_copy_url_title": "Copiar la URL del archivo adjunto en el portapapeles",
"notifications_attachment_copy_url_button": "Copiar URL",
"notifications_attachment_open_title": "Ir a {{url}}",
"notifications_attachment_open_button": "Abrir archivo adjunto",
"notifications_attachment_link_expires": "el enlace expira el día {{fecha}}",
"notifications_attachment_link_expired": "el enlace de descarga ha expirado",
"notifications_click_copy_url_title": "Copiar la URL del enlace en el portapapeles",
"notifications_click_copy_url_button": "Copiar enlace",
"notifications_actions_open_url_title": "Ir a {{url}}",
"notifications_click_open_button": "Abrir enlace",
"notifications_none_for_topic_title": "Aún no has recibido ninguna notificación en este tópico.",
"notifications_none_for_topic_description": "Para enviar notificaciones a este tópico, simplemente realice un PUT o POST a la URL del tópico.",
"notifications_none_for_any_title": "No ha recibido ninguna notificación.",
"notifications_no_subscriptions_title": "Parece que aún no tiene ninguna suscripción.",
"notifications_no_subscriptions_description": "Haga clic en el enlace \"{{linktext}}\" para crear o suscribirse a un tópico. Después, puede enviar mensajes a través de un PUT o POST y recibirá notificaciones aquí.",
"notifications_more_details": "Para más información, consulta la <websiteLink>página web</websiteLink> o la <docsLink>documentación</docsLink>.",
"notifications_loading": "Cargando notificaciones …",
"publish_dialog_title_topic": "Publicar en {{topic}}",
"publish_dialog_title_no_topic": "Publicar notificación",
"publish_dialog_progress_uploading": "Cargando …",
"publish_dialog_progress_uploading_detail": "Cargando {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "Notificación publicada",
"publish_dialog_attachment_limits_file_and_quota_reached": "supera el límite y la cuota de archivos de {{fileSizeLimit}}, restan {{remainingBytes}}",
"publish_dialog_attachment_limits_file_reached": "supera el límite de archivos de {{fileSizeLimit}}",
"publish_dialog_attachment_limits_quota_reached": "supera la cuota, restan {{remainingBytes}}",
"publish_dialog_priority_min": "Prioridad mínima",
"publish_dialog_priority_default": "Prioridad predeterminada",
"publish_dialog_priority_max": "Prioridad máxima",
"publish_dialog_base_url_label": "URL del servicio",
"publish_dialog_base_url_placeholder": "URL del servicio, por ejemplo, https://example.com",
"publish_dialog_topic_label": "Nombre del tópico",
"publish_dialog_topic_placeholder": "Nombre del tópico, ej. phil_alerts",
"publish_dialog_title_label": "Título",
"publish_dialog_message_label": "Mensaje",
"publish_dialog_tags_placeholder": "Lista de etiquetas separadas por comas, por ejemplo: warning, srv1-backup",
"publish_dialog_click_label": "Click URL",
"publish_dialog_click_placeholder": "URL que se abre cuando se hace click en la notificación",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Dirección a la que se reenviará la notificación, por ejemplo, phil@example.com",
"publish_dialog_attach_label": "URL del archivo adjunto",
"publish_dialog_filename_label": "Nombre del archivo",
"publish_dialog_delay_placeholder": "Retraso en la entrega, por ejemplo, {{unixTimestamp}}, {{relativeTime}}, o \"{{naturalLanguage}}\" (sólo en inglés)",
"publish_dialog_other_features": "Otras características:",
"publish_dialog_chip_click_label": "Click URL",
"publish_dialog_chip_email_label": "Reenviar al email",
"publish_dialog_chip_attach_url_label": "Adjuntar un archivo por URL",
"publish_dialog_chip_attach_file_label": "Adjuntar archivo local",
"publish_dialog_chip_topic_label": "Cambiar de tópico",
"publish_dialog_button_cancel_sending": "Cancelar el envío",
"publish_dialog_button_cancel": "Cancelar",
"publish_dialog_checkbox_publish_another": "Publicar otro",
"publish_dialog_attached_file_title": "Archivo adjunto:",
"publish_dialog_attached_file_filename_placeholder": "Nombre del archivo adjunto",
"publish_dialog_drop_file_here": "Suelta el archivo aquí",
"emoji_picker_search_placeholder": "Buscar emojis",
"subscribe_dialog_subscribe_title": "Suscribirse al tópico",
"subscribe_dialog_subscribe_description": "Los tópicos pueden no estar protegidos por contraseña, así que elija un nombre que no sea fácil de adivinar. Una vez suscrito, puede hacer PUT/PIST de notificaciones.",
"subscribe_dialog_subscribe_topic_placeholder": "Nombre del tópico, ej. phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Usar otro servidor",
"subscribe_dialog_login_title": "Es necesario iniciar sesión",
"subscribe_dialog_login_description": "Este tópico está protegido por contraseña. Por favor, introduzca su nombre de usuario y contraseña para suscribirse.",
"subscribe_dialog_login_username_label": "Nombre de usuario, ej. phil",
"subscribe_dialog_login_password_label": "Contraseña",
"subscribe_dialog_login_button_back": "Volver",
"subscribe_dialog_login_button_login": "Iniciar sesión",
"subscribe_dialog_error_user_not_authorized": "Usuario {{username}} no autorizado",
"subscribe_dialog_error_user_anonymous": "anónimo",
"prefs_notifications_title": "Notificaciones",
"prefs_notifications_sound_title": "Sonido de notificación",
"prefs_notifications_min_priority_any": "Cualquier prioridad",
"prefs_notifications_min_priority_low_and_higher": "Prioridad baja y superior",
"prefs_notifications_min_priority_max_only": "Solo prioridad máxima",
"prefs_notifications_delete_after_title": "Eliminar notificaciones",
"prefs_notifications_delete_after_never": "Nunca",
"prefs_notifications_delete_after_three_hours": "Después de tres horas",
"prefs_notifications_delete_after_one_day": "Después de un día",
"prefs_notifications_delete_after_one_week": "Después de una semana",
"prefs_notifications_delete_after_one_month": "Después de un mes",
"prefs_users_title": "Administrar usuarios",
"prefs_users_description": "Añada/elimine usuarios para sus tópicos protegidos aquí. Tenga en cuenta que el nombre de usuario y la contraseña se guardan en el almacenamiento local del navegador.",
"prefs_users_add_button": "Añadir usuario",
"prefs_users_dialog_title_edit": "Editar usuario",
"prefs_users_dialog_base_url_label": "URL del servicio, ej. https://ntfy.sh",
"prefs_users_dialog_button_add": "Añadir",
"prefs_users_dialog_button_save": "Guardar",
"prefs_appearance_title": "Apariencia",
"prefs_appearance_language_title": "Idioma",
"error_boundary_title": "Oh no, ntfy tuvo un error",
"error_boundary_button_copy_stack_trace": "Copiar el stack trace",
"error_boundary_stack_trace": "Stack trace",
"error_boundary_gathering_info": "Reunir más información …",
"notifications_example": "Ejemplo",
"prefs_notifications_min_priority_title": "Prioridad mínima",
"notifications_none_for_any_description": "Para enviar notificaciones a un tópico, simplemente realice un PUT o POST a la URL del tópico. Aquí hay un ejemplo usando uno de sus tópicos.",
"subscribe_dialog_subscribe_button_cancel": "Cancelar",
"subscribe_dialog_subscribe_button_subscribe": "Suscribir",
"publish_dialog_message_placeholder": "Escriba un mensaje aquí",
"publish_dialog_tags_label": "Etiquetas",
"publish_dialog_priority_label": "Prioridad",
"publish_dialog_priority_low": "Prioridad baja",
"publish_dialog_priority_high": "Prioridad alta",
"publish_dialog_delay_label": "Retraso",
"publish_dialog_title_placeholder": "Título de la notificación, por ejemplo, Alerta de espacio en disco",
"publish_dialog_details_examples_description": "Para ver ejemplos y una descripción detallada de todas las funciones de envío, consulte la <docsLink>documentación</docsLink>.",
"publish_dialog_attach_placeholder": "Adjuntar un archivo por URL, por ejemplo, https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_placeholder": "Nombre del archivo adjunto",
"publish_dialog_chip_delay_label": "Retraso en la entrega",
"prefs_notifications_min_priority_default_and_higher": "Prioridad predeterminada y superior",
"prefs_notifications_min_priority_high_and_higher": "Prioridad alta y superior",
"prefs_users_table_user_header": "Usuario",
"prefs_users_table_base_url_header": "URL del servicio",
"publish_dialog_button_send": "Enviar",
"prefs_notifications_sound_no_sound": "Sin sonido",
"prefs_users_dialog_password_label": "Contraseña",
"error_boundary_description": "Obviamente, esto no debería ocurrir. Lo sentimos mucho.<br/>Si tienes un minuto, por favor <githubLink>informa de esto en GitHub</githubLink>, o avísanos vía <discordLink>Discord</discordLink> o <matrixLink>Matrix</matrixLink>.",
"prefs_users_dialog_title_add": "Añadir usuario",
"prefs_users_dialog_button_cancel": "Cancelar",
"prefs_users_dialog_username_label": "Nombre de usuario, ej. phil",
"priority_max": "máx",
"priority_high": "alta",
"prefs_notifications_delete_after_one_month_description": "Las notificaciones se eliminan automáticamente después de un mes",
"priority_min": "mín",
"prefs_notifications_delete_after_three_hours_description": "Las notificaciones se eliminan automáticamente después de tres horas",
"prefs_notifications_sound_description_none": "Las notificaciones no reproducen ningún sonido cuando llegan",
"prefs_notifications_min_priority_description_x_or_higher": "Mostrar notificaciones si la prioridad es {{number}} ({{name}}) o superior",
"prefs_notifications_min_priority_description_max": "Mostrar notificaciones si la prioridad es 5 (máxima)",
"prefs_notifications_sound_description_some": "Las notificaciones reproducen el sonido {{sound}} cuando llegan",
"prefs_notifications_min_priority_description_any": "Mostrando todas las notificaciones, independientemente de su prioridad",
"prefs_notifications_delete_after_never_description": "Las notificaciones nunca se borran automáticamente",
"priority_default": "predeterminada",
"prefs_notifications_delete_after_one_day_description": "Las notificaciones se eliminan automáticamente después de un día",
"prefs_notifications_delete_after_one_week_description": "Las notificaciones se eliminan automáticamente después de una semana",
"priority_low": "baja"
}

View File

@@ -0,0 +1,56 @@
{
"nav_topics_title": "Sujets souscrits",
"action_bar_settings": "Paramètres",
"action_bar_send_test_notification": "Envoyer une notification de test",
"action_bar_clear_notifications": "Effacer toutes les notifications",
"action_bar_unsubscribe": "Se désabonner",
"message_bar_type_message": "Tapez un message ici",
"notifications_attachment_open_button": "Ouvrir la pièce jointe",
"notifications_attachment_link_expires": "le lien expire {{date}}",
"message_bar_error_publishing": "Notification d'erreur de publication",
"nav_button_all_notifications": "Toutes les notifications",
"nav_button_settings": "Paramètres",
"nav_button_documentation": "Documentation",
"alert_not_supported_description": "Les notifications ne sont pas prises en charge par votre navigateur.",
"notifications_attachment_copy_url_title": "Copier l'URL de la pièce jointe dans le presse-papiers",
"notifications_attachment_open_title": "Aller à {{url}}",
"notifications_attachment_link_expired": "lien de téléchargement expiré",
"nav_button_publish_message": "Publier la notification",
"notifications_copied_to_clipboard": "Copié dans le presse-papiers",
"alert_not_supported_title": "Notifications non prises en charge",
"notifications_tags": "Étiquettes",
"notifications_attachment_copy_url_button": "Copier l'URL",
"notifications_click_copy_url_title": "Copier l'URL du lien dans le presse-papiers",
"notifications_click_copy_url_button": "Copier le lien",
"notifications_click_open_button": "Ouvrir le lien",
"notifications_none_for_topic_title": "Vous n'avez pas encore reçu de notifications pour ce sujet.",
"notifications_actions_open_url_title": "Aller à {{url}}",
"notifications_example": "Exemple",
"notifications_loading": "Chargement des notifications…",
"publish_dialog_progress_uploading": "Téléversement…",
"publish_dialog_priority_min": "Priorité min.",
"publish_dialog_priority_low": "Basse priorité",
"publish_dialog_priority_default": "Priorité par défaut",
"publish_dialog_base_url_label": "URL du serveur",
"publish_dialog_base_url_placeholder": "URL du serveur, par ex. https://exemple.com",
"publish_dialog_title_label": "Titre",
"publish_dialog_message_label": "Message",
"publish_dialog_topic_label": "Nom du sujet",
"publish_dialog_message_placeholder": "Tapez un message ici",
"publish_dialog_tags_label": "Étiquettes",
"publish_dialog_email_label": "Courriel",
"publish_dialog_email_placeholder": "Adresse à laquelle transmettre la notification, par exemple phil@exemple.com",
"publish_dialog_chip_email_label": "Transférer vers le courriel",
"notifications_no_subscriptions_title": "Il semble que vous nayez pas encore dabonnements.",
"publish_dialog_progress_uploading_detail": "Téléversement {{loaded}}/{{total}} ({{percent}} %) …",
"publish_dialog_message_published": "Notification publiée",
"publish_dialog_attachment_limits_file_and_quota_reached": "dépasse la limite et le quota du fichier {{fileSizeLimit}}, {{remainingBytes}} restant",
"publish_dialog_priority_high": "Haute priorité",
"publish_dialog_priority_max": "Priorité max.",
"publish_dialog_attachment_limits_file_reached": "Dépasse la limite du fichier {{fileSizeLimit}}",
"nav_button_subscribe": "S'abonner au sujet",
"notifications_no_subscriptions_description": "Cliquez sur le lien « Ajouter un abonnement » pour créer ou vous abonner à un sujet. Après cela, vous pouvez envoyer des messages via PUT ou POST et vous recevrez des notifications ici.",
"alert_grant_title": "Les notifications sont désactivées",
"alert_grant_description": "Autorisez votre navigateur à afficher les notifications du bureau.",
"alert_grant_button": "Accorder maintenant"
}

View File

@@ -0,0 +1,154 @@
{
"notifications_click_copy_url_title": "Salin URL tautan ke papan klip",
"alert_not_supported_title": "Notifikasi tidak didukung",
"notifications_click_copy_url_button": "Salin tautan",
"notifications_no_subscriptions_description": "Klik pada tautan \"{{linktext}}\" untuk membuat atau berlangganan ke sebuah topik. Setelah itu, Anda dapat mengirim pesan via PUT atau POST dan Anda akan menerima notifikasi di sini.",
"notifications_example": "Contoh",
"subscribe_dialog_subscribe_description": "Topik mungkin tidak dilindungi oleh kata sandi, jadi pilih sebuah nama yang tidak mudah untuk ditebak. Setelah berlangganan, Anda dapat PUT/POST notifikasi.",
"subscribe_dialog_login_title": "Login dibutuhkan",
"prefs_appearance_language_title": "Bahasa",
"nav_button_all_notifications": "Semua notifikasi",
"notifications_none_for_any_title": "Anda belum menerima notifikasi apa pun.",
"action_bar_settings": "Pengaturan",
"action_bar_send_test_notification": "Kirim notifikasi uji coba",
"action_bar_clear_notifications": "Hapus semua notifikasi",
"action_bar_unsubscribe": "Batalkan langganan",
"message_bar_type_message": "Ketika sebuah pesan di sini",
"message_bar_error_publishing": "Terjadi kesalahan mempublikasikan notifikasi",
"publish_dialog_title_label": "Judul",
"publish_dialog_message_label": "Pesan",
"nav_button_settings": "Pengaturan",
"nav_button_documentation": "Dokumentasi",
"prefs_users_dialog_button_add": "Tambahkan",
"nav_topics_title": "Topik yang dilanggani",
"nav_button_subscribe": "Berlangganan ke topik",
"alert_grant_title": "Notifikasi dinonaktifkan",
"alert_grant_description": "Berikan izin ke peramban untuk menampilkan notifikasi desktop.",
"alert_not_supported_description": "Notifikasi tidak didukung dalam peramban Anda.",
"notifications_attachment_open_title": "Pergi ke {{url}}",
"notifications_attachment_open_button": "Buka lampiran",
"notifications_attachment_link_expires": "tautan kadaluwarsa {{date}}",
"notifications_attachment_link_expired": "tautan unduhan kadaluwarsa",
"notifications_actions_open_url_title": "Pergi ke {{url}}",
"notifications_click_open_button": "Buka tautan",
"publish_dialog_topic_placeholder": "Nama topik, mis. pemberitahuan_andi",
"nav_button_publish_message": "Publikasikan notifikasi",
"alert_grant_button": "Berikan sekarang",
"notifications_copied_to_clipboard": "Disalin ke papan klip",
"notifications_tags": "Tanda",
"notifications_attachment_copy_url_title": "Salin URL lampiran ke papan klip",
"notifications_attachment_copy_url_button": "Salin URL",
"notifications_none_for_topic_title": "Anda belum menerima notifikasi apa pun untuk topik ini.",
"notifications_none_for_topic_description": "Untuk mengirimkan notifikasi ke topik ini, tinggal PUT atau POST ke URL topik.",
"notifications_none_for_any_description": "Untuk mengirimkan notifikasi ke sebuah topik, tinggal PUT atau POST ke URL topik. Ini adalah contoh menggunakan salah satu topik Anda.",
"notifications_no_subscriptions_title": "Sepertinya Anda belum memiliki langganan apa pun.",
"publish_dialog_title_topic": "Publikasikan ke {{topic}}",
"subscribe_dialog_login_description": "Topik ini dilindungi oleh kata sandi. Mohon masukkan nama pengguna dan kata sandi untuk berlangganan.",
"prefs_notifications_min_priority_title": "Prioritas minimum",
"error_boundary_gathering_info": "Dapatkan info lanjut …",
"publish_dialog_title_no_topic": "Publikasikan notifikasi",
"publish_dialog_progress_uploading": "Mengunggah …",
"notifications_more_details": "Untuk informasi lanjut, lihat <websiteLink>situs web</websiteLink> atau <docsLink>dokumentasi</docsLink>.",
"publish_dialog_progress_uploading_detail": "Mengunggah {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "Notifikasi terpublikasi",
"notifications_loading": "Memuat notifikasi …",
"publish_dialog_base_url_label": "URL Layanan",
"publish_dialog_title_placeholder": "Judul notifikasi, mis. Peringatan ruang disk",
"publish_dialog_tags_label": "Tanda",
"publish_dialog_priority_label": "Prioritas",
"publish_dialog_base_url_placeholder": "URL Layanan, mis. https://contoh.com",
"publish_dialog_attach_placeholder": "Lampirkan file dengan URL, mis. https://f-droid.org/F-Droid.apk",
"publish_dialog_delay_label": "Jeda",
"publish_dialog_chip_topic_label": "Ubah topik",
"publish_dialog_button_cancel_sending": "Batalkan pengiriman",
"publish_dialog_button_send": "Kirim",
"publish_dialog_attachment_limits_file_reached": "melebihi batasan file {{fileSizeLimit}",
"publish_dialog_attachment_limits_file_and_quota_reached": "melebihi batasan file dan kuota {{fileSizeLimit}}, hanya {{remainingBytes}}",
"publish_dialog_attachment_limits_quota_reached": "melebihi kuota, hanya {{remainingBytes}}",
"publish_dialog_priority_min": "Prioritas min.",
"publish_dialog_priority_low": "Prioritas rendah",
"publish_dialog_priority_default": "Prioritas bawaan",
"publish_dialog_priority_high": "Prioritas tinggi",
"publish_dialog_priority_max": "Prioritas maks.",
"publish_dialog_topic_label": "Nama topik",
"publish_dialog_message_placeholder": "Ketik sebuah pesan di sini",
"publish_dialog_click_label": "Klik URL",
"publish_dialog_tags_placeholder": "Daftar tanda yang dipisah dengan koma, mis. peringatan, cadangan-srv1",
"publish_dialog_click_placeholder": "URL yang dibuka ketika notifikasi diklik",
"publish_dialog_email_label": "Email",
"publish_dialog_email_placeholder": "Alamat untuk meneruskan notifikasi, mis. andi@contoh.com",
"publish_dialog_attach_label": "URL Lampiran",
"publish_dialog_filename_label": "Nama File",
"publish_dialog_filename_placeholder": "Nama file lampiran",
"publish_dialog_delay_placeholder": "Penjedaan pengiriman, mis. {{unixTimestamp}}, {{relativeTime}}, atau \"{{naturalLanguage}}\" (hanya Inggris)",
"publish_dialog_other_features": "Fitur lainnya:",
"publish_dialog_chip_click_label": "Klik URL",
"publish_dialog_chip_email_label": "Teruskan ke email",
"publish_dialog_chip_attach_url_label": "Lampirkan file dengan URL",
"publish_dialog_chip_attach_file_label": "Lampirkan file lokal",
"publish_dialog_chip_delay_label": "Jeda pengiriman",
"publish_dialog_button_cancel": "Batal",
"publish_dialog_details_examples_description": "Untuk contoh dan deskripsi yang rinci oleh semua fitur pengiriman, lihat <docsLink>dokumentasi</docsLink>.",
"publish_dialog_checkbox_publish_another": "Publikasi yang lain",
"publish_dialog_attached_file_title": "File yang dilampirkan:",
"publish_dialog_attached_file_filename_placeholder": "Nama file lampiran",
"publish_dialog_drop_file_here": "Lepaskan file di sini",
"emoji_picker_search_placeholder": "Cari emoji",
"subscribe_dialog_subscribe_button_cancel": "Batal",
"subscribe_dialog_subscribe_button_subscribe": "Berlangganan",
"subscribe_dialog_error_user_anonymous": "anonim",
"prefs_notifications_min_priority_any": "Prioritas apa saja",
"prefs_notifications_delete_after_title": "Hapus notifikasi",
"prefs_notifications_delete_after_three_hours": "Setelah tiga jam",
"prefs_notifications_delete_after_one_day": "Setelah satu hari",
"prefs_users_add_button": "Tambahkan pengguna",
"prefs_users_dialog_username_label": "Nama pengguna, mis. andi",
"subscribe_dialog_subscribe_title": "Berlangganan ke topik",
"subscribe_dialog_subscribe_topic_placeholder": "Nama topik, mis. pemberitahuan_andi",
"subscribe_dialog_subscribe_use_another_label": "Gunakan server lain",
"subscribe_dialog_login_username_label": "Nama pengguna, mis. Andi",
"subscribe_dialog_login_button_login": "Masuk",
"subscribe_dialog_error_user_not_authorized": "Pengguna {{username}} tidak diizinkan",
"prefs_notifications_title": "Notifikasi",
"prefs_notifications_sound_no_sound": "Tidak ada suara",
"prefs_users_table_user_header": "Pengguna",
"prefs_users_dialog_base_url_label": "URL Layanan, mis. https://ntfy.sh",
"prefs_users_dialog_button_save": "Simpan",
"prefs_appearance_title": "Tampilan",
"subscribe_dialog_login_password_label": "Kata sandi",
"subscribe_dialog_login_button_back": "Kembali",
"prefs_notifications_sound_title": "Suara notifikasi",
"prefs_notifications_min_priority_low_and_higher": "Prioritas rendah dan lebih tinggi",
"prefs_notifications_min_priority_default_and_higher": "Prioritas bawaan dan lebih tinggi",
"prefs_notifications_min_priority_high_and_higher": "Prioritas tinggi dan lebih tinggi",
"prefs_notifications_min_priority_max_only": "Hanya prioritas maks",
"prefs_notifications_delete_after_never": "Tidak pernah",
"prefs_notifications_delete_after_one_week": "Setelah satu minggu",
"prefs_notifications_delete_after_one_month": "Setelah satu bulan",
"prefs_users_title": "Kelola pengguna",
"prefs_users_description": "Tambahkan/hapus pengguna untuk topik yang dilindungi di sini. Dicatat bahwa nama pengguna dan kata sandi disimpan dalam penyimpanan lokal peramban.",
"prefs_users_table_base_url_header": "URL Layanan",
"prefs_users_dialog_title_add": "Tambahkan pengguna",
"prefs_users_dialog_title_edit": "Edit pengguna",
"prefs_users_dialog_password_label": "Kata sandi",
"prefs_users_dialog_button_cancel": "Batal",
"error_boundary_title": "Aduh, ntfy mogok",
"error_boundary_description": "Seharusnya ini tidak terjadi. Maaf sekali tentang hal ini.<br/>Jika Anda punya beberapa menit, silakan <githubLink>laporkan ini di GitHub</githubLink>, atau beritahu kami melalui <discordLink>Discord</discordLink> atau <matrixLink>Matrix</matrixLink>.",
"error_boundary_stack_trace": "Jejak tumpukan",
"error_boundary_button_copy_stack_trace": "Salin jejak tumpukan",
"prefs_notifications_sound_description_some": "Notifikasi memainkan suara {{sound}} ketika diterima",
"prefs_notifications_min_priority_description_any": "Menampilkan semua notifikasi, apa pun prioritasnya",
"prefs_notifications_min_priority_description_max": "Tampilkan notifikasi jika prioritas adalah 5 (maks)",
"prefs_notifications_delete_after_three_hours_description": "Notifikasi dihapus secara otomatis setelah tiga jam",
"prefs_notifications_delete_after_one_week_description": "Notifikasi dihapus secara otomatis setelah satu minggu",
"prefs_notifications_delete_after_one_month_description": "Notifikasi dihapus secara otomatis setelah satu bulan",
"priority_low": "rendah",
"priority_high": "tinggi",
"priority_max": "maks",
"prefs_notifications_min_priority_description_x_or_higher": "Tampilkan notifikasi jika prioritas {{number}} ({{name}}) atau lebih",
"prefs_notifications_sound_description_none": "Notifikasi tidak boleh memainkan suara apa pun ketika diterima",
"prefs_notifications_delete_after_never_description": "Notifikasi tidak pernah dihapus secara otomatis",
"prefs_notifications_delete_after_one_day_description": "Notifikasi dihapus secara otomatis setelah satu hari",
"priority_default": "bawaan",
"priority_min": "min"
}

View File

@@ -0,0 +1,154 @@
{
"message_bar_error_publishing": "通知送信エラー",
"nav_button_all_notifications": "全ての通知",
"nav_button_settings": "設定",
"notifications_click_open_button": "リンクを開く",
"action_bar_send_test_notification": "テスト通知を送信",
"action_bar_clear_notifications": "全ての通知を消去",
"action_bar_unsubscribe": "購読解除",
"nav_button_documentation": "ドキュメント",
"alert_not_supported_description": "通知機能はこのブラウザではサポートされていません。",
"notifications_copied_to_clipboard": "クリップボードにコピーしました",
"notifications_example": "例",
"publish_dialog_title_topic": "{{topic}}に送信",
"publish_dialog_title_no_topic": "通知を送信",
"publish_dialog_progress_uploading": "アップロード中…",
"publish_dialog_progress_uploading_detail": "アップロード中 {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "通知を送信しました",
"publish_dialog_title_label": "タイトル",
"publish_dialog_filename_label": "ファイル名",
"subscribe_dialog_login_description": "このトピックはログインする必要があります。ユーザー名とパスワードを入力してください。",
"subscribe_dialog_login_username_label": "ユーザー名, 例) phil",
"subscribe_dialog_login_password_label": "パスワード",
"subscribe_dialog_login_button_back": "戻る",
"subscribe_dialog_login_button_login": "ログイン",
"prefs_notifications_min_priority_high_and_higher": "優先度高 およびそれ以上",
"prefs_notifications_min_priority_max_only": "優先度最高のみ",
"action_bar_settings": "設定",
"message_bar_type_message": "メッセージを入力してください",
"nav_topics_title": "購読しているトピック",
"nav_button_subscribe": "トピックを購読",
"alert_grant_description": "ブラウザのデスクトップ通知を許可してください。",
"alert_grant_button": "許可する",
"notifications_attachment_link_expires": "リンクは {{date}} に失効します",
"notifications_click_copy_url_button": "リンクをコピー",
"notifications_none_for_topic_description": "トピックに通知を送信するには、トピックのURLにPUTかPOSTしてください。",
"nav_button_publish_message": "通知を送信",
"alert_grant_title": "通知は無効化されています",
"alert_not_supported_title": "通知機能はサポートされていません",
"notifications_tags": "タグ",
"notifications_attachment_copy_url_button": "URLをコピー",
"notifications_attachment_open_title": "{{url}} に移動",
"notifications_attachment_link_expired": "ダウンロードリンクは失効しました",
"notifications_actions_open_url_title": "{{url}} に移動",
"notifications_attachment_copy_url_title": "添付URLをクリップボードにコピー",
"notifications_attachment_open_button": "添付ファイルを開く",
"notifications_click_copy_url_title": "リンクURLをクリップボードにコピー",
"notifications_none_for_topic_title": "このトピックではまだ通知を受信していません。",
"notifications_no_subscriptions_description": "「{{linktext}}」リンクをクリックしてトピックを作成または購読してください。その後、メッセージをPUTまたはPOSTで送信すると通知が受信できます。",
"publish_dialog_message_label": "メッセージ",
"publish_dialog_email_label": "メール",
"notifications_none_for_any_title": "まだ通知を受信していません。",
"publish_dialog_priority_max": "優先度最高",
"publish_dialog_button_cancel_sending": "送信をキャンセル",
"publish_dialog_attach_label": "添付URL",
"notifications_none_for_any_description": "トピックに通知を送信するには、トピックURLにPUTまたはPOSTしてください。トピックのひとつを利用した例を示します。",
"notifications_no_subscriptions_title": "まだ何も購読していないようです。",
"publish_dialog_attachment_limits_file_and_quota_reached": "ファイル制限とクォータ {{fileSizeLimit}} を超えました、残り {{remainingBytes}}",
"publish_dialog_priority_label": "優先度",
"publish_dialog_click_label": "クリックURL",
"publish_dialog_email_placeholder": "通知を転送するアドレス, 例) phil@example.com",
"notifications_more_details": "詳しい情報は、<websiteLink>ウェブサイト</websiteLink> または <docsLink>ドキュメント</docsLink> を参照してください。",
"publish_dialog_attachment_limits_file_reached": "ファイルサイズ制限 {{fileSizeLimit}} を超えました",
"publish_dialog_priority_min": "優先度最低",
"publish_dialog_priority_low": "優先度低",
"publish_dialog_priority_default": "優先度通常",
"publish_dialog_base_url_label": "サービスURL",
"publish_dialog_other_features": "他の機能:",
"notifications_loading": "通知を読み込み中…",
"publish_dialog_attachment_limits_quota_reached": "クォータを超過しました、残り{{remainingBytes}}",
"publish_dialog_priority_high": "優先度高",
"publish_dialog_topic_placeholder": "トピック名の例 phil_alerts",
"publish_dialog_title_placeholder": "通知タイトル 例: ディスクスペース警告",
"publish_dialog_message_placeholder": "メッセージ本文を入力してください",
"publish_dialog_tags_label": "タグ",
"publish_dialog_tags_placeholder": "コンマ区切りでタグを列挙してください 例: warning, srv1-backup",
"publish_dialog_topic_label": "トピック名",
"publish_dialog_delay_label": "遅延",
"publish_dialog_click_placeholder": "通知をクリックしたときに開くURL",
"publish_dialog_filename_placeholder": "添付ファイルの名称",
"publish_dialog_button_send": "送信",
"publish_dialog_chip_click_label": "Click URL",
"publish_dialog_chip_email_label": "メールに転送",
"publish_dialog_details_examples_description": "送信機能の例や詳細な説明については、<docsLink>ドキュメント</docsLink>を参照してください。",
"error_boundary_description": "明らかに起きてはならないことです。本当に申し訳ありません。<br/>もし時間があれば、<githubLink>GitHubにこれを報告</githubLink>するか、<discordLink>Discord</discordLink>または<matrixLink>Matrix</matrixLink>で我々に知らせて下さい。",
"publish_dialog_chip_attach_url_label": "URLでファイルを添付",
"publish_dialog_chip_attach_file_label": "ローカルファイルを添付",
"publish_dialog_chip_topic_label": "トピックを変更",
"publish_dialog_chip_delay_label": "配信を遅延させる",
"publish_dialog_attached_file_title": "添付ファイル:",
"publish_dialog_button_cancel": "キャンセル",
"publish_dialog_checkbox_publish_another": "送信後開いたままにする",
"publish_dialog_attached_file_filename_placeholder": "添付ファイル名",
"emoji_picker_search_placeholder": "絵文字を検索",
"subscribe_dialog_subscribe_title": "トピックを購読",
"prefs_users_title": "ユーザー管理",
"publish_dialog_drop_file_here": "ここにファイルをドロップしてください",
"subscribe_dialog_subscribe_topic_placeholder": "トピック名 例: phils_alerts",
"prefs_notifications_min_priority_any": "全ての優先度",
"prefs_notifications_delete_after_three_hours": "3時間後",
"prefs_users_description": "保護トピックのユーザーを追加/削除できます。ユーザー名とパスワードはブラウザのローカルストレージに保存されることに留意してください。",
"prefs_users_add_button": "ユーザー追加",
"prefs_users_dialog_button_add": "追加",
"subscribe_dialog_subscribe_use_another_label": "他のサーバーを使用",
"subscribe_dialog_error_user_not_authorized": "ユーザー名 {{username}} は許可されていません",
"prefs_notifications_delete_after_one_week": "1週間後",
"prefs_notifications_delete_after_one_month": "1か月後",
"subscribe_dialog_subscribe_description": "トピックはパスワード保護されないので、推測されにくい名前にしてください。購読した後、PUT/POSTで通知を送信できます。",
"subscribe_dialog_subscribe_button_cancel": "キャンセル",
"subscribe_dialog_subscribe_button_subscribe": "購読",
"subscribe_dialog_login_title": "ログインが必要です",
"subscribe_dialog_error_user_anonymous": "匿名",
"prefs_notifications_title": "通知",
"prefs_notifications_min_priority_low_and_higher": "優先度低 およびそれ以上",
"prefs_notifications_delete_after_never": "削除しない",
"prefs_notifications_delete_after_one_day": "1日後",
"prefs_notifications_sound_title": "通知音",
"prefs_notifications_sound_no_sound": "サウンドなし",
"prefs_notifications_min_priority_title": "表示する優先度",
"prefs_notifications_min_priority_default_and_higher": "優先度通常 およびそれ以上",
"prefs_notifications_delete_after_title": "通知を削除",
"prefs_users_dialog_button_cancel": "キャンセル",
"prefs_users_dialog_button_save": "保存",
"prefs_users_table_user_header": "ユーザー名",
"prefs_users_dialog_title_add": "ユーザー追加",
"prefs_users_dialog_title_edit": "ユーザー編集",
"prefs_users_dialog_base_url_label": "サービスURL, 例) https://ntfy.sh",
"prefs_appearance_title": "外観",
"prefs_appearance_language_title": "言語",
"prefs_users_table_base_url_header": "サービスURL",
"prefs_users_dialog_username_label": "ユーザー名, 例) phil",
"prefs_users_dialog_password_label": "パスワード",
"error_boundary_title": "ああ、ntfyがクラッシュしました",
"error_boundary_button_copy_stack_trace": "スタックトレースをコピー",
"error_boundary_stack_trace": "スタックトレース",
"error_boundary_gathering_info": "更に情報を集める…",
"publish_dialog_base_url_placeholder": "サービスURL, 例) https://example.com",
"publish_dialog_attach_placeholder": "添付ファイルをURLで指定, 例) https://f-droid.org/F-Droid.apk",
"publish_dialog_delay_placeholder": "遅延時間, 例) {{unixTimestamp}}, {{relativeTime}}, or \"{{naturalLanguage}}\" (English only)",
"prefs_notifications_sound_description_none": "通知受信時に音を鳴らしません",
"prefs_notifications_sound_description_some": "通知受信時に {{sound}} を鳴らします",
"prefs_notifications_min_priority_description_any": "優先度に関係なく全ての通知を表示します",
"prefs_notifications_min_priority_description_x_or_higher": "優先度が {{number}} ({{name}}) 以上の時に通知を表示します",
"prefs_notifications_delete_after_never_description": "通知は自動的に削除されません",
"prefs_notifications_delete_after_one_day_description": "通知は1日後に自動的に削除されます",
"prefs_notifications_delete_after_one_week_description": "通知は1週間後に自動的に削除されます",
"prefs_notifications_delete_after_one_month_description": "通知は1か月後に自動的に削除されます",
"priority_high": "高",
"priority_max": "最高",
"prefs_notifications_min_priority_description_max": "優先度が 5 (最高) の時にのみ通知を表示します",
"priority_default": "通常",
"prefs_notifications_delete_after_three_hours_description": "通知は3時間後に自動的に削除されます",
"priority_low": "低",
"priority_min": "最低"
}

View File

@@ -0,0 +1,126 @@
{
"nav_button_subscribe": "Abonner på emne",
"action_bar_settings": "Innstillinger",
"action_bar_send_test_notification": "Send testmerknad",
"action_bar_clear_notifications": "Tøm alle merknader",
"action_bar_unsubscribe": "Opphev abonnement",
"message_bar_type_message": "Skriv en melding her",
"nav_button_all_notifications": "Alle merknader",
"nav_button_settings": "Innstillinger",
"nav_button_documentation": "Dokumentasjon",
"nav_topics_title": "Abonnerte emner",
"alert_grant_title": "Merknader er avskrudd",
"alert_not_supported_title": "Merknader støttes ikke",
"notifications_copied_to_clipboard": "Kopiert til utklippstavlen",
"notifications_attachment_copy_url_title": "Kopier vedleggsnettadresse til utklippstavlen",
"notifications_attachment_copy_url_button": "Kopier nettadresse",
"notifications_attachment_open_button": "Åpne vedlegg",
"notifications_attachment_open_title": "Gå til {{url}}",
"notifications_attachment_link_expires": "lenken utløper {{date}}",
"notifications_click_copy_url_title": "Kopier lenke-nettadresse til utklippstavlen",
"notifications_actions_open_url_title": "Gå til {{url}}",
"notifications_tags": "Etiketter",
"notifications_attachment_link_expired": "nedlastingslenken har utløpt",
"notifications_none_for_any_title": "Du har ikke mottatt noen merknader.",
"notifications_click_open_button": "Åpne lenke",
"notifications_none_for_topic_title": "Du har ikke mottatt noen merknader for dette emnet enda.",
"notifications_example": "Eksempel",
"publish_dialog_title_topic": "Publiser til {{topic}}",
"publish_dialog_priority_min": "Min. prioritet",
"publish_dialog_priority_low": "Lav prioritet",
"publish_dialog_priority_default": "Forvalgt prioritet",
"publish_dialog_priority_high": "Høy prioritet",
"publish_dialog_priority_max": "Maks. prioritet",
"publish_dialog_base_url_label": "Tjeneste-nettadresse",
"publish_dialog_message_label": "Melding",
"publish_dialog_priority_label": "Prioritet",
"publish_dialog_tags_label": "Etiketter",
"publish_dialog_click_placeholder": "Nettadresse som åpnes når merknaden klikkes",
"publish_dialog_attach_label": "Vedleggs-nettadresse",
"publish_dialog_attach_placeholder": "Legg ved fil per nettadresse, f.eks. https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_label": "Filnavn",
"publish_dialog_delay_label": "Forsinkelse",
"publish_dialog_filename_placeholder": "Vedleggets filnavn",
"publish_dialog_other_features": "Andre funksjoner:",
"publish_dialog_chip_email_label": "Videresend til e-post",
"publish_dialog_chip_topic_label": "Endre emne",
"publish_dialog_button_cancel_sending": "Avbryt forsendelse",
"publish_dialog_chip_attach_file_label": "Legg ved lokal fil",
"publish_dialog_attached_file_title": "Vedlagt fil:",
"publish_dialog_attached_file_filename_placeholder": "Vedleggsfilnavn",
"subscribe_dialog_subscribe_use_another_label": "Bruk en annen tjener",
"subscribe_dialog_subscribe_button_cancel": "Avbryt",
"publish_dialog_drop_file_here": "Slipp filen her",
"subscribe_dialog_subscribe_title": "Abonner på emne",
"emoji_picker_search_placeholder": "Søk etter emoji",
"subscribe_dialog_login_button_login": "Logg inn",
"subscribe_dialog_subscribe_button_subscribe": "Abonner",
"subscribe_dialog_login_title": "Innlogging kreves",
"subscribe_dialog_login_username_label": "Brukernavn, f.eks. phil",
"subscribe_dialog_login_password_label": "Passord",
"prefs_notifications_title": "Merknader",
"prefs_notifications_sound_title": "Merknadslyd",
"prefs_notifications_sound_no_sound": "Ingen lyd",
"subscribe_dialog_error_user_anonymous": "anonym",
"error_boundary_stack_trace": "Stabelspor",
"error_boundary_button_copy_stack_trace": "Kopier stabelspor",
"message_bar_error_publishing": "Kunne ikke publisere merknader",
"nav_button_publish_message": "Publiser merknad",
"publish_dialog_title_no_topic": "Publiser merknad",
"publish_dialog_progress_uploading": "Laster opp …",
"publish_dialog_progress_uploading_detail": "Laster opp {{loaded}}/{{total}} ({{percent}}%) …",
"notifications_loading": "Laster inn merknader …",
"publish_dialog_message_published": "Merknad publisert",
"publish_dialog_email_placeholder": "Adresse å videresende merknaden til, f.eks. phil@example.com",
"error_boundary_gathering_info": "Hent mer info …",
"prefs_notifications_sound_description_some": "Merknader spiller {{sound}}-lyd når de mottas",
"prefs_notifications_min_priority_description_any": "Viser aller merknader, uavhengig av prioritet",
"prefs_notifications_min_priority_description_x_or_higher": "Vis merknader hvis prioritet er {{number}} ({{name}}) eller høyere",
"prefs_notifications_min_priority_high_and_higher": "Høy prioritet og høyere",
"prefs_notifications_min_priority_max_only": "Kun maks. prioritet",
"prefs_notifications_delete_after_one_day": "Etter én dag",
"prefs_notifications_delete_after_one_week": "Etter én uke",
"prefs_notifications_delete_after_one_month": "Etter én måned",
"prefs_notifications_delete_after_never_description": "Merknader blir aldri slettet automatisk",
"prefs_notifications_delete_after_three_hours_description": "Merknader slettes automatisk etter tre timer",
"prefs_users_title": "Håndter brukere",
"prefs_users_add_button": "Legg til bruker",
"prefs_users_table_user_header": "Bruker",
"prefs_users_dialog_title_add": "Legg til bruker",
"prefs_users_dialog_title_edit": "Rediger bruker",
"prefs_users_dialog_base_url_label": "Tjeneste-nettadresse, f.eks. https://ntfy.sh",
"prefs_users_dialog_password_label": "Passord",
"prefs_users_dialog_button_save": "Lagre",
"prefs_appearance_title": "Utseende",
"prefs_appearance_language_title": "Språk",
"prefs_users_dialog_username_label": "Brukernavn, f.eks. phil",
"priority_low": "lav",
"priority_default": "forvalg",
"priority_high": "høy",
"priority_max": "maks.",
"alert_grant_button": "Innvilg nå",
"publish_dialog_topic_label": "Emnenavn",
"prefs_notifications_delete_after_one_day_description": "Merknader slettes automatisk etter én dag",
"notifications_click_copy_url_button": "Kopier lenke",
"error_boundary_title": "Oida. Ntfy krasjet.",
"publish_dialog_message_placeholder": "Skriv en melding her",
"publish_dialog_button_cancel": "Avbryt",
"prefs_notifications_min_priority_title": "Minimumsprioritet",
"prefs_notifications_delete_after_title": "Slett merknader",
"prefs_notifications_delete_after_never": "Aldri",
"publish_dialog_email_label": "E-post",
"publish_dialog_button_send": "Send",
"prefs_notifications_delete_after_one_week_description": "Merknader slettes automatisk etter én uke",
"prefs_notifications_delete_after_one_month_description": "Merknader slettes automatisk etter én måned",
"priority_min": "min.",
"subscribe_dialog_login_button_back": "Tilbake",
"prefs_notifications_delete_after_three_hours": "Etter tre timer",
"prefs_users_table_base_url_header": "Tjeneste-nettadresse",
"prefs_users_dialog_button_cancel": "Avbryt",
"prefs_users_dialog_button_add": "Legg til",
"publish_dialog_chip_attach_url_label": "Legg ved fil per nettadresse",
"publish_dialog_tags_placeholder": "Kommainndelt liste over etiketter, f.eks. advarsel, srv1-sikkerhetskopi",
"prefs_notifications_sound_description_none": "Merknader er lydløse når de mottas",
"subscribe_dialog_subscribe_topic_placeholder": "Emnenavn, f.eks. phil_varsler",
"prefs_notifications_min_priority_default_and_higher": "Forvalgt prioritet og høyere"
}

View File

@@ -0,0 +1,38 @@
{
"notifications_attachment_open_button": "Abrir anexo",
"action_bar_clear_notifications": "Limpar todas as notificações",
"action_bar_unsubscribe": "Desinscrever",
"message_bar_type_message": "Escreva uma mensagem aqui",
"message_bar_error_publishing": "Erro ao publicar notificação",
"nav_button_all_notifications": "Todas notificações",
"nav_button_settings": "Configurações",
"nav_button_subscribe": "Inscrever no tópico",
"alert_grant_title": "Notificações estão desativadas",
"alert_grant_description": "Conceder ao navegador permissão para mostrar notificações.",
"alert_grant_button": "Conceder agora",
"alert_not_supported_title": "Notificações não são suportadas",
"alert_not_supported_description": "Notificações não são suportadas pelo seu navagador.",
"notifications_copied_to_clipboard": "Copiado para a área de transferência",
"notifications_tags": "Etiquetas",
"notifications_attachment_copy_url_title": "Copiar URL do anexo para a área de transferência",
"notifications_click_copy_url_title": "Copiar URL do link para a área de transferência",
"notifications_click_copy_url_button": "Copiar link",
"notifications_click_open_title": "Ir para {{url}}",
"notifications_click_open_button": "Abrir link",
"notifications_none_for_topic_title": "Você ainda não recebeu nenhuma notificação para esse tópico.",
"notifications_none_for_topic_description": "Para enviar notificações para esse tópico, basta usar os métodos PUT ou POST na URL do tópico.",
"notifications_none_for_any_title": "Você ainda não recebeu nenhuma notificação.",
"notifications_none_for_any_description": "Para enviar notificações a um tópico, basta usar os métodos PUT ou POST para o URL do tópico. Aqui um exemplo usando um dos seus tópicos.",
"notifications_no_subscriptions_title": "Parece que você não tem nenhuma inscrição ainda.",
"notifications_no_subscriptions_description": "Clique no link \"{{linktext}}\" para criar ou inscrever em um tópico. Depois disso, poderá enviar mensagens via PUT ou POST e você receberá notificações aqui.",
"action_bar_settings": "Configurações",
"action_bar_send_test_notification": "Enviar notificação de teste",
"nav_button_documentation": "Documentação",
"nav_button_publish_message": "Publicar notificação",
"nav_topics_title": "Tópicos inscritos",
"notifications_attachment_open_title": "Ir para {{url}}",
"notifications_attachment_link_expires": "link expira em {{date}}",
"notifications_attachment_copy_url_button": "Copiar URL",
"notifications_attachment_link_expired": "link para transferência expirado",
"notifications_example": "Exemplo"
}

View File

@@ -0,0 +1,154 @@
{
"publish_dialog_priority_min": "Мин. приоритет",
"action_bar_settings": "Настройки",
"action_bar_send_test_notification": "Отправить тестовое уведомление",
"action_bar_clear_notifications": "Удалить все уведомления",
"action_bar_unsubscribe": "Отписаться",
"message_bar_type_message": "Введите сообщение здесь",
"notifications_none_for_topic_description": "Чтобы отправить уведомление на данную тему, просто отправьте PUT или POST на URL-адрес этой темы.",
"notifications_none_for_any_description": "Чтобы отправить уведомления на тему, просто отправьте PUT или POST на URL-адрес темы. Вот пример используя одну из ваших тем.",
"notifications_no_subscriptions_title": "Похоже у вас ещё нет подписок.",
"alert_grant_description": "Разрешите браузеру показывать уведомления.",
"notifications_no_subscriptions_description": "Нажмите \"{{linktext}}\" ссылку, чтобы создать или подписаться на тему. После этого вы сможете отправлять сообщения используя PUT или POST, и вы будете получать здесь уведомления.",
"notifications_example": "Пример",
"notifications_more_details": "Дополнительную информацию найдёте на <websiteLink>сайте</websiteLink> или в <docsLink>документации</docsLink>.",
"notifications_loading": "Загружаются уведомления …",
"publish_dialog_title_topic": "Опубликовать в {{topic}}",
"publish_dialog_title_no_topic": "Опубликовать уведомление",
"publish_dialog_progress_uploading": "Загружается …",
"publish_dialog_progress_uploading_detail": "Загружается {{loaded}}/{{total}} ({{percent}}%) …",
"publish_dialog_message_published": "Уведомление опубликовано",
"publish_dialog_attachment_limits_file_and_quota_reached": "превышает {{fileSizeLimit}} размер файла, {{remainingBytes}} осталось",
"publish_dialog_attachment_limits_file_reached": "превышает {{fileSizeLimit}} размер файла",
"publish_dialog_attachment_limits_quota_reached": "превышает квоту, {{remainingBytes}} осталось",
"publish_dialog_priority_low": "Низкий приоритет",
"publish_dialog_priority_default": "Приоритет по умолчанию",
"publish_dialog_priority_high": "Высокий приоритет",
"publish_dialog_priority_max": "Макс. приоритет",
"publish_dialog_base_url_label": "URL-адрес сервиса",
"publish_dialog_base_url_placeholder": "URL-адрес сервиса, например https://example.com",
"publish_dialog_topic_label": "Название темы",
"publish_dialog_topic_placeholder": "Название темы, например phil_alerts",
"publish_dialog_title_label": "Заголовок",
"publish_dialog_title_placeholder": "Заголовок уведомления, например Disk space alert",
"publish_dialog_message_label": "Сообщение",
"publish_dialog_message_placeholder": "Текст сообщения",
"publish_dialog_tags_label": "Тэги",
"publish_dialog_tags_placeholder": "Список тэгов, разделённый запятой, например warning, srv1-backup",
"publish_dialog_priority_label": "Приоритет",
"publish_dialog_click_label": "Нажмите на URL-адрес",
"publish_dialog_click_placeholder": "URL-адрес который откроется когда будет нажато уведомление",
"publish_dialog_email_label": "Эл. почта",
"message_bar_error_publishing": "Ошибка отправки уведомления",
"alert_not_supported_title": "Уведомления не поддерживаются",
"alert_not_supported_description": "Уведомления не поддерживаются вашим браузером.",
"notifications_copied_to_clipboard": "Скопировано в буфер обмена",
"notifications_attachment_open_button": "Открыть вложение",
"notifications_none_for_topic_title": "Вы ещё не получали уведомления для этой темы.",
"nav_topics_title": "Подписки на темы",
"nav_button_all_notifications": "Все уведомления",
"nav_button_settings": "Настройки",
"nav_button_documentation": "Документация",
"nav_button_publish_message": "Опубликовать уведомление",
"nav_button_subscribe": "Подписаться на тему",
"alert_grant_button": "Разрешить",
"notifications_attachment_copy_url_button": "Скопировать URL-адрес",
"notifications_attachment_open_title": "Перейти на {{url}}",
"notifications_attachment_link_expired": "срок действия ссылки для скачивания истёк",
"notifications_click_copy_url_button": "Скопировать ссылку",
"notifications_none_for_any_title": "Вы ещё не получали никаких уведомлений.",
"alert_grant_title": "Уведомления отключены",
"notifications_attachment_copy_url_title": "Скопировать URL-адрес вложения",
"notifications_actions_open_url_title": "Перейти на {{url}}",
"notifications_tags": "Тэги",
"notifications_attachment_link_expires": "срок действия ссылки истекает {{date}}",
"notifications_click_copy_url_title": "Скопировать URL-адрес ссылки",
"notifications_click_open_button": "Открыть ссылку",
"subscribe_dialog_subscribe_title": "Подписаться на тему",
"publish_dialog_button_cancel": "Отмена",
"subscribe_dialog_subscribe_description": "Темы могут быть не защищены паролем, поэтому укажите сложное имя. После подписки вы можете размещать/отправлять уведомления.",
"prefs_users_description": "Добавляйте/удаляйте пользователей для защищенных тем. Обратите внимание, что имя пользователя и пароль хранятся в локальном хранилище браузера.",
"error_boundary_description": "Этого, очевидно, не должно происходить. Очень сожалею об этом. <br/>Если у вас есть минутка, пожалуйста <githubLink>сообщить об этом на GitHub</githubLink>, или сообщите нам через <discordLink>Discord</discordLink> или <matrixLink>Matrix</matrixLink>.",
"publish_dialog_email_placeholder": "Адрес для пересылки уведомления. Например, phil@example.com",
"publish_dialog_attach_placeholder": "Прикрепите файл по URL. Например, https://f-droid.org/F-Droid.apk",
"publish_dialog_filename_label": "Имя файла",
"publish_dialog_delay_label": "Задержка",
"publish_dialog_delay_placeholder": "Задержка доставки. Например, {{unixTimestamp}}, {{relativeTime}}, or \"{{naturalLanguage}}\" (English only)",
"publish_dialog_chip_click_label": "Адрес",
"publish_dialog_chip_email_label": "Переслать на электронную почту",
"publish_dialog_chip_attach_url_label": "Прикрепить файл по URL",
"publish_dialog_chip_attach_file_label": "Прикрепить локальный файл",
"publish_dialog_chip_delay_label": "Задержка отправки",
"publish_dialog_chip_topic_label": "Изменить тему",
"publish_dialog_details_examples_description": "Примеры и подробное описание всех функций см. в e <docsLink>документации</docsLink>.",
"publish_dialog_attach_label": "URL-адрес вложения",
"publish_dialog_filename_placeholder": "Имя файла вложения",
"publish_dialog_other_features": "Другие возможности:",
"publish_dialog_button_cancel_sending": "Отменить отправку",
"publish_dialog_button_send": "Отправить",
"publish_dialog_checkbox_publish_another": "Опубликовать еще",
"publish_dialog_attached_file_title": "Прикрепленный файл:",
"publish_dialog_attached_file_filename_placeholder": "Имя прикреплённого файла",
"emoji_picker_search_placeholder": "Поиск эмодзи",
"subscribe_dialog_subscribe_topic_placeholder": "Название темы. Например, phil_alerts",
"subscribe_dialog_subscribe_use_another_label": "Использовать другой сервер",
"subscribe_dialog_subscribe_button_cancel": "Отмена",
"subscribe_dialog_subscribe_button_subscribe": "Подписаться",
"subscribe_dialog_login_title": "Требуется авторизация",
"subscribe_dialog_login_description": "Эта тема защищена паролем. Пожалуйста, введите имя пользователя и пароль, чтобы подписаться.",
"subscribe_dialog_login_username_label": "Имя пользователя. Например, phil",
"subscribe_dialog_login_password_label": "Пароль",
"subscribe_dialog_login_button_back": "Назад",
"subscribe_dialog_login_button_login": "Войти",
"subscribe_dialog_error_user_not_authorized": "Пользователь {{username}} не авторизован",
"subscribe_dialog_error_user_anonymous": "аноним",
"prefs_notifications_title": "Уведомления",
"prefs_notifications_sound_title": "Звук уведомления",
"prefs_notifications_sound_description_none": "Уведомления не воспроизводят никаких звуков при получении",
"prefs_notifications_sound_no_sound": "Без звука",
"prefs_notifications_min_priority_title": "Минимальный приоритет",
"prefs_notifications_min_priority_description_any": "Показать все уведомления, независимо от приоритета",
"prefs_notifications_min_priority_description_x_or_higher": "Показывать уведомления, если приоритет {{number}} ({{name}}) или выше",
"prefs_notifications_min_priority_description_max": "Показывать уведомления, если приоритет равен 5 (максимум)",
"prefs_notifications_min_priority_any": "Любой приоритет",
"prefs_notifications_min_priority_low_and_higher": "Низкий и высокий приоритет",
"prefs_notifications_min_priority_max_only": "Только максимальный приоритет",
"prefs_notifications_delete_after_title": "Удалить уведомления",
"prefs_notifications_delete_after_never": "Никогда",
"prefs_notifications_delete_after_three_hours": "Через три часа",
"prefs_notifications_sound_description_some": "Уведомления воспроизводят звук {{sound}}",
"prefs_notifications_min_priority_default_and_higher": "Приоритет по умолчанию и высокий",
"prefs_notifications_delete_after_one_day": "Через день",
"prefs_notifications_delete_after_one_week": "Через неделю",
"prefs_notifications_delete_after_one_month": "Через месяц",
"prefs_notifications_delete_after_never_description": "Уведомления никогда не удаляются автоматически",
"prefs_notifications_delete_after_three_hours_description": "Уведомления автоматически удаляются через три часа",
"prefs_notifications_delete_after_one_day_description": "Уведомления автоматически удаляются через один день",
"prefs_notifications_delete_after_one_week_description": "Уведомления автоматически удаляются через неделю",
"prefs_notifications_delete_after_one_month_description": "Уведомления автоматически удаляются через месяц",
"prefs_users_title": "Управление пользователями",
"prefs_users_add_button": "Добавить пользователя",
"prefs_users_table_user_header": "Пользователь",
"prefs_users_table_base_url_header": "URL службы",
"prefs_users_dialog_title_add": "Добавить пользователя",
"prefs_users_dialog_title_edit": "Редактировать пользователя",
"prefs_users_dialog_base_url_label": "URL-адрес службы. Например, https://ntfy.sh",
"prefs_users_dialog_username_label": "Имя пользователя. Например, phil",
"prefs_users_dialog_password_label": "Пароль",
"prefs_users_dialog_button_cancel": "Отмена",
"prefs_users_dialog_button_add": "Добавить",
"prefs_users_dialog_button_save": "Сохранить",
"prefs_appearance_title": "Внешний вид",
"prefs_appearance_language_title": "Язык",
"priority_min": "минимум",
"priority_low": "низкий",
"priority_default": "по умолчанию",
"priority_high": "высокий",
"priority_max": "максимальный",
"error_boundary_title": "О нет, Ntfy сломался",
"error_boundary_button_copy_stack_trace": "Копирование трассировки стека",
"error_boundary_stack_trace": "Трассировка стека",
"error_boundary_gathering_info": "Соберите больше информации …",
"publish_dialog_drop_file_here": "Перетащите файл юда",
"prefs_notifications_min_priority_high_and_higher": "Высокий приоритет и выше"
}

View File

@@ -0,0 +1,154 @@
{
"nav_button_subscribe": "Konuya abone ol",
"nav_button_settings": "Ayarlar",
"action_bar_send_test_notification": "Test bildirimi gönder",
"message_bar_type_message": "Buraya bir mesaj yazın",
"action_bar_clear_notifications": "Tüm bildirimleri temizle",
"action_bar_unsubscribe": "Abonelikten çık",
"action_bar_settings": "Ayarlar",
"message_bar_error_publishing": "Bildirim yayınlanırken hata oluştu",
"nav_topics_title": "Abone olunan konular",
"nav_button_all_notifications": "Tüm bildirimler",
"publish_dialog_tags_placeholder": "Virgülle ayrılmış etiket listesi, örn. uyarı, srv1-yedekleme",
"publish_dialog_priority_label": "Öncelik",
"publish_dialog_click_label": "Tıklama URL'si",
"publish_dialog_click_placeholder": "Bildirim tıklandığında açılan URL",
"publish_dialog_email_label": "E-posta adresi",
"publish_dialog_email_placeholder": "Bildirimin iletileceği adres, örn. phil@example.com",
"publish_dialog_attach_label": "Ek URL'si",
"publish_dialog_filename_label": "Dosya adı",
"publish_dialog_filename_placeholder": "Ek dosya adı",
"publish_dialog_delay_label": "Gecikme",
"publish_dialog_button_cancel": "İptal",
"publish_dialog_button_send": "Gönder",
"publish_dialog_checkbox_publish_another": "Başka bir tane yayınla",
"publish_dialog_attached_file_title": "Ekli dosya:",
"publish_dialog_attached_file_filename_placeholder": "Ek dosya adı",
"subscribe_dialog_subscribe_title": "Konuya abone ol",
"subscribe_dialog_subscribe_description": "Konular parola korumalı olmayabilir, bu nedenle tahmin edilmesi kolay olmayan bir ad seçin. Abone olduktan sonra PUT/POST bildirimleri yapabilirsiniz.",
"subscribe_dialog_subscribe_topic_placeholder": "Konu adı, örn. benim_uyarilarim",
"subscribe_dialog_subscribe_use_another_label": "Başka bir sunucu kullan",
"subscribe_dialog_subscribe_button_cancel": "İptal",
"subscribe_dialog_subscribe_button_subscribe": "Abone ol",
"subscribe_dialog_login_title": "Oturum açma gerekli",
"subscribe_dialog_login_description": "Bu konu parola korumalı. Abone olmak için lütfen kullanıcı adı ve parola girin.",
"subscribe_dialog_login_username_label": "Kullanıcı adı, örn. phil",
"subscribe_dialog_login_password_label": "Parola",
"subscribe_dialog_login_button_back": "Geri",
"subscribe_dialog_login_button_login": "Oturum aç",
"subscribe_dialog_error_user_not_authorized": "{{username}} kullanıcısı yetkili değil",
"subscribe_dialog_error_user_anonymous": "anonim",
"prefs_notifications_title": "Bildirimler",
"prefs_notifications_sound_title": "Bildirim sesi",
"prefs_notifications_sound_no_sound": "Ses yok",
"prefs_notifications_min_priority_title": "En düşük öncelik",
"prefs_notifications_min_priority_any": "Herhangi bir öncelik",
"publish_dialog_topic_placeholder": "Konu adı, örn. benim_uyarilarim",
"alert_grant_button": "Şimdi ver",
"alert_not_supported_title": "Bildirimler desteklenmiyor",
"notifications_attachment_link_expires": "bağlantının süresi {{date}} tarihinde doluyor",
"notifications_click_copy_url_title": "Bağlantı URL'sini panoya kopyala",
"notifications_loading": "Bildirimler yükleniyor…",
"publish_dialog_progress_uploading": "Karşıya yükleniyor…",
"publish_dialog_attachment_limits_file_reached": "{{fileSizeLimit}} dosya sınırınııyor",
"publish_dialog_priority_default": "Öntanımlı öncelik",
"publish_dialog_chip_click_label": "Tıklama URL'si",
"publish_dialog_attach_placeholder": "URL ile dosya ekle, örn. https://f-droid.org/F-Droid.apk",
"prefs_notifications_delete_after_never": "Hiçbir zaman",
"notifications_attachment_copy_url_button": "URL'yi kopyala",
"notifications_attachment_open_button": "Eki aç",
"nav_button_documentation": "Belgelendirme",
"nav_button_publish_message": "Bildirim yayınla",
"alert_grant_title": "Bildirimler devre dışı",
"alert_grant_description": "Tarayıcınıza masaüstü bildirimlerini görüntüleme izni verin.",
"alert_not_supported_description": "Tarayıcınızda bildirimler desteklenmiyor.",
"notifications_copied_to_clipboard": "Panoya kopyalandı",
"notifications_tags": "Etiketler",
"notifications_attachment_copy_url_title": "Ek URL'sini panoya kopyala",
"notifications_attachment_open_title": "{{url}} adresine git",
"notifications_none_for_topic_title": "Bu konu için henüz herhangi bir bildirim almadınız.",
"notifications_none_for_topic_description": "Bu konuya bildirim göndermek için konu URL'sine PUT veya POST göndermeniz yeterlidir.",
"notifications_none_for_any_title": "Herhangi bir bildirim almadınız.",
"notifications_attachment_link_expired": "indirme bağlantısının süresi doldu",
"notifications_click_copy_url_button": "Bağlantıyı kopyala",
"notifications_actions_open_url_title": "{{url}} adresine git",
"notifications_click_open_button": "Bağlantıyı aç",
"notifications_no_subscriptions_description": "Bir konu oluşturmak veya bir konuya abone olmak için \"{{linktext}}\" bağlantısına tıklayın. Bundan sonra PUT veya POST yoluyla mesaj gönderebilirsiniz ve buradan bildirimler alırsınız.",
"notifications_example": "Örnek",
"notifications_more_details": "Daha fazla bilgi için <websiteLink>web sitesine</websiteLink> veya <docsLink>belgelendirmeye</docsLink> bakın.",
"publish_dialog_chip_attach_url_label": "URL ile dosya ekle",
"prefs_notifications_min_priority_default_and_higher": "Öntanımlı öncelik ve üstü",
"prefs_notifications_delete_after_three_hours": "Üç saat sonra",
"notifications_none_for_any_description": "Bir konuya bildirim göndermek için konu URL'sine PUT veya POST göndermeniz yeterlidir. İşte konularınızdan birini kullanan bir örnek.",
"notifications_no_subscriptions_title": "Henüz aboneliğiniz yok gibi görünüyor.",
"publish_dialog_title_topic": "{{topic}} konusuna yayınla",
"publish_dialog_title_no_topic": "Bildirim yayınla",
"publish_dialog_progress_uploading_detail": "Karşıya yükleniyor: {{loaded}}/{{total}} ({{percent}}%)…",
"publish_dialog_attachment_limits_file_and_quota_reached": "{{fileSizeLimit}} dosya sınırını ve kotasınııyor, kalan {{remainingBytes}}",
"publish_dialog_priority_min": "En düşük öncelik",
"publish_dialog_priority_low": "Düşük öncelik",
"publish_dialog_base_url_label": "Hizmet URL'si",
"publish_dialog_attachment_limits_quota_reached": "kotayııyor, kalan {{remainingBytes}}",
"publish_dialog_message_published": "Bildirim yayınlandı",
"publish_dialog_title_label": "Başlık",
"publish_dialog_priority_high": "Yüksek öncelik",
"publish_dialog_priority_max": "En yüksek öncelik",
"publish_dialog_message_label": "Mesaj",
"publish_dialog_other_features": "Diğer özellikler:",
"publish_dialog_chip_email_label": "E-posta adresine ilet",
"publish_dialog_topic_label": "Konu adı",
"publish_dialog_base_url_placeholder": "Hizmet URL'si, örn. https://example.com",
"publish_dialog_title_placeholder": "Bildirim başlığı, örn. Disk alanı uyarısı",
"publish_dialog_message_placeholder": "Buraya bir mesaj yazın",
"publish_dialog_tags_label": "Etiketler",
"publish_dialog_delay_placeholder": "Teslimat gecikmesi, örn. {{unixTimestamp}}, {{relativeTime}} veya \"{{naturalLanguage}}\"",
"publish_dialog_chip_attach_file_label": "Yerel dosya ekle",
"publish_dialog_chip_delay_label": "Teslimat gecikmesi",
"publish_dialog_chip_topic_label": "Konuyu değiştir",
"publish_dialog_button_cancel_sending": "Göndermeyi iptal et",
"prefs_notifications_delete_after_one_week": "Bir hafta sonra",
"prefs_notifications_delete_after_one_month": "Bir ay sonra",
"publish_dialog_details_examples_description": "Örnekler ve tüm gönderme özelliklerinin ayrıntılııklaması için lütfen <docsLink>belgelendirmeye</docsLink> bakın.",
"emoji_picker_search_placeholder": "Emoji ara",
"prefs_notifications_delete_after_title": "Bildirimleri sil",
"prefs_notifications_delete_after_one_day": "Bir gün sonra",
"publish_dialog_drop_file_here": "Dosyayı buraya bırakın",
"prefs_notifications_min_priority_low_and_higher": "Düşük öncelik ve üstü",
"prefs_notifications_min_priority_high_and_higher": "Yüksek öncelik ve üstü",
"prefs_notifications_min_priority_max_only": "Yalnızca en yüksek öncelik",
"prefs_users_title": "Kullanıcıları yönet",
"prefs_users_dialog_title_edit": "Kullanıcıyı düzenle",
"prefs_users_dialog_base_url_label": "Hizmet URL'si, örn. https://ntfy.sh",
"prefs_users_description": "Burada korunan konularınız için kullanıcı ekleyin/kaldırın. Lütfen kullanıcı adı ve parolanın tarayıcının yerel deposunda saklandığını unutmayın.",
"prefs_users_add_button": "Kullanıcı ekle",
"prefs_users_table_base_url_header": "Hizmet URL'si",
"prefs_users_dialog_title_add": "Kullanıcı ekle",
"prefs_users_dialog_username_label": "Kullanıcı adı, örn. phil",
"prefs_users_table_user_header": "Kullanıcı",
"prefs_users_dialog_password_label": "Parola",
"prefs_users_dialog_button_add": "Ekle",
"prefs_users_dialog_button_cancel": "İptal",
"prefs_users_dialog_button_save": "Kaydet",
"prefs_appearance_title": "Görünüm",
"prefs_appearance_language_title": "Dil",
"error_boundary_title": "Olamaz, ntfy çöktü",
"error_boundary_gathering_info": "Daha fazla bilgi topla…",
"error_boundary_description": "Bunun olmaması gerekiyordu. Çok üzgünüm.<br/>Bir dakikanız varsa, lütfen <githubLink>bunu GitHub üzerinden bildirin</githubLink> ya da <discordLink>Discord</discordLink> veya <matrixLink>Matrix</matrixLink> aracılığıyla bize iletin.",
"error_boundary_button_copy_stack_trace": "Yığın izlemeyi kopyala",
"error_boundary_stack_trace": "Yığın izleme",
"prefs_notifications_sound_description_none": "Bildirimler geldiğinde herhangi bir ses çalmaz",
"prefs_notifications_sound_description_some": "Bildirimler geldiğinde {{sound}} sesini çalar",
"prefs_notifications_min_priority_description_any": "Öncelikten bağımsız olarak tüm bildirimleri göster",
"prefs_notifications_min_priority_description_x_or_higher": "Öncelik {{number}} ({{name}}) veya üstüyse bildirimleri göster",
"prefs_notifications_delete_after_one_month_description": "Bildirimler bir ay sonra otomatik olarak silinir",
"prefs_notifications_delete_after_three_hours_description": "Bildirimler üç saat sonra otomatik olarak silinir",
"prefs_notifications_delete_after_one_week_description": "Bildirimler bir hafta sonra otomatik olarak silinir",
"priority_min": "en düşük",
"priority_low": "düşük",
"priority_max": "en yüksek",
"prefs_notifications_delete_after_one_day_description": "Bildirimler bir gün sonra otomatik olarak silinir",
"priority_default": "öntanımlı",
"prefs_notifications_min_priority_description_max": "Öncelik 5 (en fazla) ise bildirimleri göster",
"prefs_notifications_delete_after_never_description": "Bildirimler asla otomatik olarak silinmez",
"priority_high": "yüksek"
}

View File

@@ -92,6 +92,19 @@ class SubscriptionManager {
});
}
async updateNotification(notification) {
const exists = await db.notifications.get(notification.id);
if (!exists) {
return false;
}
try {
await db.notifications.put({ ...notification });
} catch (e) {
console.error(`[SubscriptionManager] Error updating notification`, e);
}
return true;
}
async deleteNotification(notificationId) {
await db.notifications.delete(notificationId);
}

View File

@@ -24,7 +24,7 @@ export const expandUrl = (url) => [`https://${url}`, `http://${url}`];
export const expandSecureUrl = (url) => `https://${url}`;
export const validUrl = (url) => {
return url.match(/^https?:\/\//);
return url.match(/^https?:\/\/.+/);
}
export const validTopic = (topic) => {
@@ -105,6 +105,18 @@ export const encodeBase64Url = (s) => {
return Base64.encodeURI(s);
}
export const maybeAppendActionErrors = (message, notification) => {
const actionErrors = (notification.actions ?? [])
.map(action => action.error)
.filter(action => !!action)
.join("\n")
if (actionErrors.length === 0) {
return message;
} else {
return `${message}\n\n${actionErrors}`;
}
}
export const shuffle = (arr) => {
let j, x;
for (let index = arr.length - 1; index > 0; index--) {
@@ -153,17 +165,38 @@ export const openUrl = (url) => {
};
export const sounds = {
"beep": beep,
"juntos": juntos,
"pristine": pristine,
"ding": ding,
"dadum": dadum,
"pop": pop,
"pop-swoosh": popSwoosh
"ding": {
file: ding,
label: "Ding"
},
"juntos": {
file: juntos,
label: "Juntos"
},
"pristine": {
file: pristine,
label: "Pristine"
},
"dadum": {
file: dadum,
label: "Dadum"
},
"pop": {
file: pop,
label: "Pop"
},
"pop-swoosh": {
file: popSwoosh,
label: "Pop swoosh"
},
"beep": {
file: beep,
label: "Beep"
}
};
export const playSound = async (sound) => {
const audio = new Audio(sounds[sound]);
export const playSound = async (id) => {
const audio = new Audio(sounds[id].file);
return audio.play();
};

View File

@@ -22,14 +22,17 @@ import api from "../app/Api";
import routes from "./routes";
import subscriptionManager from "../app/SubscriptionManager";
import logo from "../img/ntfy.svg";
import {useTranslation} from "react-i18next";
import {Portal, Snackbar} from "@mui/material";
const ActionBar = (props) => {
const { t } = useTranslation();
const location = useLocation();
let title = "ntfy";
if (props.selected) {
title = topicShortUrl(props.selected.baseUrl, props.selected.topic);
} else if (location.pathname === "/settings") {
title = "Settings";
title = t("action_bar_settings");
}
return (
<AppBar position="fixed" sx={{
@@ -66,8 +69,10 @@ const ActionBar = (props) => {
// Originally from https://mui.com/components/menus/#MenuListComposition.js
const SettingsIcons = (props) => {
const { t } = useTranslation();
const navigate = useNavigate();
const [open, setOpen] = useState(false);
const [snackOpen, setSnackOpen] = useState(false);
const anchorRef = useRef(null);
const subscription = props.subscription;
@@ -143,6 +148,7 @@ const SettingsIcons = (props) => {
});
} catch (e) {
console.log(`[ActionBar] Error publishing message`, e);
setSnackOpen(true);
}
setOpen(false);
}
@@ -189,15 +195,23 @@ const SettingsIcons = (props) => {
<Paper>
<ClickAwayListener onClickAway={handleClose}>
<MenuList autoFocusItem={open} onKeyDown={handleListKeyDown}>
<MenuItem onClick={handleSendTestMessage}>Send test notification</MenuItem>
<MenuItem onClick={handleClearAll}>Clear all notifications</MenuItem>
<MenuItem onClick={handleUnsubscribe}>Unsubscribe</MenuItem>
<MenuItem onClick={handleSendTestMessage}>{t("action_bar_send_test_notification")}</MenuItem>
<MenuItem onClick={handleClearAll}>{t("action_bar_clear_notifications")}</MenuItem>
<MenuItem onClick={handleUnsubscribe}>{t("action_bar_unsubscribe")}</MenuItem>
</MenuList>
</ClickAwayListener>
</Paper>
</Grow>
)}
</Popper>
<Portal>
<Snackbar
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message={t("message_bar_error_publishing")}
/>
</Portal>
</>
);
};

View File

@@ -1,4 +1,5 @@
import * as React from 'react';
import { Suspense } from "react";
import {useEffect, useState} from 'react';
import Box from '@mui/material/Box';
import {ThemeProvider} from '@mui/material/styles';
@@ -18,29 +19,33 @@ import {expandUrl} from "../app/utils";
import ErrorBoundary from "./ErrorBoundary";
import routes from "./routes";
import {useAutoSubscribe, useBackgroundProcesses, useConnectionListeners} from "./hooks";
import SendDialog from "./SendDialog";
import PublishDialog from "./PublishDialog";
import Messaging from "./Messaging";
import "./i18n"; // Translations!
import {Backdrop, CircularProgress} from "@mui/material";
// TODO races when two tabs are open
// TODO investigate service workers
const App = () => {
return (
<BrowserRouter>
<ThemeProvider theme={theme}>
<CssBaseline/>
<ErrorBoundary>
<Routes>
<Route element={<Layout/>}>
<Route path={routes.root} element={<AllSubscriptions/>}/>
<Route path={routes.settings} element={<Preferences/>}/>
<Route path={routes.subscription} element={<SingleSubscription/>}/>
<Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/>
</Route>
</Routes>
</ErrorBoundary>
</ThemeProvider>
</BrowserRouter>
<Suspense fallback={<Loader />}>
<BrowserRouter>
<ThemeProvider theme={theme}>
<CssBaseline/>
<ErrorBoundary>
<Routes>
<Route element={<Layout/>}>
<Route path={routes.root} element={<AllSubscriptions/>}/>
<Route path={routes.settings} element={<Preferences/>}/>
<Route path={routes.subscription} element={<SingleSubscription/>}/>
<Route path={routes.subscriptionExternal} element={<SingleSubscription/>}/>
</Route>
</Routes>
</ErrorBoundary>
</ThemeProvider>
</BrowserRouter>
</Suspense>
);
}
@@ -86,7 +91,7 @@ const Layout = () => {
mobileDrawerOpen={mobileDrawerOpen}
onMobileDrawerToggle={() => setMobileDrawerOpen(!mobileDrawerOpen)}
onNotificationGranted={setNotificationsGranted}
onPublishMessageClick={() => setSendDialogOpenMode(SendDialog.OPEN_MODE_DEFAULT)}
onPublishMessageClick={() => setSendDialogOpenMode(PublishDialog.OPEN_MODE_DEFAULT)}
/>
<Main>
<Toolbar/>
@@ -122,6 +127,18 @@ const Main = (props) => {
);
};
const Loader = () => (
<Backdrop
open={true}
sx={{
zIndex: 100000,
backgroundColor: (theme) => theme.palette.mode === 'light' ? theme.palette.grey[100] : theme.palette.grey[900]
}}
>
<CircularProgress color="success" disableShrink />
</Backdrop>
);
const updateTitle = (newNotificationsCount) => {
document.title = (newNotificationsCount > 0) ? `(${newNotificationsCount}) ntfy` : "ntfy";
}

View File

@@ -9,6 +9,7 @@ import IconButton from "@mui/material/IconButton";
import {Close} from "@mui/icons-material";
import Popper from "@mui/material/Popper";
import {splitNoEmpty} from "../app/utils";
import {useTranslation} from "react-i18next";
// Create emoji list by category and create a search base (string with all search words)
//
@@ -36,6 +37,7 @@ rawEmojis.forEach(emoji => {
});
const EmojiPicker = (props) => {
const { t } = useTranslation();
const open = Boolean(props.anchorEl);
const [search, setSearch] = useState("");
const searchRef = useRef(null);
@@ -71,7 +73,7 @@ const EmojiPicker = (props) => {
inputRef={searchRef}
margin="dense"
size="small"
placeholder="Search emoji"
placeholder={t("emoji_picker_search_placeholder")}
value={search}
onChange={ev => setSearch(ev.target.value)}
type="text"

View File

@@ -1,9 +1,10 @@
import * as React from "react";
import StackTrace from "stacktrace-js";
import {CircularProgress} from "@mui/material";
import {CircularProgress, Link} from "@mui/material";
import Button from "@mui/material/Button";
import {Trans, withTranslation} from "react-i18next";
class ErrorBoundary extends React.Component {
class ErrorBoundaryImpl extends React.Component {
constructor(props) {
super(props);
this.state = {
@@ -45,22 +46,28 @@ class ErrorBoundary extends React.Component {
}
render() {
const { t } = this.props;
if (this.state.error) {
return (
<div style={{margin: '20px'}}>
<h2>Oh no, ntfy crashed 😮</h2>
<h2>{t("error_boundary_title")} 😮</h2>
<p>
This should obviously not happen. Very sorry about this.<br/>
If you have a minute, please <a href="https://github.com/binwiederhier/ntfy/issues">report this on GitHub</a>, or let us
know via <a href="https://discord.gg/cT7ECsZj9w">Discord</a> or <a href="https://matrix.to/#/#ntfy:matrix.org">Matrix</a>.
<Trans
i18nKey="error_boundary_description"
components={{
githubLink: <Link href="https://github.com/binwiederhier/ntfy/issues"/>,
discordLink: <Link href="https://discord.gg/cT7ECsZj9w"/>,
matrixLink: <Link href="https://matrix.to/#/#ntfy:matrix.org"/>
}}
/>
</p>
<p>
<Button variant="outlined" onClick={() => this.copyStack()}>Copy stack trace</Button>
<Button variant="outlined" onClick={() => this.copyStack()}>{t("error_boundary_button_copy_stack_trace")}</Button>
</p>
<h3>Stack trace</h3>
<h3>{t("error_boundary_stack_trace")}</h3>
{this.state.niceStack
? <pre>{this.state.niceStack}</pre>
: <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> Gather more info ...</>}
: <><CircularProgress size="20px" sx={{verticalAlign: "text-bottom"}}/> {t("error_boundary_gathering_info")}</>}
<pre>{this.state.originalStack}</pre>
</div>
);
@@ -69,4 +76,5 @@ class ErrorBoundary extends React.Component {
}
}
const ErrorBoundary = withTranslation()(ErrorBoundaryImpl); // Adds props.t
export default ErrorBoundary;

View File

@@ -1,15 +1,15 @@
import * as React from 'react';
import {useState} from 'react';
import Navigation from "./Navigation";
import {topicUrl} from "../app/utils";
import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton";
import TextField from "@mui/material/TextField";
import SendIcon from "@mui/icons-material/Send";
import api from "../app/Api";
import SendDialog from "./SendDialog";
import PublishDialog from "./PublishDialog";
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import {Portal, Snackbar} from "@mui/material";
import {useTranslation} from "react-i18next";
const Messaging = (props) => {
const [message, setMessage] = useState("");
@@ -19,10 +19,10 @@ const Messaging = (props) => {
const subscription = props.selected;
const handleOpenDialogClick = () => {
props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT);
props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT);
};
const handleSendDialogClose = () => {
const handleDialogClose = () => {
props.onDialogOpenModeChange("");
setDialogKey(prev => prev+1);
};
@@ -35,21 +35,22 @@ const Messaging = (props) => {
onMessageChange={setMessage}
onOpenDialogClick={handleOpenDialogClick}
/>}
<SendDialog
key={`sendDialog${dialogKey}`} // Resets dialog when canceled/closed
<PublishDialog
key={`publishDialog${dialogKey}`} // Resets dialog when canceled/closed
openMode={dialogOpenMode}
baseUrl={subscription?.baseUrl ?? window.location.origin}
topic={subscription?.topic ?? ""}
message={message}
onClose={handleSendDialogClose}
onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : SendDialog.OPEN_MODE_DRAG)} // Only update if not already open
onResetOpenMode={() => props.onDialogOpenModeChange(SendDialog.OPEN_MODE_DEFAULT)}
onClose={handleDialogClose}
onDragEnter={() => props.onDialogOpenModeChange(prev => (prev) ? prev : PublishDialog.OPEN_MODE_DRAG)} // Only update if not already open
onResetOpenMode={() => props.onDialogOpenModeChange(PublishDialog.OPEN_MODE_DEFAULT)}
/>
</>
);
}
const MessageBar = (props) => {
const { t } = useTranslation();
const subscription = props.subscription;
const [snackOpen, setSnackOpen] = useState(false);
const handleSendClick = async () => {
@@ -80,7 +81,7 @@ const MessageBar = (props) => {
<TextField
autoFocus
margin="dense"
placeholder="Type a message here"
placeholder={t("message_bar_type_message")}
type="text"
fullWidth
variant="standard"
@@ -101,7 +102,7 @@ const MessageBar = (props) => {
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message="Error publishing message"
message={t("message_bar_error_publishing")}
/>
</Portal>
</Paper>

View File

@@ -24,6 +24,7 @@ import Box from "@mui/material/Box";
import notifier from "../app/Notifier";
import config from "../app/config";
import ArticleIcon from '@mui/icons-material/Article';
import {useTranslation} from "react-i18next";
const navWidth = 280;
@@ -61,6 +62,7 @@ const Navigation = (props) => {
Navigation.width = navWidth;
const NavList = (props) => {
const { t } = useTranslation();
const navigate = useNavigate();
const location = useLocation();
const [subscribeDialogKey, setSubscribeDialogKey] = useState(0);
@@ -95,14 +97,14 @@ const NavList = (props) => {
{!showSubscriptionsList &&
<ListItemButton onClick={() => navigate(routes.root)} selected={location.pathname === config.appRoot}>
<ListItemIcon><ChatBubble/></ListItemIcon>
<ListItemText primary="All notifications"/>
<ListItemText primary={t("nav_button_all_notifications")}/>
</ListItemButton>}
{showSubscriptionsList &&
<>
<ListSubheader>Subscribed topics</ListSubheader>
<ListSubheader>{t("nav_topics_title")}</ListSubheader>
<ListItemButton onClick={() => navigate(routes.root)} selected={location.pathname === config.appRoot}>
<ListItemIcon><ChatBubble/></ListItemIcon>
<ListItemText primary="All notifications"/>
<ListItemText primary={t("nav_button_all_notifications")}/>
</ListItemButton>
<SubscriptionList
subscriptions={props.subscriptions}
@@ -112,19 +114,19 @@ const NavList = (props) => {
</>}
<ListItemButton onClick={() => navigate(routes.settings)} selected={location.pathname === routes.settings}>
<ListItemIcon><SettingsIcon/></ListItemIcon>
<ListItemText primary="Settings"/>
<ListItemText primary={t("nav_button_settings")}/>
</ListItemButton>
<ListItemButton onClick={() => openUrl("/docs")}>
<ListItemIcon><ArticleIcon/></ListItemIcon>
<ListItemText primary="Documentation"/>
<ListItemText primary={t("nav_button_documentation")}/>
</ListItemButton>
<ListItemButton onClick={() => props.onPublishMessageClick()}>
<ListItemIcon><Send/></ListItemIcon>
<ListItemText primary="Publish message"/>
<ListItemText primary={t("nav_button_publish_message")}/>
</ListItemButton>
<ListItemButton onClick={() => setSubscribeDialogOpen(true)}>
<ListItemIcon><AddIcon/></ListItemIcon>
<ListItemText primary="Subscribe to topic"/>
<ListItemText primary={t("nav_button_subscribe")}/>
</ListItemButton>
</List>
<SubscribeDialog
@@ -179,20 +181,19 @@ const SubscriptionItem = (props) => {
};
const NotificationGrantAlert = (props) => {
const { t } = useTranslation();
return (
<>
<Alert severity="warning" sx={{paddingTop: 2}}>
<AlertTitle>Notifications are disabled</AlertTitle>
<Typography gutterBottom>
Grant your browser permission to display desktop notifications.
</Typography>
<AlertTitle>{t("alert_grant_title")}</AlertTitle>
<Typography gutterBottom>{t("alert_grant_description")}</Typography>
<Button
sx={{float: 'right'}}
color="inherit"
size="small"
onClick={props.onRequestPermissionClick}
>
Grant now
{t("alert_grant_button")}
</Button>
</Alert>
<Divider/>
@@ -201,13 +202,12 @@ const NotificationGrantAlert = (props) => {
};
const NotificationNotSupportedAlert = () => {
const { t } = useTranslation();
return (
<>
<Alert severity="warning" sx={{paddingTop: 2}}>
<AlertTitle>Notifications not supported</AlertTitle>
<Typography gutterBottom>
Notifications are not supported in your browser.
</Typography>
<AlertTitle>{t("alert_not_supported_title")}</AlertTitle>
<Typography gutterBottom>{t("alert_not_supported_description")}</Typography>
</Alert>
<Divider/>
</>

View File

@@ -19,7 +19,7 @@ import {
formatBytes,
formatMessage,
formatShortDateTime,
formatTitle,
formatTitle, maybeAppendActionErrors,
openUrl,
shortUrl,
topicShortUrl,
@@ -39,6 +39,7 @@ import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
import logoOutline from "../img/ntfy-outline.svg";
import AttachmentIcon from "./AttachmentIcon";
import {Trans, useTranslation} from "react-i18next";
const Notifications = (props) => {
if (props.mode === "all") {
@@ -72,6 +73,7 @@ const SingleSubscription = (props) => {
}
const NotificationList = (props) => {
const { t } = useTranslation();
const pageSize = 20;
const notifications = props.notifications;
const [snackOpen, setSnackOpen] = useState(false);
@@ -112,7 +114,7 @@ const NotificationList = (props) => {
open={snackOpen}
autoHideDuration={3000}
onClose={() => setSnackOpen(false)}
message="Copied to clipboard"
message={t("notifications_copied_to_clipboard")}
/>
</Stack>
</Container>
@@ -121,6 +123,7 @@ const NotificationList = (props) => {
}
const NotificationItem = (props) => {
const { t } = useTranslation();
const notification = props.notification;
const attachment = notification.attachment;
const date = formatShortDateTime(notification.time);
@@ -135,9 +138,10 @@ const NotificationItem = (props) => {
props.onShowSnack();
};
const expired = attachment && attachment.expires && attachment.expires < Date.now()/1000;
const showAttachmentActions = attachment && !expired;
const showClickAction = notification.click;
const showActions = showAttachmentActions || showClickAction;
const hasAttachmentActions = attachment && !expired;
const hasClickAction = notification.click;
const hasUserActions = notification.actions && notification.actions.length > 0;
const showActions = hasAttachmentActions || hasClickAction || hasUserActions;
return (
<Card sx={{ minWidth: 275, padding: 1 }}>
<CardContent>
@@ -158,28 +162,31 @@ const NotificationItem = (props) => {
</svg>}
</Typography>
{notification.title && <Typography variant="h5" component="div">{formatTitle(notification)}</Typography>}
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>{autolink(formatMessage(notification))}</Typography>
<Typography variant="body1" sx={{ whiteSpace: 'pre-line' }}>
{autolink(maybeAppendActionErrors(formatMessage(notification), notification))}
</Typography>
{attachment && <Attachment attachment={attachment}/>}
{tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">Tags: {tags}</Typography>}
{tags && <Typography sx={{ fontSize: 14 }} color="text.secondary">{t("notifications_tags")}: {tags}</Typography>}
</CardContent>
{showActions &&
<CardActions sx={{paddingTop: 0}}>
{showAttachmentActions && <>
<Tooltip title="Copy attachment URL to clipboard">
<Button onClick={() => handleCopy(attachment.url)}>Copy URL</Button>
{hasAttachmentActions && <>
<Tooltip title={t("notifications_attachment_copy_url_title")}>
<Button onClick={() => handleCopy(attachment.url)}>{t("notifications_attachment_copy_url_button")}</Button>
</Tooltip>
<Tooltip title={`Go to ${attachment.url}`}>
<Button onClick={() => openUrl(attachment.url)}>Open attachment</Button>
<Tooltip title={t("notifications_attachment_open_title", { url: attachment.url })}>
<Button onClick={() => openUrl(attachment.url)}>{t("notifications_attachment_open_button")}</Button>
</Tooltip>
</>}
{showClickAction && <>
<Tooltip title="Copy link URL to clipboard">
<Button onClick={() => handleCopy(notification.click)}>Copy link</Button>
{hasClickAction && <>
<Tooltip title={t("notifications_click_copy_url_title")}>
<Button onClick={() => handleCopy(notification.click)}>{t("notifications_click_copy_url_button")}</Button>
</Tooltip>
<Tooltip title={`Go to ${notification.click}`}>
<Button onClick={() => openUrl(notification.click)}>Open link</Button>
<Tooltip title={t("notifications_actions_open_url_title", { url: notification.click })}>
<Button onClick={() => openUrl(notification.click)}>{t("notifications_click_open_button")}</Button>
</Tooltip>
</>}
{hasUserActions && <UserActions notification={notification}/>}
</CardActions>}
</Card>
);
@@ -208,6 +215,7 @@ const priorityFiles = {
};
const Attachment = (props) => {
const { t } = useTranslation();
const attachment = props.attachment;
const expired = attachment.expires && attachment.expires < Date.now()/1000;
const expires = attachment.expires && attachment.expires > Date.now()/1000;
@@ -224,10 +232,10 @@ const Attachment = (props) => {
infos.push(formatBytes(attachment.size));
}
if (expires) {
infos.push(`link expires ${formatShortDateTime(attachment.expires)}`);
infos.push(t("notifications_attachment_link_expires", { date: formatShortDateTime(attachment.expires) }));
}
if (expired) {
infos.push(`download link expired`);
infos.push(t("notifications_attachment_link_expired"));
}
const maybeInfoText = (infos.length > 0) ? <><br/>{infos.join(", ")}</> : null;
@@ -325,83 +333,173 @@ const Image = (props) => {
);
}
const UserActions = (props) => {
return (
<>{props.notification.actions.map(action =>
<UserAction key={action.id} notification={props.notification} action={action}/>)}</>
);
};
const UserAction = (props) => {
const { t } = useTranslation();
const notification = props.notification;
const action = props.action;
if (action.action === "broadcast") {
return (
<Tooltip title={t("notifications_actions_not_supported")}>
<span><Button disabled>{action.label}</Button></span>
</Tooltip>
);
} else if (action.action === "view") {
return (
<Tooltip title={t("notifications_actions_open_url_title", { url: action.url })}>
<Button onClick={() => openUrl(action.url)}>{action.label}</Button>
</Tooltip>
);
} else if (action.action === "http") {
const method = action.method ?? "POST";
const label = action.label + (ACTION_LABEL_SUFFIX[action.progress ?? 0] ?? "");
return (
<Tooltip title={t("notifications_actions_http_request_title", { method: method, url: action.url })}>
<Button onClick={() => performHttpAction(notification, action)}>{label}</Button>
</Tooltip>
);
}
return null; // Others
};
const performHttpAction = async (notification, action) => {
console.log(`[Notifications] Performing HTTP user action`, action);
try {
updateActionStatus(notification, action, ACTION_PROGRESS_ONGOING, null);
const response = await fetch(action.url, {
method: action.method ?? "POST",
headers: action.headers ?? {},
body: action.body ?? ""
});
console.log(`[Notifications] HTTP user action response`, response);
const success = response.status >= 200 && response.status <= 299;
if (success) {
updateActionStatus(notification, action, ACTION_PROGRESS_SUCCESS, null);
} else {
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: Unexpected response HTTP ${response.status}`);
}
} catch (e) {
console.log(`[Notifications] HTTP action failed`, e);
updateActionStatus(notification, action, ACTION_PROGRESS_FAILED, `${action.label}: ${e} Check developer console for details.`);
}
};
const updateActionStatus = (notification, action, progress, error) => {
notification.actions = notification.actions.map(a => {
if (a.id !== action.id) {
return a;
}
return { ...a, progress: progress, error: error };
});
subscriptionManager.updateNotification(notification);
}
const ACTION_PROGRESS_ONGOING = 1;
const ACTION_PROGRESS_SUCCESS = 2;
const ACTION_PROGRESS_FAILED = 3;
const ACTION_LABEL_SUFFIX = {
[ACTION_PROGRESS_ONGOING]: " …",
[ACTION_PROGRESS_SUCCESS]: " ✔",
[ACTION_PROGRESS_FAILED]: " ❌"
};
const NoNotifications = (props) => {
const { t } = useTranslation();
const shortUrl = topicShortUrl(props.subscription.baseUrl, props.subscription.topic);
return (
<VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
<img src={logoOutline} height="64" width="64" alt="No notifications"/><br />
You haven't received any notifications for this topic yet.
<img src={logoOutline} height="64" width="64"/><br />
{t("notifications_none_for_topic_title")}
</Typography>
<Paragraph>
To send notifications to this topic, simply PUT or POST to the topic URL.
{t("notifications_none_for_topic_description")}
</Paragraph>
<Paragraph>
Example:<br/>
{t("notifications_example")}:<br/>
<tt>
$ curl -d "Hi" {shortUrl}
</tt>
</Paragraph>
<Paragraph>
For more detailed instructions, check out the <Link href="https://ntfy.sh" target="_blank" rel="noopener">website</Link> or
{" "}<Link href="https://ntfy.sh/docs" target="_blank" rel="noopener">documentation</Link>.
<ForMoreDetails/>
</Paragraph>
</VerticallyCenteredContainer>
);
};
const NoNotificationsWithoutSubscription = (props) => {
const { t } = useTranslation();
const subscription = props.subscriptions[0];
const shortUrl = topicShortUrl(subscription.baseUrl, subscription.topic);
return (
<VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
<img src={logoOutline} height="64" width="64" alt="No notifications"/><br />
You haven't received any notifications.
<img src={logoOutline} height="64" width="64"/><br />
{t("notifications_none_for_any_title")}
</Typography>
<Paragraph>
To send notifications to a topic, simply PUT or POST to the topic URL. Here's
an example using one of your topics.
{t("notifications_none_for_any_description")}
</Paragraph>
<Paragraph>
Example:<br/>
{t("notifications_example")}:<br/>
<tt>
$ curl -d "Hi" {shortUrl}
</tt>
</Paragraph>
<Paragraph>
For more detailed instructions, check out the <Link href="https://ntfy.sh" target="_blank" rel="noopener">website</Link> or
{" "}<Link href="https://ntfy.sh/docs" target="_blank" rel="noopener">documentation</Link>.
<ForMoreDetails/>
</Paragraph>
</VerticallyCenteredContainer>
);
};
const NoSubscriptions = () => {
const { t } = useTranslation();
return (
<VerticallyCenteredContainer maxWidth="xs">
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
<img src={logoOutline} height="64" width="64" alt="No topics"/><br />
It looks like you don't have any subscriptions yet.
<img src={logoOutline} height="64" width="64"/><br />
{t("notifications_no_subscriptions_title")}
</Typography>
<Paragraph>
Click the "Add subscription" link to create or subscribe to a topic. After that, you can send messages
via PUT or POST and you'll receive notifications here.
{t("notifications_no_subscriptions_description", {
linktext: t("nav_button_subscribe")
})}
</Paragraph>
<Paragraph>
For more information, check out the <Link href="https://ntfy.sh" target="_blank" rel="noopener">website</Link> or
{" "}<Link href="https://ntfy.sh/docs" target="_blank" rel="noopener">documentation</Link>.
<ForMoreDetails/>
</Paragraph>
</VerticallyCenteredContainer>
);
};
const ForMoreDetails = () => {
return (
<Trans
i18nKey="notifications_more_details"
components={{
websiteLink: <Link href="https://ntfy.sh" target="_blank" rel="noopener"/>,
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener"/>
}}
/>
);
};
const Loading = () => {
const { t } = useTranslation();
return (
<VerticallyCenteredContainer>
<Typography variant="h5" color="text.secondary" align="center" sx={{ paddingBottom: 1 }}>
<CircularProgress disableShrink sx={{marginBottom: 1}}/><br />
Loading notifications ...
{t("notifications_loading")}
</Typography>
</VerticallyCenteredContainer>
);

View File

@@ -32,13 +32,20 @@ import DialogTitle from "@mui/material/DialogTitle";
import DialogContent from "@mui/material/DialogContent";
import DialogActions from "@mui/material/DialogActions";
import userManager from "../app/UserManager";
import {playSound} from "../app/utils";
import {playSound, shuffle, sounds, validUrl} from "../app/utils";
import {useTranslation} from "react-i18next";
import priority1 from "../img/priority-1.svg";
import priority2 from "../img/priority-2.svg";
import priority3 from "../img/priority-3.svg";
import priority4 from "../img/priority-4.svg";
import priority5 from "../img/priority-5.svg";
const Preferences = () => {
return (
<Container maxWidth="md" sx={{marginTop: 3, marginBottom: 3}}>
<Stack spacing={3}>
<Notifications/>
<Appearance/>
<Users/>
</Stack>
</Container>
@@ -46,10 +53,11 @@ const Preferences = () => {
};
const Notifications = () => {
const { t } = useTranslation();
return (
<Card sx={{p: 3}}>
<Typography variant="h5">
Notifications
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_notifications_title")}
</Typography>
<PrefGroup>
<Sound/>
@@ -60,8 +68,8 @@ const Notifications = () => {
);
};
const Sound = () => {
const { t } = useTranslation();
const sound = useLiveQuery(async () => prefs.sound());
const handleChange = async (ev) => {
await prefs.setSound(ev.target.value);
@@ -69,19 +77,19 @@ const Sound = () => {
if (!sound) {
return null; // While loading
}
let description;
if (sound === "none") {
description = t("prefs_notifications_sound_description_none");
} else {
description = t("prefs_notifications_sound_description_some", { sound: sounds[sound].label });
}
return (
<Pref title="Notification sound">
<Pref title={t("prefs_notifications_sound_title")} description={description}>
<div style={{ display: 'flex', width: '100%' }}>
<FormControl fullWidth variant="standard" sx={{ margin: 1 }}>
<Select value={sound} onChange={handleChange}>
<MenuItem value={"none"}>No sound</MenuItem>
<MenuItem value={"ding"}>Ding</MenuItem>
<MenuItem value={"juntos"}>Juntos</MenuItem>
<MenuItem value={"pristine"}>Pristine</MenuItem>
<MenuItem value={"dadum"}>Dadum</MenuItem>
<MenuItem value={"pop"}>Pop</MenuItem>
<MenuItem value={"pop-swoosh"}>Pop swoosh</MenuItem>
<MenuItem value={"beep"}>Beep</MenuItem>
<MenuItem value={"none"}>{t("prefs_notifications_sound_no_sound")}</MenuItem>
{Object.entries(sounds).map(s => <MenuItem key={s[0]} value={s[0]}>{s[1].label}</MenuItem>)}
</Select>
</FormControl>
<IconButton onClick={() => playSound(sound)} disabled={sound === "none"}>
@@ -93,6 +101,7 @@ const Sound = () => {
};
const MinPriority = () => {
const { t } = useTranslation();
const minPriority = useLiveQuery(async () => prefs.minPriority());
const handleChange = async (ev) => {
await prefs.setMinPriority(ev.target.value);
@@ -100,15 +109,33 @@ const MinPriority = () => {
if (!minPriority) {
return null; // While loading
}
const priorities = {
1: t("priority_min"),
2: t("priority_low"),
3: t("priority_default"),
4: t("priority_high"),
5: t("priority_max")
}
let description;
if (minPriority === 1) {
description = t("prefs_notifications_min_priority_description_any");
} else if (minPriority === 5) {
description = t("prefs_notifications_min_priority_description_max");
} else {
description = t("prefs_notifications_min_priority_description_x_or_higher", {
number: minPriority,
name: priorities[minPriority]
});
}
return (
<Pref title="Minimum priority">
<Pref title={t("prefs_notifications_min_priority_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={minPriority} onChange={handleChange}>
<MenuItem value={1}>Any priority</MenuItem>
<MenuItem value={2}>Low priority and higher</MenuItem>
<MenuItem value={3}>Default priority and higher</MenuItem>
<MenuItem value={4}>High priority and higher</MenuItem>
<MenuItem value={5}>Only max priority</MenuItem>
<MenuItem value={1}>{t("prefs_notifications_min_priority_any")}</MenuItem>
<MenuItem value={2}>{t("prefs_notifications_min_priority_low_and_higher")}</MenuItem>
<MenuItem value={3}>{t("prefs_notifications_min_priority_default_and_higher")}</MenuItem>
<MenuItem value={4}>{t("prefs_notifications_min_priority_high_and_higher")}</MenuItem>
<MenuItem value={5}>{t("prefs_notifications_min_priority_max_only")}</MenuItem>
</Select>
</FormControl>
</Pref>
@@ -116,22 +143,32 @@ const MinPriority = () => {
};
const DeleteAfter = () => {
const { t } = useTranslation();
const deleteAfter = useLiveQuery(async () => prefs.deleteAfter());
const handleChange = async (ev) => {
await prefs.setDeleteAfter(ev.target.value);
}
if (!deleteAfter) {
if (deleteAfter === null || deleteAfter === undefined) { // !deleteAfter will not work with "0"
return null; // While loading
}
const description = (() => {
switch (deleteAfter) {
case 0: return t("prefs_notifications_delete_after_never_description");
case 10800: return t("prefs_notifications_delete_after_three_hours_description");
case 86400: return t("prefs_notifications_delete_after_one_day_description");
case 604800: return t("prefs_notifications_delete_after_one_week_description");
case 2592000: return t("prefs_notifications_delete_after_one_month_description");
}
})();
return (
<Pref title="Delete notifications">
<Pref title={t("prefs_notifications_delete_after_title")} description={description}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={deleteAfter} onChange={handleChange}>
<MenuItem value={0}>Never</MenuItem>
<MenuItem value={10800}>After three hours</MenuItem>
<MenuItem value={86400}>After one day</MenuItem>
<MenuItem value={604800}>After one week</MenuItem>
<MenuItem value={2592000}>After one month</MenuItem>
<MenuItem value={0}>{t("prefs_notifications_delete_after_never")}</MenuItem>
<MenuItem value={10800}>{t("prefs_notifications_delete_after_three_hours")}</MenuItem>
<MenuItem value={86400}>{t("prefs_notifications_delete_after_one_day")}</MenuItem>
<MenuItem value={604800}>{t("prefs_notifications_delete_after_one_week")}</MenuItem>
<MenuItem value={2592000}>{t("prefs_notifications_delete_after_one_month")}</MenuItem>
</Select>
</FormControl>
</Pref>
@@ -140,10 +177,7 @@ const DeleteAfter = () => {
const PrefGroup = (props) => {
return (
<div style={{
display: 'flex',
flexWrap: 'wrap'
}}>
<div>
{props.children}
</div>
)
@@ -151,30 +185,36 @@ const PrefGroup = (props) => {
const Pref = (props) => {
return (
<>
<div style={{
display: "flex",
flexDirection: "row",
marginTop: "10px",
marginBottom: "20px",
}}>
<div style={{
flex: '1 0 30%',
display: 'inline-flex',
flex: '1 0 40%',
display: 'flex',
flexDirection: 'column',
minHeight: '60px',
justifyContent: 'center'
justifyContent: 'center',
paddingRight: '30px'
}}>
<b>{props.title}</b>
<div><b>{props.title}</b></div>
{props.description && <div><em>{props.description}</em></div>}
</div>
<div style={{
flex: '1 0 calc(70% - 50px)',
display: 'inline-flex',
flex: '1 0 calc(60% - 50px)',
display: 'flex',
flexDirection: 'column',
minHeight: '60px',
justifyContent: 'center'
}}>
{props.children}
</div>
</>
</div>
);
};
const Users = () => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const users = useLiveQuery(() => userManager.all());
@@ -196,18 +236,17 @@ const Users = () => {
};
return (
<Card sx={{ padding: 1 }}>
<CardContent>
<Typography variant="h5">
Manage users
<CardContent sx={{ paddingBottom: 1 }}>
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_users_title")}
</Typography>
<Paragraph>
Add/remove users for your protected topics here. Please note that username and password are
stored in the browser's local storage.
{t("prefs_users_description")}
</Paragraph>
{users?.length > 0 && <UserTable users={users}/>}
</CardContent>
<CardActions>
<Button onClick={handleAddClick}>Add user</Button>
<Button onClick={handleAddClick}>{t("prefs_users_add_button")}</Button>
<UserDialog
key={`userAddDialog${dialogKey}`}
open={dialogOpen}
@@ -222,6 +261,7 @@ const Users = () => {
};
const UserTable = (props) => {
const { t } = useTranslation();
const [dialogKey, setDialogKey] = useState(0);
const [dialogOpen, setDialogOpen] = useState(false);
const [dialogUser, setDialogUser] = useState(null);
@@ -254,8 +294,8 @@ const UserTable = (props) => {
<Table size="small">
<TableHead>
<TableRow>
<TableCell>User</TableCell>
<TableCell>Service URL</TableCell>
<TableCell sx={{paddingLeft: 0}}>{t("prefs_users_table_user_header")}</TableCell>
<TableCell>{t("prefs_users_table_base_url_header")}</TableCell>
<TableCell/>
</TableRow>
</TableHead>
@@ -265,7 +305,7 @@ const UserTable = (props) => {
key={user.baseUrl}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell component="th" scope="row">{user.username}</TableCell>
<TableCell component="th" scope="row" sx={{paddingLeft: 0}}>{user.username}</TableCell>
<TableCell>{user.baseUrl}</TableCell>
<TableCell align="right">
<IconButton onClick={() => handleEditClick(user)}>
@@ -291,6 +331,7 @@ const UserTable = (props) => {
};
const UserDialog = (props) => {
const { t } = useTranslation();
const [baseUrl, setBaseUrl] = useState("");
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
@@ -300,8 +341,12 @@ const UserDialog = (props) => {
if (editMode) {
return username.length > 0 && password.length > 0;
}
const baseUrlValid = validUrl(baseUrl);
const baseUrlExists = props.users?.map(user => user.baseUrl).includes(baseUrl);
return !baseUrlExists && username.length > 0 && password.length > 0;
return baseUrlValid
&& !baseUrlExists
&& username.length > 0
&& password.length > 0;
})();
const handleSubmit = async () => {
props.onSubmit({
@@ -319,13 +364,13 @@ const UserDialog = (props) => {
}, [editMode, props.user]);
return (
<Dialog open={props.open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{editMode ? "Edit user" : "Add user"}</DialogTitle>
<DialogTitle>{editMode ? t("prefs_users_dialog_title_edit") : t("prefs_users_dialog_title_add")}</DialogTitle>
<DialogContent>
{!editMode && <TextField
autoFocus
margin="dense"
id="baseUrl"
label="Service URL, e.g. https://ntfy.sh"
label={t("prefs_users_dialog_base_url_label")}
value={baseUrl}
onChange={ev => setBaseUrl(ev.target.value)}
type="url"
@@ -336,7 +381,7 @@ const UserDialog = (props) => {
autoFocus={editMode}
margin="dense"
id="username"
label="Username, e.g. phil"
label={t("prefs_users_dialog_username_label")}
value={username}
onChange={ev => setUsername(ev.target.value)}
type="text"
@@ -346,7 +391,7 @@ const UserDialog = (props) => {
<TextField
margin="dense"
id="password"
label="Password"
label={t("prefs_users_dialog_password_label")}
type="password"
value={password}
onChange={ev => setPassword(ev.target.value)}
@@ -355,11 +400,53 @@ const UserDialog = (props) => {
/>
</DialogContent>
<DialogActions>
<Button onClick={props.onCancel}>Cancel</Button>
<Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? "Save" : "Add"}</Button>
<Button onClick={props.onCancel}>{t("prefs_users_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!addButtonEnabled}>{editMode ? t("prefs_users_dialog_button_save") : t("prefs_users_dialog_button_add")}</Button>
</DialogActions>
</Dialog>
);
};
const Appearance = () => {
const { t } = useTranslation();
return (
<Card sx={{p: 3}}>
<Typography variant="h5" sx={{marginBottom: 2}}>
{t("prefs_appearance_title")}
</Typography>
<PrefGroup>
<Language/>
</PrefGroup>
</Card>
);
};
const Language = () => {
const { t, i18n } = useTranslation();
const randomFlags = shuffle(["🇬🇧", "🇺🇸", "🇪🇸", "🇧🇬", "🇩🇪", "🇮🇩", "🇯🇵", "🇷🇺", "🇹🇷"]).slice(0, 3);
const title = t("prefs_appearance_language_title") + " " + randomFlags.join(" ");
// Remember: Flags are not languages. Don't put flags next to the language in the list.
// Languages names from: https://www.omniglot.com/language/names.htm
// Better: Sidebar in Wikipedia: https://en.wikipedia.org/wiki/Bokm%C3%A5l
return (
<Pref title={title}>
<FormControl fullWidth variant="standard" sx={{ m: 1 }}>
<Select value={i18n.language} onChange={(ev) => i18n.changeLanguage(ev.target.value)}>
<MenuItem value="en">English</MenuItem>
<MenuItem value="es">Español</MenuItem>
<MenuItem value="bg">Български</MenuItem>
<MenuItem value="de">Deutsch</MenuItem>
<MenuItem value="id">Bahasa Indonesia</MenuItem>
<MenuItem value="ja">日本語</MenuItem>
<MenuItem value="nb_NO">Norsk bokmål</MenuItem>
<MenuItem value="ru">Русский</MenuItem>
<MenuItem value="tr">Türkçe</MenuItem>
</Select>
</FormControl>
</Pref>
)
};
export default Preferences;

View File

@@ -25,8 +25,10 @@ import DialogFooter from "./DialogFooter";
import api from "../app/Api";
import userManager from "../app/UserManager";
import EmojiPicker from "./EmojiPicker";
import {Trans, useTranslation} from "react-i18next";
const SendDialog = (props) => {
const PublishDialog = (props) => {
const { t } = useTranslation();
const [baseUrl, setBaseUrl] = useState("");
const [topic, setTopic] = useState("");
const [message, setMessage] = useState("");
@@ -123,10 +125,13 @@ const SendDialog = (props) => {
const headers = maybeWithBasicAuth({}, user);
const progressFn = (ev) => {
if (ev.loaded > 0 && ev.total > 0) {
const percent = Math.round(ev.loaded * 100.0 / ev.total);
setStatus(`Uploading ${formatBytes(ev.loaded)}/${formatBytes(ev.total)} (${percent}%) ...`);
setStatus(t("publish_dialog_progress_uploading_detail", {
loaded: formatBytes(ev.loaded),
total: formatBytes(ev.total),
percent: Math.round(ev.loaded * 100.0 / ev.total)
}));
} else {
setStatus(`Uploading ...`);
setStatus(t("publish_dialog_progress_uploading"));
}
};
const request = api.publishXHR(url, body, headers, progressFn);
@@ -135,7 +140,7 @@ const SendDialog = (props) => {
if (!publishAnother) {
props.onClose();
} else {
setStatus("Message published");
setStatus(t("publish_dialog_message_published"));
setActiveRequest(null);
}
} catch (e) {
@@ -152,11 +157,14 @@ const SendDialog = (props) => {
const fileSizeLimitReached = fileSizeLimit > 0 && file.size > fileSizeLimit;
const quotaReached = remainingBytes > 0 && file.size > remainingBytes;
if (fileSizeLimitReached && quotaReached) {
return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit and quota, ${formatBytes(remainingBytes)} remaining`);
return setAttachFileError(t("publish_dialog_attachment_limits_file_and_quota_reached", {
fileSizeLimit: formatBytes(fileSizeLimit),
remainingBytes: formatBytes(remainingBytes)
}));
} else if (fileSizeLimitReached) {
return setAttachFileError(`exceeds ${formatBytes(fileSizeLimit)} file limit`);
return setAttachFileError(t("publish_dialog_attachment_limits_file_reached", { fileSizeLimit: formatBytes(fileSizeLimit) }));
} else if (quotaReached) {
return setAttachFileError(`exceeds quota, ${formatBytes(remainingBytes)} remaining`);
return setAttachFileError(t("publish_dialog_attachment_limits_quota_reached", { remainingBytes: formatBytes(remainingBytes) }));
}
setAttachFileError("");
} catch (e) {
@@ -188,7 +196,7 @@ const SendDialog = (props) => {
const handleAttachFileDragLeave = () => {
setDropZone(false);
if (props.openMode === SendDialog.OPEN_MODE_DRAG) {
if (props.openMode === PublishDialog.OPEN_MODE_DRAG) {
props.onClose(); // Only close dialog if it was not open before dragging file in
}
};
@@ -205,6 +213,14 @@ const SendDialog = (props) => {
setEmojiPickerAnchorEl(null);
};
const priorities = {
1: { label: t("publish_dialog_priority_min"), file: priority1 },
2: { label: t("publish_dialog_priority_low"), file: priority2 },
3: { label: t("publish_dialog_priority_default"), file: priority3 },
4: { label: t("publish_dialog_priority_high"), file: priority4 },
5: { label: t("publish_dialog_priority_max"), file: priority5 }
};
return (
<>
{dropZone && <DropArea
@@ -212,7 +228,7 @@ const SendDialog = (props) => {
onDragLeave={handleAttachFileDragLeave}/>
}
<Dialog maxWidth="md" open={open} onClose={props.onCancel} fullScreen={fullScreen}>
<DialogTitle>{(baseUrl && topic) ? `Publish to ${topicShortUrl(baseUrl, topic)}` : "Publish message"}</DialogTitle>
<DialogTitle>{(baseUrl && topic) ? t("publish_dialog_title_topic", { topic: topicShortUrl(baseUrl, topic) }) : t("publish_dialog_title_no_topic")}</DialogTitle>
<DialogContent>
{dropZone && <DropBox/>}
{showTopicUrl &&
@@ -223,8 +239,8 @@ const SendDialog = (props) => {
}}>
<TextField
margin="dense"
label="Server URL"
placeholder="Server URL, e.g. https://example.com"
label={t("publish_dialog_base_url_label")}
placeholder={t("publish_dialog_base_url_placeholder")}
value={baseUrl}
onChange={ev => setBaseUrl(ev.target.value)}
disabled={disabled}
@@ -234,8 +250,8 @@ const SendDialog = (props) => {
/>
<TextField
margin="dense"
label="Topic"
placeholder="Topic name, e.g. phil_alerts"
label={t("publish_dialog_topic_label")}
placeholder={t("publish_dialog_topic_placeholder")}
value={topic}
onChange={ev => setTopic(ev.target.value)}
disabled={disabled}
@@ -248,19 +264,19 @@ const SendDialog = (props) => {
}
<TextField
margin="dense"
label="Title"
label={t("publish_dialog_title_label")}
placeholder={t("publish_dialog_title_placeholder")}
value={title}
onChange={ev => setTitle(ev.target.value)}
disabled={disabled}
type="text"
fullWidth
variant="standard"
placeholder="Notification title, e.g. Disk space alert"
/>
<TextField
margin="dense"
label="Message"
placeholder="Type a message here"
label={t("publish_dialog_message_label")}
placeholder={t("publish_dialog_message_placeholder")}
value={message}
onChange={ev => setMessage(ev.target.value)}
disabled={disabled}
@@ -282,8 +298,8 @@ const SendDialog = (props) => {
</DialogIconButton>
<TextField
margin="dense"
label="Tags"
placeholder="Comma-separated list of tags, e.g. warning, srv1-backup"
label={t("publish_dialog_tags_label")}
placeholder={t("publish_dialog_tags_placeholder")}
value={tags}
onChange={ev => setTags(ev.target.value)}
disabled={disabled}
@@ -294,11 +310,11 @@ const SendDialog = (props) => {
<FormControl
variant="standard"
margin="dense"
sx={{minWidth: 120, maxWidth: 200, flexGrow: 1}}
sx={{minWidth: 170, maxWidth: 300, flexGrow: 1}}
>
<InputLabel/>
<Select
label="Priority"
label={t("publish_dialog_priority_label")}
margin="dense"
value={priority}
onChange={(ev) => setPriority(ev.target.value)}
@@ -322,8 +338,8 @@ const SendDialog = (props) => {
}}>
<TextField
margin="dense"
label="Click URL"
placeholder="URL that is opened when notification is clicked"
label={t("publish_dialog_click_label")}
placeholder={t("publish_dialog_click_placeholder")}
value={clickUrl}
onChange={ev => setClickUrl(ev.target.value)}
disabled={disabled}
@@ -340,8 +356,8 @@ const SendDialog = (props) => {
}}>
<TextField
margin="dense"
label="Email"
placeholder="Address to forward the message to, e.g. phil@example.com"
label={t("publish_dialog_email_label")}
placeholder={t("publish_dialog_email_placeholder")}
value={email}
onChange={ev => setEmail(ev.target.value)}
disabled={disabled}
@@ -360,8 +376,8 @@ const SendDialog = (props) => {
}}>
<TextField
margin="dense"
label="Attachment URL"
placeholder="Attach file by URL, e.g. https://f-droid.org/F-Droid.apk"
label={t("publish_dialog_attach_label")}
placeholder={t("publish_dialog_attach_placeholder")}
value={attachUrl}
onChange={ev => {
const url = ev.target.value;
@@ -385,8 +401,8 @@ const SendDialog = (props) => {
/>
<TextField
margin="dense"
label="Filename"
placeholder="Attachment filename"
label={t("publish_dialog_filename_label")}
placeholder={t("publish_dialog_filename_placeholder")}
value={filename}
onChange={ev => {
setFilename(ev.target.value);
@@ -424,8 +440,12 @@ const SendDialog = (props) => {
}}>
<TextField
margin="dense"
label="Delay"
placeholder="Delay delivery, e.g. 1649029748, 30m, or tomorrow, 9am"
label={t("publish_dialog_delay_label")}
placeholder={t("publish_dialog_delay_placeholder", {
unixTimestamp: "1649029748",
relativeTime: "30m",
naturalLanguage: "tomorrow, 9am"
})}
value={delay}
onChange={ev => setDelay(ev.target.value)}
disabled={disabled}
@@ -436,33 +456,37 @@ const SendDialog = (props) => {
</ClosableRow>
}
<Typography variant="body1" sx={{marginTop: 2, marginBottom: 1}}>
Other features:
{t("publish_dialog_other_features")}
</Typography>
<div>
{!showClickUrl && <Chip clickable disabled={disabled} label="Click URL" onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showEmail && <Chip clickable disabled={disabled} label="Forward to email" onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label="Attach file by URL" onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label="Attach local file" onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showDelay && <Chip clickable disabled={disabled} label="Delay delivery" onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showTopicUrl && <Chip clickable disabled={disabled} label="Change topic" onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showClickUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_click_label")} onClick={() => setShowClickUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showEmail && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_email_label")} onClick={() => setShowEmail(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachUrl && !showAttachFile && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_url_label")} onClick={() => setShowAttachUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showAttachFile && !showAttachUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_attach_file_label")} onClick={() => handleAttachFileClick()} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showDelay && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_delay_label")} onClick={() => setShowDelay(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
{!showTopicUrl && <Chip clickable disabled={disabled} label={t("publish_dialog_chip_topic_label")} onClick={() => setShowTopicUrl(true)} sx={{marginRight: 1, marginBottom: 1}}/>}
</div>
<Typography variant="body1" sx={{marginTop: 1, marginBottom: 1}}>
For examples and a detailed description of all send features, please
refer to the <Link href="/docs" target="_blank">documentation</Link>.
<Trans
i18nKey="publish_dialog_details_examples_description"
components={{
docsLink: <Link href="https://ntfy.sh/docs" target="_blank" rel="noopener"/>
}}
/>
</Typography>
</DialogContent>
<DialogFooter status={status}>
{activeRequest && <Button onClick={() => activeRequest.abort()}>Cancel sending</Button>}
{activeRequest && <Button onClick={() => activeRequest.abort()}>{t("publish_dialog_button_cancel_sending")}</Button>}
{!activeRequest &&
<>
<FormControlLabel
label="Publish another"
label={t("publish_dialog_checkbox_publish_another")}
sx={{marginRight: 2}}
control={
<Checkbox size="small" checked={publishAnother} onChange={(ev) => setPublishAnother(ev.target.checked)} />
} />
<Button onClick={props.onClose}>Cancel</Button>
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>Send</Button>
<Button onClick={props.onClose}>{t("publish_dialog_button_cancel")}</Button>
<Button onClick={handleSubmit} disabled={!sendButtonEnabled}>{t("publish_dialog_button_send")}</Button>
</>
}
</DialogFooter>
@@ -506,11 +530,12 @@ const DialogIconButton = (props) => {
};
const AttachmentBox = (props) => {
const { t } = useTranslation();
const file = props.file;
return (
<>
<Typography variant="body1" sx={{marginTop: 2}}>
Attached file:
{t("publish_dialog_attached_file_title")}
</Typography>
<Box sx={{
display: 'flex',
@@ -523,6 +548,7 @@ const AttachmentBox = (props) => {
<ExpandingTextField
minWidth={140}
variant="body2"
placeholder={t("publish_dialog_attached_file_filename_placeholder")}
value={props.filename}
onChange={(ev) => props.onChangeFilename(ev.target.value)}
disabled={props.disabled}
@@ -568,7 +594,7 @@ const ExpandingTextField = (props) => {
</Typography>
<TextField
margin="dense"
placeholder="Attachment filename"
placeholder={props.placeholder}
value={props.value}
onChange={props.onChange}
type="text"
@@ -610,6 +636,7 @@ const DropArea = (props) => {
};
const DropBox = () => {
const { t } = useTranslation();
return (
<Box sx={{
position: 'absolute',
@@ -635,21 +662,13 @@ const DropBox = () => {
alignItems: "center",
}}
>
<Typography variant="h5">Drop file here</Typography>
<Typography variant="h5">{t("publish_dialog_drop_file_here")}</Typography>
</Box>
</Box>
);
}
const priorities = {
1: { label: "Min. priority", file: priority1 },
2: { label: "Low priority", file: priority2 },
3: { label: "Default priority", file: priority3 },
4: { label: "High priority", file: priority4 },
5: { label: "Max. priority", file: priority5 }
};
PublishDialog.OPEN_MODE_DEFAULT = "default";
PublishDialog.OPEN_MODE_DRAG = "drag";
SendDialog.OPEN_MODE_DEFAULT = "default";
SendDialog.OPEN_MODE_DRAG = "drag";
export default SendDialog;
export default PublishDialog;

View File

@@ -14,6 +14,7 @@ import userManager from "../app/UserManager";
import subscriptionManager from "../app/SubscriptionManager";
import poller from "../app/Poller";
import DialogFooter from "./DialogFooter";
import {useTranslation} from "react-i18next";
const publicBaseUrl = "https://ntfy.sh";
@@ -51,6 +52,7 @@ const SubscribeDialog = (props) => {
};
const SubscribePage = (props) => {
const { t } = useTranslation();
const [anotherServerVisible, setAnotherServerVisible] = useState(false);
const [errorText, setErrorText] = useState("");
const baseUrl = (anotherServerVisible) ? props.baseUrl : window.location.origin;
@@ -60,12 +62,12 @@ const SubscribePage = (props) => {
.filter(s => s !== window.location.origin);
const handleSubscribe = async () => {
const user = await userManager.get(baseUrl); // May be undefined
const username = (user) ? user.username : "anonymous";
const username = (user) ? user.username : t("subscribe_dialog_error_user_anonymous");
const success = await api.auth(baseUrl, topic, user);
if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
if (user) {
setErrorText(`User ${username} not authorized`);
setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return;
} else {
props.onNeedsLogin();
@@ -90,17 +92,16 @@ const SubscribePage = (props) => {
})();
return (
<>
<DialogTitle>Subscribe to topic</DialogTitle>
<DialogTitle>{t("subscribe_dialog_subscribe_title")}</DialogTitle>
<DialogContent>
<DialogContentText>
Topics may not be password-protected, so choose a name that's not easy to guess.
Once subscribed, you can PUT/POST notifications.
{t("subscribe_dialog_subscribe_description")}
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="topic"
placeholder="Topic name, e.g. phil_alerts"
placeholder={t("subscribe_dialog_subscribe_topic_placeholder")}
inputProps={{ maxLength: 64 }}
value={props.topic}
onChange={ev => props.setTopic(ev.target.value)}
@@ -111,7 +112,7 @@ const SubscribePage = (props) => {
<FormControlLabel
sx={{pt: 1}}
control={<Checkbox onChange={handleUseAnotherChanged}/>}
label="Use another server" />
label={t("subscribe_dialog_subscribe_use_another_label")} />
{anotherServerVisible && <Autocomplete
freeSolo
options={existingBaseUrls}
@@ -124,14 +125,15 @@ const SubscribePage = (props) => {
/>}
</DialogContent>
<DialogFooter status={errorText}>
<Button onClick={props.onCancel}>Cancel</Button>
<Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>Subscribe</Button>
<Button onClick={props.onCancel}>{t("subscribe_dialog_subscribe_button_cancel")}</Button>
<Button onClick={handleSubscribe} disabled={!subscribeButtonEnabled}>{t("subscribe_dialog_subscribe_button_subscribe")}</Button>
</DialogFooter>
</>
);
};
const LoginPage = (props) => {
const { t } = useTranslation();
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [errorText, setErrorText] = useState("");
@@ -142,7 +144,7 @@ const LoginPage = (props) => {
const success = await api.auth(baseUrl, topic, user);
if (!success) {
console.log(`[SubscribeDialog] Login to ${topicUrl(baseUrl, topic)} failed for user ${username}`);
setErrorText(`User ${username} not authorized`);
setErrorText(t("subscribe_dialog_error_user_not_authorized", { username: username }));
return;
}
console.log(`[SubscribeDialog] Successful login to ${topicUrl(baseUrl, topic)} for user ${username}`);
@@ -151,17 +153,16 @@ const LoginPage = (props) => {
};
return (
<>
<DialogTitle>Login required</DialogTitle>
<DialogTitle>{t("subscribe_dialog_login_title")}</DialogTitle>
<DialogContent>
<DialogContentText>
This topic is password-protected. Please enter username and
password to subscribe.
{t("subscribe_dialog_login_description")}
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="username"
label="Username, e.g. phil"
label={t("subscribe_dialog_login_username_label")}
value={username}
onChange={ev => setUsername(ev.target.value)}
type="text"
@@ -171,7 +172,7 @@ const LoginPage = (props) => {
<TextField
margin="dense"
id="password"
label="Password"
label={t("subscribe_dialog_login_password_label")}
type="password"
value={password}
onChange={ev => setPassword(ev.target.value)}
@@ -180,8 +181,8 @@ const LoginPage = (props) => {
/>
</DialogContent>
<DialogFooter status={errorText}>
<Button onClick={props.onBack}>Back</Button>
<Button onClick={handleLogin}>Login</Button>
<Button onClick={props.onBack}>{t("subscribe_dialog_login_button_back")}</Button>
<Button onClick={handleLogin}>{t("subscribe_dialog_login_button_login")}</Button>
</DialogFooter>
</>
);

View File

@@ -0,0 +1,29 @@
import i18n from 'i18next';
import Backend from 'i18next-http-backend';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
// Translations using i18next
// - Options: https://www.i18next.com/overview/configuration-options
// - Browser Language Detector: https://github.com/i18next/i18next-browser-languageDetector
// - HTTP Backend (load files via fetch): https://github.com/i18next/i18next-http-backend
//
// See example project here:
// https://github.com/i18next/react-i18next/tree/master/example/react
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
.init({
fallbackLng: 'en',
debug: true,
interpolation: {
escapeValue: false, // not needed for react as it escapes by default
},
backend: {
loadPath: '/static/langs/{{lng}}.json',
}
});
export default i18n;

View File

@@ -13,4 +13,5 @@ const routes = {
return `/${subscription.topic}`;
}
};
export default routes;

View File

@@ -1,8 +1,6 @@
import * as React from 'react';
import ReactDOM from 'react-dom';
import { createRoot } from 'react-dom/client';
import App from './components/App';
ReactDOM.render(
<App />,
document.querySelector('#root')
);
const root = createRoot(document.querySelector('#root'));
root.render(<App />);