Embed new web UI into server
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -2,6 +2,7 @@ dist/
 | 
			
		|||
build/
 | 
			
		||||
.idea/
 | 
			
		||||
server/docs/
 | 
			
		||||
server/site/
 | 
			
		||||
tools/fbsend/fbsend
 | 
			
		||||
playground/
 | 
			
		||||
*.iml
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										29
									
								
								Makefile
									
										
									
									
									
								
							
							
						
						| 
						 | 
				
			
			@ -44,6 +44,28 @@ docs-deps: .PHONY
 | 
			
		|||
docs: docs-deps
 | 
			
		||||
	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
 | 
			
		||||
 | 
			
		||||
check: test fmt-check vet lint staticcheck
 | 
			
		||||
| 
						 | 
				
			
			@ -94,7 +116,7 @@ staticcheck: .PHONY
 | 
			
		|||
 | 
			
		||||
# 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 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
 | 
			
		||||
 | 
			
		||||
build-simple: clean
 | 
			
		||||
	mkdir -p dist/ntfy_linux_amd64 server/docs
 | 
			
		||||
	touch server/docs/dummy
 | 
			
		||||
	mkdir -p dist/ntfy_linux_amd64 server/docs server/site
 | 
			
		||||
	touch server/docs/index.html
 | 
			
		||||
	touch server/site/app.html
 | 
			
		||||
	export CGO_ENABLED=1
 | 
			
		||||
	go build \
 | 
			
		||||
		-o dist/ntfy_linux_amd64/ntfy \
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,7 +13,6 @@ import (
 | 
			
		|||
	"golang.org/x/sync/errgroup"
 | 
			
		||||
	"heckel.io/ntfy/auth"
 | 
			
		||||
	"heckel.io/ntfy/util"
 | 
			
		||||
	"html/template"
 | 
			
		||||
	"io"
 | 
			
		||||
	"log"
 | 
			
		||||
	"net"
 | 
			
		||||
| 
						 | 
				
			
			@ -61,35 +60,31 @@ type handleFunc func(http.ResponseWriter, *http.Request, *visitor) error
 | 
			
		|||
 | 
			
		||||
var (
 | 
			
		||||
	// If changed, don't forget to update Android App and auth_sqlite.go
 | 
			
		||||
	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!
 | 
			
		||||
	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$`)
 | 
			
		||||
	rawPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/raw$`)
 | 
			
		||||
	wsPathRegex      = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
 | 
			
		||||
	authPathRegex    = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
 | 
			
		||||
	publishPathRegex = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
 | 
			
		||||
	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!
 | 
			
		||||
	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$`)
 | 
			
		||||
	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$`)
 | 
			
		||||
	wsPathRegex       = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/ws$`)
 | 
			
		||||
	authPathRegex     = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/auth$`)
 | 
			
		||||
	publishPathRegex  = regexp.MustCompile(`^/[-_A-Za-z0-9]{1,64}(,[-_A-Za-z0-9]{1,64})*/(publish|send|trigger)$`)
 | 
			
		||||
 | 
			
		||||
	staticRegex      = regexp.MustCompile(`^/static/.+`)
 | 
			
		||||
	docsRegex        = regexp.MustCompile(`^/docs(|/.*)$`)
 | 
			
		||||
	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?://`)
 | 
			
		||||
 | 
			
		||||
	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"
 | 
			
		||||
	exampleSource string
 | 
			
		||||
 | 
			
		||||
	//go:embed static
 | 
			
		||||
	webStaticFs       embed.FS
 | 
			
		||||
	webStaticFsCached = &util.CachingEmbedFS{ModTime: time.Now(), FS: webStaticFs}
 | 
			
		||||
	//go:embed site
 | 
			
		||||
	webFs        embed.FS
 | 
			
		||||
	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
 | 
			
		||||
	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)
 | 
			
		||||
	} else if r.Method == http.MethodOptions {
 | 
			
		||||
		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) {
 | 
			
		||||
		return s.limitRequests(s.authWrite(s.handlePublish))(w, r, v)
 | 
			
		||||
	} 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)
 | 
			
		||||
	} else if r.Method == http.MethodGet && authPathRegex.MatchString(r.URL.Path) {
 | 
			
		||||
		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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *Server) handleHome(w http.ResponseWriter, r *http.Request) error {
 | 
			
		||||
	return indexTemplate.Execute(w, &indexPage{
 | 
			
		||||
		Topic:         r.URL.Path[1:],
 | 
			
		||||
		CacheDuration: s.config.CacheDuration,
 | 
			
		||||
	})
 | 
			
		||||
	r.URL.Path = webHomeIndex
 | 
			
		||||
	return s.handleStatic(w, r)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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")
 | 
			
		||||
		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 {
 | 
			
		||||
| 
						 | 
				
			
			@ -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 {
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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 -->
 | 
			
		||||
 | 
			
		||||
[](https://codesandbox.io/s/github/mui/material-ui/tree/master/examples/create-react-app)
 | 
			
		||||
 | 
			
		||||
<!-- #default-branch-switch -->
 | 
			
		||||
 | 
			
		||||
[](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>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
 | 
			
		||||
    <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 -->
 | 
			
		||||
    <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>
 | 
			
		||||
        <ol>
 | 
			
		||||
            <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/publish/">API</a></li>
 | 
			
		||||
            <li><a href="docs/install/">Self-hosting</a></li>
 | 
			
		||||
            <li><a href="https://github.com/binwiederhier/ntfy">GitHub</a></li>
 | 
			
		||||
        </ol>
 | 
			
		||||
    </div>
 | 
			
		||||
| 
						 | 
				
			
			@ -90,7 +89,7 @@
 | 
			
		|||
        Here's what that looks like in the <a href="docs/subscribe/phone/">Android app</a>:
 | 
			
		||||
    </p>
 | 
			
		||||
    <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>
 | 
			
		||||
    </figure>
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -170,7 +169,6 @@
 | 
			
		|||
    <center id="ironicCenterTagDontFreakOut"><i>Made with ❤️ by <a href="https://heckel.io">Philipp C. Heckel</a></i></center>
 | 
			
		||||
</div>
 | 
			
		||||
<div id="lightbox" class="lightbox"></div>
 | 
			
		||||
<script src="static/js/emoji.js"></script>
 | 
			
		||||
<script src="static/js/app.js"></script>
 | 
			
		||||
<script src="static/js/home.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
| 
						 | 
				
			
			@ -20,18 +20,15 @@
 | 
			
		|||
  <!-- Previews in Google, Slack, WhatsApp, etc. -->
 | 
			
		||||
  <meta property="og:type" content="website" />
 | 
			
		||||
  <meta property="og:locale" content="en_US" />
 | 
			
		||||
  <meta property="og:site_name" content="ntfy.sh" />
 | 
			
		||||
  <meta property="og:title" content="ntfy.sh | Send push notifications to your phone or desktop 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:site_name" content="ntfy web" />
 | 
			
		||||
  <meta property="og:title" content="ntfy web | Web app to receive push notifications from scripts via PUT/POST" />
 | 
			
		||||
  <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:url" content="https://ntfy.sh" />
 | 
			
		||||
 | 
			
		||||
  <!-- Never index -->
 | 
			
		||||
  <meta name="robots" content="noindex, nofollow" />
 | 
			
		||||
 | 
			
		||||
  <!-- Server configuration -->
 | 
			
		||||
  <script src="%PUBLIC_URL%/config.js"></script>
 | 
			
		||||
 | 
			
		||||
  <!-- FIXME Roboto -->
 | 
			
		||||
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" />
 | 
			
		||||
</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;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -21,7 +21,6 @@ import {BrowserRouter, Route, Routes, useLocation, useNavigate} from "react-rout
 | 
			
		|||
import {subscriptionRoute} from "../app/utils";
 | 
			
		||||
 | 
			
		||||
// TODO support unsubscribed routes
 | 
			
		||||
// TODO embed into ntfy server
 | 
			
		||||
// TODO googlefonts
 | 
			
		||||
// TODO new notification indicator
 | 
			
		||||
// TODO sound
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -251,7 +251,7 @@ const NothingHereYet = (props) => {
 | 
			
		|||
    return (
 | 
			
		||||
        <VerticallyCenteredContainer maxWidth="xs">
 | 
			
		||||
            <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.
 | 
			
		||||
            </Typography>
 | 
			
		||||
            <Paragraph>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -109,6 +109,7 @@ const SubscribePage = (props) => {
 | 
			
		|||
                    margin="dense"
 | 
			
		||||
                    id="topic"
 | 
			
		||||
                    placeholder="Topic name, e.g. phil_alerts"
 | 
			
		||||
                    inputProps={{ maxLength: 64 }}
 | 
			
		||||
                    value={props.topic}
 | 
			
		||||
                    onChange={ev => props.setTopic(ev.target.value)}
 | 
			
		||||
                    type="text"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||