Embed new web UI into server
1
.gitignore
vendored
|
@ -2,6 +2,7 @@ dist/
|
||||||
build/
|
build/
|
||||||
.idea/
|
.idea/
|
||||||
server/docs/
|
server/docs/
|
||||||
|
server/site/
|
||||||
tools/fbsend/fbsend
|
tools/fbsend/fbsend
|
||||||
playground/
|
playground/
|
||||||
*.iml
|
*.iml
|
||||||
|
|
29
Makefile
|
@ -44,6 +44,28 @@ docs-deps: .PHONY
|
||||||
docs: docs-deps
|
docs: docs-deps
|
||||||
mkdocs build
|
mkdocs build
|
||||||
|
|
||||||
|
|
||||||
|
# Web app
|
||||||
|
|
||||||
|
web-deps:
|
||||||
|
cd web && npm install
|
||||||
|
|
||||||
|
web-build:
|
||||||
|
cd web \
|
||||||
|
&& npm run build \
|
||||||
|
&& mv build/index.html build/app.html \
|
||||||
|
&& rm -rf ../server/site \
|
||||||
|
&& mv build ../server/site \
|
||||||
|
&& rm \
|
||||||
|
../server/site/precache* \
|
||||||
|
../server/site/service-worker.js \
|
||||||
|
../server/site/asset-manifest.json \
|
||||||
|
../server/site/static/js/*.js.map \
|
||||||
|
../server/site/static/js/*.js.LICENSE.txt
|
||||||
|
|
||||||
|
web: web-deps web-build
|
||||||
|
|
||||||
|
|
||||||
# Test/check targets
|
# Test/check targets
|
||||||
|
|
||||||
check: test fmt-check vet lint staticcheck
|
check: test fmt-check vet lint staticcheck
|
||||||
|
@ -94,7 +116,7 @@ staticcheck: .PHONY
|
||||||
|
|
||||||
# Building targets
|
# Building targets
|
||||||
|
|
||||||
build-deps: docs
|
build-deps: docs web
|
||||||
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
|
which arm-linux-gnueabi-gcc || { echo "ERROR: ARMv6/v7 cross compiler not installed. On Ubuntu, run: apt install gcc-arm-linux-gnueabi"; exit 1; }
|
||||||
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
which aarch64-linux-gnu-gcc || { echo "ERROR: ARM64 cross compiler not installed. On Ubuntu, run: apt install gcc-aarch64-linux-gnu"; exit 1; }
|
||||||
|
|
||||||
|
@ -105,8 +127,9 @@ build-snapshot: build-deps
|
||||||
goreleaser build --snapshot --rm-dist --debug
|
goreleaser build --snapshot --rm-dist --debug
|
||||||
|
|
||||||
build-simple: clean
|
build-simple: clean
|
||||||
mkdir -p dist/ntfy_linux_amd64 server/docs
|
mkdir -p dist/ntfy_linux_amd64 server/docs server/site
|
||||||
touch server/docs/dummy
|
touch server/docs/index.html
|
||||||
|
touch server/site/app.html
|
||||||
export CGO_ENABLED=1
|
export CGO_ENABLED=1
|
||||||
go build \
|
go build \
|
||||||
-o dist/ntfy_linux_amd64/ntfy \
|
-o dist/ntfy_linux_amd64/ntfy \
|
||||||
|
|
|
@ -13,7 +13,6 @@ import (
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
"heckel.io/ntfy/auth"
|
"heckel.io/ntfy/auth"
|
||||||
"heckel.io/ntfy/util"
|
"heckel.io/ntfy/util"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
@ -63,6 +62,7 @@ var (
|
||||||
// If changed, don't forget to update Android App and auth_sqlite.go
|
// If changed, don't forget to update Android App and auth_sqlite.go
|
||||||
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
|
topicRegex = regexp.MustCompile(`^[-_A-Za-z0-9]{1,64}$`) // No /!
|
||||||
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
|
topicPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}$`) // Regex must match JS & Android app!
|
||||||
|
extTopicPathRegex = regexp.MustCompile(`^/[^/]+\.[^/]+/[-_A-Za-z0-9]{1,64}$`) // Extended topic path, for web-app, e.g. /example.com/mytopic
|
||||||
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
|
jsonPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/json$`)
|
||||||
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
|
ssePathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/sse$`)
|
||||||
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
rawPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
|
||||||
|
@ -73,23 +73,18 @@ var (
|
||||||
staticRegex = regexp.MustCompile(`^/static/.+`)
|
staticRegex = regexp.MustCompile(`^/static/.+`)
|
||||||
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
docsRegex = regexp.MustCompile(`^/docs(|/.*)$`)
|
||||||
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
|
||||||
disallowedTopics = []string{"docs", "static", "file"} // If updated, also update in Android app
|
disallowedTopics = []string{"docs", "static", "file", "app", "settings"} // If updated, also update in Android app
|
||||||
attachURLRegex = regexp.MustCompile(`^https?://`)
|
attachURLRegex = regexp.MustCompile(`^https?://`)
|
||||||
|
|
||||||
templateFnMap = template.FuncMap{
|
|
||||||
"durationToHuman": util.DurationToHuman,
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:embed "index.gohtml"
|
|
||||||
indexSource string
|
|
||||||
indexTemplate = template.Must(template.New("index").Funcs(templateFnMap).Parse(indexSource))
|
|
||||||
|
|
||||||
//go:embed "example.html"
|
//go:embed "example.html"
|
||||||
exampleSource string
|
exampleSource string
|
||||||
|
|
||||||
//go:embed static
|
//go:embed site
|
||||||
webStaticFs embed.FS
|
webFs embed.FS
|
||||||
webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
|
webFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webFs}
|
||||||
|
webSiteDir = "/site"
|
||||||
|
webHomeIndex = "/home.html" // Landing page, only if "web-index: home"
|
||||||
|
webAppIndex = "/app.html" // React app
|
||||||
|
|
||||||
//go:embed docs
|
//go:embed docs
|
||||||
docsStaticFs embed.FS
|
docsStaticFs embed.FS
|
||||||
|
@ -284,8 +279,6 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.limitRequests(s.handleFile)(w, r, v)
|
return s.limitRequests(s.handleFile)(w, r, v)
|
||||||
} else if r.Method == http.MethodOptions {
|
} else if r.Method == http.MethodOptions {
|
||||||
return s.handleOptions(w, r)
|
return s.handleOptions(w, r)
|
||||||
} else if r.Method == http.MethodGet && topicPathRegex.MatchString(r.URL.Path) {
|
|
||||||
return s.handleTopic(w, r)
|
|
||||||
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
} else if (r.Method == http.MethodPut || r.Method == http.MethodPost) && topicPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
|
return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && publishPathRegex.MatchString(r.URL.Path) {
|
||||||
|
@ -300,15 +293,15 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request, v *visit
|
||||||
return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
|
return s.limitRequests(s.authRead(s.handleSubscribeWS))(w, r, v)
|
||||||
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
|
} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
|
||||||
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
|
return s.limitRequests(s.authRead(s.handleTopicAuth))(w, r, v)
|
||||||
|
} else if r.Method == http.MethodGet && (topicPathRegex.MatchString(r.URL.Path) || extTopicPathRegex.MatchString(r.URL.Path)) {
|
||||||
|
return s.handleTopic(w, r)
|
||||||
}
|
}
|
||||||
return errHTTPNotFound
|
return errHTTPNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
|
||||||
return indexTemplate.Execute(w, &indexPage{
|
r.URL.Path = webHomeIndex
|
||||||
Topic: r.URL.Path[1:],
|
return s.handleStatic(w, r)
|
||||||
CacheDuration: s.config.CacheDuration,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
|
||||||
|
@ -319,7 +312,8 @@ func (s *Server) handleTopic(w http.ResponseWriter, r *http.Request) error {
|
||||||
_, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n")
|
_, err := io.WriteString(w, `{"unifiedpush":{"version":1}}`+"\n")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.handleHome(w, r)
|
r.URL.Path = webAppIndex
|
||||||
|
return s.handleStatic(w, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
func (s *Server) handleEmpty(_ http.ResponseWriter, _ *http.Request, _ *visitor) error {
|
||||||
|
@ -339,7 +333,8 @@ func (s *Server) handleExample(w http.ResponseWriter, _ *http.Request) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
|
func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error {
|
||||||
http.FileServer(http.FS(webStaticFsCached)).ServeHTTP(w, r)
|
r.URL.Path = webSiteDir + r.URL.Path
|
||||||
|
http.FileServer(http.FS(webFsCached)).ServeHTTP(w, r)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +0,0 @@
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z"/></svg>
|
|
Before Width: | Height: | Size: 268 B |
Before Width: | Height: | Size: 4.6 KiB |
Before Width: | Height: | Size: 3.5 KiB |
|
@ -1,44 +0,0 @@
|
||||||
# Create React App example
|
|
||||||
|
|
||||||
## How to use
|
|
||||||
|
|
||||||
Download the example [or clone the repo](https://github.com/mui/material-ui):
|
|
||||||
|
|
||||||
<!-- #default-branch-switch -->
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl https://codeload.github.com/mui/material-ui/tar.gz/master | tar -xz --strip=2 material-ui-master/examples/create-react-app
|
|
||||||
cd create-react-app
|
|
||||||
```
|
|
||||||
|
|
||||||
Install it and run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
npm start
|
|
||||||
```
|
|
||||||
|
|
||||||
or:
|
|
||||||
|
|
||||||
<!-- #default-branch-switch -->
|
|
||||||
|
|
||||||
[![Edit on CodeSandbox](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/create-react-app)
|
|
||||||
|
|
||||||
<!-- #default-branch-switch -->
|
|
||||||
|
|
||||||
[![Edit on StackBlitz](https://developer.stackblitz.com/img/open_in_stackblitz.svg)](https://stackblitz.com/github/mui/material-ui/tree/master/examples/create-react-app)
|
|
||||||
|
|
||||||
## The idea behind the example
|
|
||||||
|
|
||||||
<!-- #default-branch-switch -->
|
|
||||||
|
|
||||||
This example demonstrates how you can use [Create React App](https://github.com/facebookincubator/create-react-app).
|
|
||||||
It includes `@mui/material` and its peer dependencies, including `emotion`, the default style engine in MUI v5.
|
|
||||||
If you prefer, you can [use styled-components instead](https://mui.com/guides/interoperability/#styled-components).
|
|
||||||
|
|
||||||
## What's next?
|
|
||||||
|
|
||||||
<!-- #default-branch-switch -->
|
|
||||||
|
|
||||||
You now have a working example project.
|
|
||||||
You can head back to the documentation, continuing browsing it from the [templates](https://mui.com/getting-started/templates/) section.
|
|
|
@ -1,3 +0,0 @@
|
||||||
var config = {
|
|
||||||
defaultBaseUrl: 'https://ntfy.sh'
|
|
||||||
};
|
|
Before Width: | Height: | Size: 3.8 KiB |
|
@ -1,11 +1,10 @@
|
||||||
{{- /*gotype: heckel.io/ntfy/server.indexPage*/ -}}
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
|
||||||
<title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
|
<title>ntfy.sh | Send push notifications to your phone via PUT/POST</title>
|
||||||
<link rel="stylesheet" href="static/css/app.css" type="text/css">
|
<link rel="stylesheet" href="static/css/home.css" type="text/css">
|
||||||
|
|
||||||
<!-- Mobile view -->
|
<!-- Mobile view -->
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
|
||||||
|
@ -37,9 +36,9 @@
|
||||||
<div id="name">ntfy</div>
|
<div id="name">ntfy</div>
|
||||||
<ol>
|
<ol>
|
||||||
<li><a href="docs/">Getting started</a></li>
|
<li><a href="docs/">Getting started</a></li>
|
||||||
|
<li><a href="app">Web app</a></li>
|
||||||
<li><a href="docs/subscribe/phone/">Android/iOS</a></li>
|
<li><a href="docs/subscribe/phone/">Android/iOS</a></li>
|
||||||
<li><a href="docs/publish/">API</a></li>
|
<li><a href="docs/publish/">API</a></li>
|
||||||
<li><a href="docs/install/">Self-hosting</a></li>
|
|
||||||
<li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
|
<li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
|
@ -90,7 +89,7 @@
|
||||||
Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
|
Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
|
||||||
</p>
|
</p>
|
||||||
<figure>
|
<figure>
|
||||||
<img src="static/img/priority-notification.png" style="max-height: 200px"/>
|
<img src="static/img/screenshot-phone-popover.png" style="max-height: 200px"/>
|
||||||
<figcaption>Urgent notification with pop-over</figcaption>
|
<figcaption>Urgent notification with pop-over</figcaption>
|
||||||
</figure>
|
</figure>
|
||||||
|
|
||||||
|
@ -170,7 +169,6 @@
|
||||||
<center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
|
<center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
|
||||||
</div>
|
</div>
|
||||||
<div id="lightbox" class="lightbox"></div>
|
<div id="lightbox" class="lightbox"></div>
|
||||||
<script src="static/js/emoji.js"></script>
|
<script src="static/js/home.js"></script>
|
||||||
<script src="static/js/app.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -20,18 +20,15 @@
|
||||||
<!-- Previews in Google, Slack, WhatsApp, etc. -->
|
<!-- Previews in Google, Slack, WhatsApp, etc. -->
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:locale" content="en_US" />
|
<meta property="og:locale" content="en_US" />
|
||||||
<meta property="og:site_name" content="ntfy.sh" />
|
<meta property="og:site_name" content="ntfy web" />
|
||||||
<meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop via PUT/POST" />
|
<meta property="og:title" content="ntfy web | Web app to receive push notifications from scripts via PUT/POST" />
|
||||||
<meta property="og:description" content="ntfy is a simple HTTP-based pub-sub notification service. It allows you to send desktop notifications via scripts from any computer, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
|
<meta property="og:description" content="ntfy lets you send push notifications via scripts from any computer or phone, entirely without signup or cost. Made with ❤ by Philipp C. Heckel, Apache License 2.0, source at https://heckel.io/ntfy." />
|
||||||
<meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
|
<meta property="og:image" content="%PUBLIC_URL%/static/img/ntfy.png" />
|
||||||
<meta property="og:url" content="https://ntfy.sh" />
|
<meta property="og:url" content="https://ntfy.sh" />
|
||||||
|
|
||||||
<!-- Never index -->
|
<!-- Never index -->
|
||||||
<meta name="robots" content="noindex, nofollow" />
|
<meta name="robots" content="noindex, nofollow" />
|
||||||
|
|
||||||
<!-- Server configuration -->
|
|
||||||
<script src="%PUBLIC_URL%/config.js"></script>
|
|
||||||
|
|
||||||
<!-- FIXME Roboto -->
|
<!-- FIXME Roboto -->
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
|
||||||
</head>
|
</head>
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "Your Orders",
|
|
||||||
"name": "Your Orders",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 297 KiB After Width: | Height: | Size: 297 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 134 KiB |
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 227 KiB |
Before Width: | Height: | Size: 225 KiB After Width: | Height: | Size: 225 KiB |
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 224 KiB |
Before Width: | Height: | Size: 270 KiB After Width: | Height: | Size: 270 KiB |
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
|
@ -1,2 +1,5 @@
|
||||||
const config = window.config;
|
//const config = window.config;
|
||||||
|
const config = {
|
||||||
|
defaultBaseUrl: "https://ntfy.sh"
|
||||||
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-rout
|
||||||
import {subscriptionRoute} from "../app/utils";
|
import {subscriptionRoute} from "../app/utils";
|
||||||
|
|
||||||
// TODO support unsubscribed routes
|
// TODO support unsubscribed routes
|
||||||
// TODO embed into ntfy server
|
|
||||||
// TODO googlefonts
|
// TODO googlefonts
|
||||||
// TODO new notification indicator
|
// TODO new notification indicator
|
||||||
// TODO sound
|
// TODO sound
|
||||||
|
|
|
@ -251,7 +251,7 @@ const NothingHereYet = (props) => {
|
||||||
return (
|
return (
|
||||||
<VerticallyCenteredContainer maxWidth="xs">
|
<VerticallyCenteredContainer maxWidth="xs">
|
||||||
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
<Typography variant="h5" align="center" sx={{ paddingBottom: 1 }}>
|
||||||
<img src="static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
|
<img src="/static/img/ntfy-outline.svg" height="64" width="64" alt="No notifications"/><br />
|
||||||
You haven't received any notifications for this topic yet.
|
You haven't received any notifications for this topic yet.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Paragraph>
|
<Paragraph>
|
||||||
|
|
|
@ -109,6 +109,7 @@ const SubscribePage = (props) => {
|
||||||
margin="dense"
|
margin="dense"
|
||||||
id="topic"
|
id="topic"
|
||||||
placeholder="Topic name, e.g. phil_alerts"
|
placeholder="Topic name, e.g. phil_alerts"
|
||||||
|
inputProps={{ maxLength: 64 }}
|
||||||
value={props.topic}
|
value={props.topic}
|
||||||
onChange={ev => props.setTopic(ev.target.value)}
|
onChange={ev => props.setTopic(ev.target.value)}
|
||||||
type="text"
|
type="text"
|
||||||
|
|