diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..3bf2a12
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,3 @@
+dist
+*/node_modules
+Dockerfile*
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index b700532..2300230 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -5,3 +5,7 @@
c87549e71a10bc789eac8036078228f06e515a8e
ca5d736a7169eb6b4b0d849e061d5bf9565dcc53
2e27f58963feb9e4d1c573d4745d07770777fa7d
+
+# Run eslint (https://github.com/binwiederhier/ntfy/pull/748)
+f558b4dbe9bb5b9e0e87fada1215de2558353173
+8319f1cf26113167fb29fe12edaff5db74caf35f
diff --git a/.gitignore b/.gitignore
index b0c2d33..f695607 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
dist/
+dev-dist/
build/
.idea/
.vscode/
diff --git a/Dockerfile b/Dockerfile
index 7c2052e..feb813f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM alpine
+FROM r.batts.cloud/debian:testing
LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
LABEL org.opencontainers.image.url="https://ntfy.sh/"
diff --git a/Dockerfile-build b/Dockerfile-build
new file mode 100644
index 0000000..9c6d1bc
--- /dev/null
+++ b/Dockerfile-build
@@ -0,0 +1,54 @@
+FROM r.batts.cloud/golang:1.19 as builder
+
+ARG VERSION=dev
+ARG COMMIT=unknown
+
+RUN apt-get update
+RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash
+RUN apt-get install -y \
+ build-essential \
+ nodejs \
+ python3-pip \
+ python3-venv
+
+WORKDIR /app
+ADD Makefile .
+
+# docs
+ADD ./requirements.txt .
+RUN make docs-deps
+ADD ./mkdocs.yml .
+ADD ./docs ./docs
+RUN make docs-build
+
+# web
+ADD ./web/package.json ./web/package-lock.json ./web/
+RUN make web-deps
+ADD ./web ./web
+RUN make web-build
+
+# cli & server
+ADD go.mod go.sum main.go ./
+ADD ./client ./client
+ADD ./cmd ./cmd
+ADD ./log ./log
+ADD ./server ./server
+ADD ./user ./user
+ADD ./util ./util
+RUN make VERSION=$VERSION COMMIT=$COMMIT cli-linux-server
+
+FROM r.batts.cloud/debian:testing
+
+LABEL org.opencontainers.image.authors="philipp.heckel@gmail.com"
+LABEL org.opencontainers.image.url="https://ntfy.sh/"
+LABEL org.opencontainers.image.documentation="https://docs.ntfy.sh/"
+LABEL org.opencontainers.image.source="https://github.com/binwiederhier/ntfy"
+LABEL org.opencontainers.image.vendor="Philipp C. Heckel"
+LABEL org.opencontainers.image.licenses="Apache-2.0, GPL-2.0"
+LABEL org.opencontainers.image.title="ntfy"
+LABEL org.opencontainers.image.description="Send push notifications to your phone or desktop using PUT/POST"
+
+COPY --from=builder /app/dist/ntfy_linux_server/ntfy /usr/bin/ntfy
+
+EXPOSE 80/tcp
+ENTRYPOINT ["ntfy"]
diff --git a/Makefile b/Makefile
index 6786acb..440bfa6 100644
--- a/Makefile
+++ b/Makefile
@@ -31,12 +31,16 @@ help:
@echo " make cli-darwin-server - Build client & server (no GoReleaser, current arch, macOS)"
@echo " make cli-client - Build client only (no GoReleaser, current arch, Linux/macOS/Windows)"
@echo
+ @echo "Build dev Docker:"
+ @echo " make docker-dev - Build client & server for current architecture using Docker only"
+ @echo
@echo "Build web app:"
@echo " make web - Build the web app"
@echo " make web-deps - Install web app dependencies (npm install the universe)"
@echo " make web-build - Actually build the web app"
- @echo " make web-format - Run prettier on the web app
- @echo " make web-format-check - Run prettier on the web app, but don't change anything
+ @echo " make web-lint - Run eslint on the web app"
+ @echo " make web-format - Run prettier on the web app"
+ @echo " make web-format-check - Run prettier on the web app, but don't change anything"
@echo
@echo "Build documentation:"
@echo " make docs - Build the documentation"
@@ -82,23 +86,33 @@ build: web docs cli
update: web-deps-update cli-deps-update docs-deps-update
docker pull alpine
+docker-dev:
+ docker build \
+ --file ./Dockerfile-build \
+ --tag binwiederhier/ntfy:$(VERSION) \
+ --tag binwiederhier/ntfy:dev \
+ --build-arg VERSION=$(VERSION) \
+ --build-arg COMMIT=$(COMMIT) \
+ ./
+
# Ubuntu-specific
build-deps-ubuntu:
- sudo apt update
- sudo apt install -y \
+ sudo apt-get update
+ sudo apt-get install -y \
curl \
gcc-aarch64-linux-gnu \
gcc-arm-linux-gnueabi \
jq
- which pip3 || sudo apt install -y python3-pip
+ which pip3 || sudo apt-get install -y python3-pip
# Documentation
docs: docs-deps docs-build
-docs-build: .PHONY
- @if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
+docs-build: venv .PHONY
+ @. venv/bin/activate && \
+ if ! /bin/echo -e "import sys\nif sys.version_info < (3,8):\n exit(1)" | python3; then \
if which python3.8; then \
echo "python3.8 $(shell which mkdocs) build"; \
python3.8 $(shell which mkdocs) build; \
@@ -111,10 +125,15 @@ docs-build: .PHONY
mkdocs build; \
fi
-docs-deps: .PHONY
+venv:
+ python3 -m venv ./venv
+
+docs-deps: venv .PHONY
+ . venv/bin/activate && \
pip3 install -r requirements.txt
-docs-deps-update: .PHONY
+docs-deps-update: venv .PHONY
+ . venv/bin/activate && \
pip3 install -r requirements.txt --upgrade
@@ -129,8 +148,7 @@ web-build:
&& rm -rf ../server/site \
&& mv build ../server/site \
&& rm \
- ../server/site/config.js \
- ../server/site/asset-manifest.json
+ ../server/site/config.js
web-deps:
cd web && npm install
@@ -145,6 +163,9 @@ web-format:
web-format-check:
cd web && npm run format:check
+web-lint:
+ cd web && npm run lint
+
# Main server/client build
cli: cli-deps
@@ -233,7 +254,7 @@ cli-build-results:
# Test/check targets
-check: test web-format-check fmt-check vet lint staticcheck
+check: test web-format-check fmt-check vet web-lint lint staticcheck
test: .PHONY
go test $(shell go list ./... | grep -vE 'ntfy/(test|examples|tools)')
diff --git a/client/client.go b/client/client.go
index b744fa1..93cf7da 100644
--- a/client/client.go
+++ b/client/client.go
@@ -11,23 +11,25 @@ import (
"heckel.io/ntfy/util"
"io"
"net/http"
+ "regexp"
"strings"
"sync"
"time"
)
-// Event type constants
const (
- MessageEvent = "message"
- KeepaliveEvent = "keepalive"
- OpenEvent = "open"
- PollRequestEvent = "poll_request"
+ // MessageEvent identifies a message event
+ MessageEvent = "message"
)
const (
maxResponseBytes = 4096
)
+var (
+ topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // Same as in server/server.go
+)
+
// Client is the ntfy client that can be used to publish and subscribe to ntfy topics
type Client struct {
Messages chan *Message
@@ -96,8 +98,14 @@ func (c *Client) Publish(topic, message string, options ...PublishOption) (*Mess
// To pass title, priority and tags, check out WithTitle, WithPriority, WithTagsList, WithDelay, WithNoCache,
// WithNoFirebase, and the generic WithHeader.
func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishOption) (*Message, error) {
- topicURL := c.expandTopicURL(topic)
- req, _ := http.NewRequest("POST", topicURL, body)
+ topicURL, err := c.expandTopicURL(topic)
+ if err != nil {
+ return nil, err
+ }
+ req, err := http.NewRequest("POST", topicURL, body)
+ if err != nil {
+ return nil, err
+ }
for _, option := range options {
if err := option(req); err != nil {
return nil, err
@@ -133,11 +141,14 @@ func (c *Client) PublishReader(topic string, body io.Reader, options ...PublishO
// By default, all messages will be returned, but you can change this behavior using a SubscribeOption.
// See WithSince, WithSinceAll, WithSinceUnixTime, WithScheduled, and the generic WithQueryParam.
func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, error) {
+ topicURL, err := c.expandTopicURL(topic)
+ if err != nil {
+ return nil, err
+ }
ctx := context.Background()
messages := make([]*Message, 0)
msgChan := make(chan *Message)
errChan := make(chan error)
- topicURL := c.expandTopicURL(topic)
log.Debug("%s Polling from topic", util.ShortTopicURL(topicURL))
options = append(options, WithPoll())
go func() {
@@ -166,15 +177,18 @@ func (c *Client) Poll(topic string, options ...SubscribeOption) ([]*Message, err
// Example:
//
// c := client.New(client.NewConfig())
-// subscriptionID := c.Subscribe("mytopic")
+// subscriptionID, _ := c.Subscribe("mytopic")
// for m := range c.Messages {
// fmt.Printf("New message: %s", m.Message)
// }
-func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
+func (c *Client) Subscribe(topic string, options ...SubscribeOption) (string, error) {
+ topicURL, err := c.expandTopicURL(topic)
+ if err != nil {
+ return "", err
+ }
c.mu.Lock()
defer c.mu.Unlock()
subscriptionID := util.RandomString(10)
- topicURL := c.expandTopicURL(topic)
log.Debug("%s Subscribing to topic", util.ShortTopicURL(topicURL))
ctx, cancel := context.WithCancel(context.Background())
c.subscriptions[subscriptionID] = &subscription{
@@ -183,7 +197,7 @@ func (c *Client) Subscribe(topic string, options ...SubscribeOption) string {
cancel: cancel,
}
go handleSubscribeConnLoop(ctx, c.Messages, topicURL, subscriptionID, options...)
- return subscriptionID
+ return subscriptionID, nil
}
// Unsubscribe unsubscribes from a topic that has been previously subscribed to using the unique
@@ -199,31 +213,16 @@ func (c *Client) Unsubscribe(subscriptionID string) {
sub.cancel()
}
-// UnsubscribeAll unsubscribes from a topic that has been previously subscribed with Subscribe.
-// If there are multiple subscriptions matching the topic, all of them are unsubscribed from.
-//
-// A topic can be either a full URL (e.g. https://myhost.lan/mytopic), a short URL which is then prepended https://
-// (e.g. myhost.lan -> https://myhost.lan), or a short name which is expanded using the default host in the
-// config (e.g. mytopic -> https://ntfy.sh/mytopic).
-func (c *Client) UnsubscribeAll(topic string) {
- c.mu.Lock()
- defer c.mu.Unlock()
- topicURL := c.expandTopicURL(topic)
- for _, sub := range c.subscriptions {
- if sub.topicURL == topicURL {
- delete(c.subscriptions, sub.ID)
- sub.cancel()
- }
- }
-}
-
-func (c *Client) expandTopicURL(topic string) string {
+func (c *Client) expandTopicURL(topic string) (string, error) {
if strings.HasPrefix(topic, "http://") || strings.HasPrefix(topic, "https://") {
- return topic
+ return topic, nil
} else if strings.Contains(topic, "/") {
- return fmt.Sprintf("https://%s", topic)
+ return fmt.Sprintf("https://%s", topic), nil
}
- return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic)
+ if !topicRegex.MatchString(topic) {
+ return "", fmt.Errorf("invalid topic name: %s", topic)
+ }
+ return fmt.Sprintf("%s/%s", c.config.DefaultHost, topic), nil
}
func handleSubscribeConnLoop(ctx context.Context, msgChan chan *Message, topicURL, subcriptionID string, options ...SubscribeOption) {
diff --git a/client/client_test.go b/client/client_test.go
index a71ea5c..f0b15a3 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -21,7 +21,7 @@ func TestClient_Publish_Subscribe(t *testing.T) {
defer test.StopServer(t, s, port)
c := client.New(newTestConfig(port))
- subscriptionID := c.Subscribe("mytopic")
+ subscriptionID, _ := c.Subscribe("mytopic")
time.Sleep(time.Second)
msg, err := c.Publish("mytopic", "some message")
diff --git a/cmd/subscribe.go b/cmd/subscribe.go
index 2691e6a..c85c468 100644
--- a/cmd/subscribe.go
+++ b/cmd/subscribe.go
@@ -72,7 +72,7 @@ ntfy subscribe TOPIC COMMAND
$NTFY_TITLE $title, $t Message title
$NTFY_PRIORITY $priority, $prio, $p Message priority (1=min, 5=max)
$NTFY_TAGS $tags, $tag, $ta Message tags (comma separated list)
- $NTFY_RAW $raw Raw JSON message
+ $NTFY_RAW $raw Raw JSON message
Examples:
ntfy sub mytopic 'notify-send "$m"' # Execute command for incoming messages
@@ -194,7 +194,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
topicOptions = append(topicOptions, auth)
}
- subscriptionID := cl.Subscribe(s.Topic, topicOptions...)
+ subscriptionID, err := cl.Subscribe(s.Topic, topicOptions...)
+ if err != nil {
+ return err
+ }
if s.Command != "" {
cmds[subscriptionID] = s.Command
} else if conf.DefaultCommand != "" {
@@ -204,7 +207,10 @@ func doSubscribe(c *cli.Context, cl *client.Client, conf *client.Config, topic,
}
}
if topic != "" {
- subscriptionID := cl.Subscribe(topic, options...)
+ subscriptionID, err := cl.Subscribe(topic, options...)
+ if err != nil {
+ return err
+ }
cmds[subscriptionID] = command
}
for m := range cl.Messages {
diff --git a/docs/develop.md b/docs/develop.md
index a53c503..baab3f3 100644
--- a/docs/develop.md
+++ b/docs/develop.md
@@ -163,6 +163,15 @@ $ make release-snapshot
During development, you may want to be more picky and build only certain things. Here are a few examples.
+### Build a Docker image only for Linux
+
+This is useful to test the final build with web app, docs, and server without any dependencies locally
+
+``` shell
+$ make docker-dev
+$ docker run --rm -p 80:80 binwiederhier/ntfy:dev serve
+```
+
### Build the ntfy binary
To build only the `ntfy` binary **without the web app or documentation**, use the `make cli-...` targets:
diff --git a/docs/releases.md b/docs/releases.md
index 523f46f..0e93b67 100644
--- a/docs/releases.md
+++ b/docs/releases.md
@@ -1222,8 +1222,15 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
### ntfy server v2.6.0 (UNRELEASED)
-**Bug fixes + maintenance:**
+**Bug fixes:**
* Support encoding any header as RFC 2047 ([#737](https://github.com/binwiederhier/ntfy/issues/737), thanks to [@cfouche3005](https://github.com/cfouche3005) for reporting)
+* Do not forward poll requests for UnifiedPush messages (no ticket, thanks to NoName for reporting)
+* Fix `ntfy pub %` segfaulting ([#760](https://github.com/binwiederhier/ntfy/issues/760), thanks to [@clesmian](https://github.com/clesmian) for reporting)
+
+**Maintenance:**
+
* Improved GitHub Actions flow ([#745](https://github.com/binwiederhier/ntfy/pull/745), thanks to [@nimbleghost](https://github.com/nimbleghost))
* Web: Add JS formatter "prettier" ([#746](https://github.com/binwiederhier/ntfy/pull/746), thanks to [@nimbleghost](https://github.com/nimbleghost))
+* Web: Add eslint with eslint-config-airbnb ([#748](https://github.com/binwiederhier/ntfy/pull/748), thanks to [@nimbleghost](https://github.com/nimbleghost))
+* Web: Switch to Vite ([#749](https://github.com/binwiederhier/ntfy/pull/749), thanks to [@nimbleghost](https://github.com/nimbleghost))
diff --git a/server/server.go b/server/server.go
index 179359d..d2fac01 100644
--- a/server/server.go
+++ b/server/server.go
@@ -760,7 +760,7 @@ func (s *Server) handlePublishInternal(r *http.Request, v *visitor) (*message, e
if s.config.TwilioAccount != "" && call != "" {
go s.callPhone(v, r, m, call)
}
- if s.config.UpstreamBaseURL != "" {
+ if s.config.UpstreamBaseURL != "" && !unifiedpush { // UP messages are not sent to upstream
go s.forwardPollRequest(v, m)
}
} else {
@@ -963,10 +963,6 @@ func (s *Server) parsePublishParams(r *http.Request, m *message) (cache bool, fi
return false, false, "", "", false, errHTTPBadRequestActionsInvalid.Wrap(e.Error())
}
}
- contentType, markdown := readParam(r, "content-type"), readBoolParam(r, false, "x-markdown", "markdown", "md")
- if markdown || strings.ToLower(contentType) == "text/markdown" {
- m.ContentType = "text/markdown"
- }
unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too!
if unifiedpush {
firebase = false
diff --git a/server/server_firebase.go b/server/server_firebase.go
index b8158d2..6318b98 100644
--- a/server/server_firebase.go
+++ b/server/server_firebase.go
@@ -144,18 +144,17 @@ func toFirebaseMessage(m *message, auther user.Auther) (*messaging.Message, erro
}
if allowForward {
data = map[string]string{
- "id": m.ID,
- "time": fmt.Sprintf("%d", m.Time),
- "event": m.Event,
- "topic": m.Topic,
- "priority": fmt.Sprintf("%d", m.Priority),
- "tags": strings.Join(m.Tags, ","),
- "click": m.Click,
- "icon": m.Icon,
- "title": m.Title,
- "message": m.Message,
- "content_type": m.ContentType,
- "encoding": m.Encoding,
+ "id": m.ID,
+ "time": fmt.Sprintf("%d", m.Time),
+ "event": m.Event,
+ "topic": m.Topic,
+ "priority": fmt.Sprintf("%d", m.Priority),
+ "tags": strings.Join(m.Tags, ","),
+ "click": m.Click,
+ "icon": m.Icon,
+ "title": m.Title,
+ "message": m.Message,
+ "encoding": m.Encoding,
}
if len(m.Actions) > 0 {
actions, err := json.Marshal(m.Actions)
diff --git a/server/server_test.go b/server/server_test.go
index fe84b85..d7c4a7c 100644
--- a/server/server_test.go
+++ b/server/server_test.go
@@ -219,7 +219,7 @@ func TestServer_StaticSites(t *testing.T) {
rr = request(t, s, "GET", "/mytopic", "", nil)
require.Equal(t, 200, rr.Code)
- require.Contains(t, rr.Body.String(), ``)
+ require.Contains(t, rr.Body.String(), ``)
rr = request(t, s, "GET", "/docs", "", nil)
require.Equal(t, 301, rr.Code)
@@ -2559,6 +2559,29 @@ func TestServer_UpstreamBaseURL_With_Access_Token_Success(t *testing.T) {
})
}
+func TestServer_UpstreamBaseURL_DoNotForwardUnifiedPush(t *testing.T) {
+ upstreamServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ t.Fatal("UnifiedPush messages should not be forwarded")
+ }))
+ defer upstreamServer.Close()
+
+ c := newTestConfigWithAuthFile(t)
+ c.BaseURL = "http://myserver.internal"
+ c.UpstreamBaseURL = upstreamServer.URL
+ s := newTestServer(t, c)
+
+ // Send UP message, this should not forward to upstream server
+ response := request(t, s, "PUT", "/mytopic?up=1", `hi there`, nil)
+ require.Equal(t, 200, response.Code)
+ m := toMessage(t, response.Body.String())
+ require.NotEmpty(t, m.ID)
+ require.Equal(t, "hi there", m.Message)
+
+ // Forwarding is done asynchronously, so wait a bit.
+ // This ensures that the t.Fatal above is actually not triggered.
+ time.Sleep(500 * time.Millisecond)
+}
+
func newTestConfig(t *testing.T) *Config {
conf := NewConfig()
conf.BaseURL = "http://127.0.0.1:12345"
diff --git a/server/types.go b/server/types.go
index 7798a65..9e4ff55 100644
--- a/server/types.go
+++ b/server/types.go
@@ -24,24 +24,23 @@ const (
// message represents a message published to a topic
type message struct {
- ID string `json:"id"` // Random message ID
- Time int64 `json:"time"` // Unix time in seconds
- Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
- Event string `json:"event"` // One of the above
- Topic string `json:"topic"`
- Title string `json:"title,omitempty"`
- Message string `json:"message,omitempty"`
- Priority int `json:"priority,omitempty"`
- Tags []string `json:"tags,omitempty"`
- Click string `json:"click,omitempty"`
- Icon string `json:"icon,omitempty"`
- Actions []*action `json:"actions,omitempty"`
- Attachment *attachment `json:"attachment,omitempty"`
- PollID string `json:"poll_id,omitempty"`
- ContentType string `json:"content_type,omitempty"` // text/plain by default (if empty), or text/markdown
- Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
- Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
- User string `json:"-"` // Username of the uploader, used to associated attachments
+ ID string `json:"id"` // Random message ID
+ Time int64 `json:"time"` // Unix time in seconds
+ Expires int64 `json:"expires,omitempty"` // Unix time in seconds (not required for open/keepalive)
+ Event string `json:"event"` // One of the above
+ Topic string `json:"topic"`
+ Title string `json:"title,omitempty"`
+ Message string `json:"message,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Click string `json:"click,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ Actions []*action `json:"actions,omitempty"`
+ Attachment *attachment `json:"attachment,omitempty"`
+ PollID string `json:"poll_id,omitempty"`
+ Encoding string `json:"encoding,omitempty"` // empty for raw UTF-8, or "base64" for encoded bytes
+ Sender netip.Addr `json:"-"` // IP address of uploader, used for rate limiting
+ User string `json:"-"` // Username of the uploader, used to associated attachments
}
func (m *message) Context() log.Context {
diff --git a/web/.eslintignore b/web/.eslintignore
new file mode 100644
index 0000000..29c9584
--- /dev/null
+++ b/web/.eslintignore
@@ -0,0 +1 @@
+src/app/emojis.js
\ No newline at end of file
diff --git a/web/.eslintrc b/web/.eslintrc
new file mode 100644
index 0000000..adf6613
--- /dev/null
+++ b/web/.eslintrc
@@ -0,0 +1,37 @@
+{
+ "extends": ["airbnb", "prettier"],
+ "env": {
+ "browser": true
+ },
+ "globals": {
+ "config": "readonly"
+ },
+ "parserOptions": {
+ "ecmaVersion": 2023
+ },
+ "rules": {
+ "no-console": "off",
+ "class-methods-use-this": "off",
+ "func-style": ["error", "expression"],
+ "no-restricted-syntax": ["error", "ForInStatement", "LabeledStatement", "WithStatement"],
+ "no-await-in-loop": "error",
+ "import/no-cycle": "warn",
+ "react/prop-types": "off",
+ "react/destructuring-assignment": "off",
+ "react/jsx-no-useless-fragment": "off",
+ "react/jsx-props-no-spreading": "off",
+ "react/jsx-no-duplicate-props": [
+ "error",
+ {
+ "ignoreCase": false // For 's [iI]nputProps
+ }
+ ],
+ "react/function-component-definition": [
+ "error",
+ {
+ "namedComponents": "arrow-function",
+ "unnamedComponents": "arrow-function"
+ }
+ ]
+ }
+}
diff --git a/web/.prettierignore b/web/.prettierignore
index 1465272..802cdb8 100644
--- a/web/.prettierignore
+++ b/web/.prettierignore
@@ -1,3 +1,4 @@
build/
dist/
public/static/langs/
+src/app/emojis.js
diff --git a/web/public/index.html b/web/index.html
similarity index 80%
rename from web/public/index.html
rename to web/index.html
index e8c7f8a..c146e64 100644
--- a/web/public/index.html
+++ b/web/index.html
@@ -15,7 +15,7 @@
-
+
@@ -26,15 +26,15 @@
property="og:description"
content="ntfy lets you send push notifications via scripts from any computer or phone. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy."
/>
-
+
-
-
+
+
-
+
+