Styling
This commit is contained in:
		
							parent
							
								
									39574c954b
								
							
						
					
					
						commit
						317621c696
					
				
					 10 changed files with 252 additions and 135 deletions
				
			
		
							
								
								
									
										
											BIN
										
									
								
								assets/favicon.xcf
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								assets/favicon.xcf
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,6 +1,7 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <meta charset="UTF-8"> | ||||
|     <title>ntfy.sh: EventSource Example</title> | ||||
|     <style> | ||||
|         body { font-size: 1.2em; line-height: 130%; } | ||||
|  |  | |||
|  | @ -1,19 +1,39 @@ | |||
| <!DOCTYPE html> | ||||
| <html lang="en"> | ||||
| <head> | ||||
|     <title>ntfy.sh</title> | ||||
|     <style> | ||||
|         body { font-size: 1.2em; line-height: 130%; } | ||||
|         #error { color: darkred; font-style: italic; } | ||||
|         #main { max-width: 900px; margin: 0 auto 50px auto; } | ||||
|     </style> | ||||
|     <meta charset="UTF-8"> | ||||
| 
 | ||||
|     <title>ntfy.sh | simple HTTP-based pub-sub</title> | ||||
|     <link rel="stylesheet" href="static/css/app.css" type="text/css"> | ||||
| 
 | ||||
|     <!-- Mobile view --> | ||||
|     <meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"> | ||||
|     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> | ||||
|     <meta name="HandheldFriendly" content="true"> | ||||
| 
 | ||||
|     <!-- Mobile browsers, background color --> | ||||
|     <meta name="theme-color" content="#004c79"> | ||||
|     <meta name="msapplication-navbutton-color" content="#004c79"> | ||||
|     <meta name="apple-mobile-web-app-status-bar-style" content="#004c79"> | ||||
| 
 | ||||
|     <!-- Favicon, see favicon.io --> | ||||
|     <link rel="icon" type="image/png" href="static/img/favicon.png"> | ||||
| 
 | ||||
|     <!-- 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 | simple HTTP-based pub-sub" /> | ||||
|     <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:image" content="/static/img/ntfy.png" /> | ||||
|     <meta property="og:url" content="https://ntfy.sh" /> | ||||
| </head> | ||||
| <body> | ||||
| <div id="main"> | ||||
|     <h1>ntfy.sh - simple HTTP-based pub-sub</h1> | ||||
|     <p> | ||||
|         <b>ntfy</b> (pronounce: <i>notify</i>) is a simple <b>HTTP-based pub-sub notification service and tool</b>. | ||||
|         It allows you to send <b>desktop notifications via scripts</b>, entirely <b>without signup or cost</b>. | ||||
|         <b>ntfy</b> (pronounce: <i>notify</i>) is a simple HTTP-based pub-sub notification service and tool. | ||||
|         It allows you to send <b>desktop notifications via scripts from any computer</b>, entirely <b>without signup or cost</b>. | ||||
|         It's also <a href="https://github.com/binwiederhier/ntfy">open source</a> if you want to run your own. | ||||
|     </p> | ||||
|     <p id="error"></p> | ||||
|  | @ -37,151 +57,31 @@ | |||
|         <p> | ||||
|             <label for="topicField">Topic ID:</label> | ||||
|             <input type="text" id="topicField" placeholder="Letters, numbers, _ and -"  pattern="[-_A-Za-z]{1,64}" autofocus /> | ||||
|             <input type="submit" id="subscribeButton" value="Subscribe topic" /> | ||||
|             <input type="submit" id="subscribeButton" value="Subscribe" /> | ||||
|         </p> | ||||
|     </form> | ||||
|     <p id="topicsHeader">Subscribed topics:</p> | ||||
|     <ul id="topicsList"></ul> | ||||
| 
 | ||||
|     <h3>Subscribe via your app, or via the CLI</h3> | ||||
|     <tt> | ||||
|     <code> | ||||
|         curl -s ntfy.sh/mytopic/raw # one message per line (\n are replaced with a space)<br/> | ||||
|         curl -s ntfy.sh/mytopic/json # one JSON message per line<br/> | ||||
|         curl -s ntfy.sh/mytopic/sse # server-sent events (SSE) stream | ||||
|     </tt> | ||||
|     </code> | ||||
| 
 | ||||
|     <h3>Publishing messages</h3> | ||||
|     <h2>Publishing messages</h2> | ||||
|     <p> | ||||
|         Publishing messages can be done via PUT or POST using. Here's an example using <tt>curl</tt>: | ||||
|     </p> | ||||
|     <tt> | ||||
|     <code> | ||||
|         curl -d "long process is done" ntfy.sh/mytopic | ||||
|     </tt> | ||||
|     </code> | ||||
|     <p> | ||||
|         Messages published to a non-existing topic or a topic without subscribers will not be delivered later. | ||||
|         There is (currently) no buffering of any kind. If you're not listening, the message won't be delivered. | ||||
|     </p> | ||||
| </div> | ||||
| 
 | ||||
| <script type="text/javascript"> | ||||
|     let topics = {}; | ||||
| 
 | ||||
|     const topicsHeader = document.getElementById("topicsHeader"); | ||||
|     const topicsList = document.getElementById("topicsList"); | ||||
|     const topicField = document.getElementById("topicField"); | ||||
|     const subscribeButton = document.getElementById("subscribeButton"); | ||||
|     const subscribeForm = document.getElementById("subscribeForm"); | ||||
|     const errorField = document.getElementById("error"); | ||||
| 
 | ||||
|     const subscribe = (topic) => { | ||||
|         if (Notification.permission !== "granted") { | ||||
|             Notification.requestPermission().then((permission) => { | ||||
|                 if (permission === "granted") { | ||||
|                     subscribeInternal(topic, 0); | ||||
|                 } else { | ||||
|                     showNotificationDeniedError(); | ||||
|                 } | ||||
|             }); | ||||
|         } else { | ||||
|             subscribeInternal(topic, 0); | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const subscribeInternal = (topic, delaySec) => { | ||||
|         setTimeout(() => { | ||||
|             // Render list entry | ||||
|             let topicEntry = document.getElementById(`topic-${topic}`); | ||||
|             if (!topicEntry) { | ||||
|                 topicEntry = document.createElement('li'); | ||||
|                 topicEntry.id = `topic-${topic}`; | ||||
|                 topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|                 topicsList.appendChild(topicEntry); | ||||
|             } | ||||
|             topicsHeader.style.display = ''; | ||||
| 
 | ||||
|             // Open event source | ||||
|             let eventSource = new EventSource(`${topic}/sse`); | ||||
|             eventSource.onopen = () => { | ||||
|                 topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|                 delaySec = 0; // Reset on successful connection | ||||
|             }; | ||||
|             eventSource.onerror = (e) => { | ||||
|                 const newDelaySec = (delaySec + 5 <= 15) ? delaySec + 5 : 15; | ||||
|                 topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button disabled="disabled">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|                 eventSource.close() | ||||
|                 subscribeInternal(topic, newDelaySec); | ||||
|             }; | ||||
|             eventSource.onmessage = (e) => { | ||||
|                 const event = JSON.parse(e.data); | ||||
|                 new Notification(event.message); | ||||
|             }; | ||||
|             topics[topic] = eventSource; | ||||
|             localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); | ||||
|         }, delaySec * 1000); | ||||
|     }; | ||||
| 
 | ||||
|     const unsubscribe = (topic) => { | ||||
|         topics[topic].close(); | ||||
|         delete topics[topic]; | ||||
|         localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); | ||||
|         document.getElementById(`topic-${topic}`).remove(); | ||||
|         if (Object.keys(topics).length === 0) { | ||||
|             topicsHeader.style.display = 'none'; | ||||
|         } | ||||
|     }; | ||||
| 
 | ||||
|     const test = (topic) => { | ||||
|         fetch(`/${topic}`, { | ||||
|             method: 'PUT', | ||||
|             body: `This is a test notification for topic ${topic}!` | ||||
|         }) | ||||
|     }; | ||||
| 
 | ||||
|     const showError = (msg) => { | ||||
|         errorField.innerHTML = msg; | ||||
|         topicField.disabled = true; | ||||
|         subscribeButton.disabled = true; | ||||
|     }; | ||||
| 
 | ||||
|     const showBrowserIncompatibleError = () => { | ||||
|         showError("Your browser is not compatible to use the web-based desktop notifications."); | ||||
|     }; | ||||
| 
 | ||||
|     const showNotificationDeniedError = () => { | ||||
|         showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications."); | ||||
|     }; | ||||
| 
 | ||||
|     subscribeForm.onsubmit = function () { | ||||
|         if (!topicField.value) { | ||||
|             return false; | ||||
|         } | ||||
|         subscribe(topicField.value); | ||||
|         topicField.value = ""; | ||||
|         return false; | ||||
|     }; | ||||
| 
 | ||||
|     // Disable Web UI if notifications of EventSource are not available | ||||
|     if (!window["Notification"] || !window["EventSource"]) { | ||||
|         showBrowserIncompatibleError(); | ||||
|     } else if (Notification.permission === "denied") { | ||||
|         showNotificationDeniedError(); | ||||
|     } | ||||
| 
 | ||||
|     // Reset UI | ||||
|     topicField.value = ""; | ||||
| 
 | ||||
|     // Restore topics | ||||
|     const storedTopics = localStorage.getItem('topics'); | ||||
|     if (storedTopics && Notification.permission === "granted") { | ||||
|         const storedTopicsArray = JSON.parse(storedTopics) | ||||
|         storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); }); | ||||
|         if (storedTopicsArray.length === 0) { | ||||
|             topicsHeader.style.display = 'none'; | ||||
|         } | ||||
|     } else { | ||||
|         topicsHeader.style.display = 'none'; | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
| <script src="static/js/app.js"></script> | ||||
| </body> | ||||
| </html> | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ package server | |||
| 
 | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"embed" | ||||
| 	_ "embed" // required for go:embed | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
|  | @ -51,10 +52,14 @@ var ( | |||
| 	jsonRegex  = regexp.MustCompile(`^/[^/]+/json$`) | ||||
| 	sseRegex   = regexp.MustCompile(`^/[^/]+/sse$`) | ||||
| 	rawRegex   = regexp.MustCompile(`^/[^/]+/raw$`) | ||||
| 	staticRegex   = regexp.MustCompile(`^/static/.+`) | ||||
| 
 | ||||
| 	//go:embed "index.html" | ||||
| 	indexSource string | ||||
| 
 | ||||
| 	//go:embed static | ||||
| 	webStaticFs embed.FS | ||||
| 
 | ||||
| 	errHTTPNotFound        = &errHTTP{http.StatusNotFound, http.StatusText(http.StatusNotFound)} | ||||
| 	errHTTPTooManyRequests = &errHTTP{http.StatusTooManyRequests, http.StatusText(http.StatusTooManyRequests)} | ||||
| ) | ||||
|  | @ -123,6 +128,8 @@ func (s *Server) handleInternal(w http.ResponseWriter, r *http.Request) error { | |||
| 	} | ||||
| 	if r.Method == http.MethodGet && r.URL.Path == "/" { | ||||
| 		return s.handleHome(w, r) | ||||
| 	} else if r.Method == http.MethodGet && staticRegex.MatchString(r.URL.Path) { | ||||
| 		return s.handleStatic(w, r) | ||||
| 	} else if r.Method == http.MethodGet && jsonRegex.MatchString(r.URL.Path) { | ||||
| 		return s.handleSubscribeJSON(w, r) | ||||
| 	} else if r.Method == http.MethodGet && sseRegex.MatchString(r.URL.Path) { | ||||
|  | @ -241,6 +248,11 @@ func (s *Server) handleOptions(w http.ResponseWriter, r *http.Request) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) handleStatic(w http.ResponseWriter, r *http.Request) error { | ||||
| 	http.FileServer(http.FS(webStaticFs)).ServeHTTP(w, r) | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s *Server) createTopic(id string) *topic { | ||||
| 	s.mu.Lock() | ||||
| 	defer s.mu.Unlock() | ||||
|  |  | |||
							
								
								
									
										76
									
								
								server/static/css/app.css
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								server/static/css/app.css
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,76 @@ | |||
| /* general styling */ | ||||
| 
 | ||||
| html, body { | ||||
|     font-family: 'Lato', sans-serif; | ||||
|     color: #333; | ||||
|     font-size: 1.1em; | ||||
| } | ||||
| 
 | ||||
| a { | ||||
|     color: #39005a; | ||||
| } | ||||
| 
 | ||||
| a:hover { | ||||
|     text-decoration: none; | ||||
| } | ||||
| 
 | ||||
| h1 { | ||||
|     margin-top: 25px; | ||||
|     margin-bottom: 18px; | ||||
|     font-size: 2.5em; | ||||
| } | ||||
| 
 | ||||
| h2 { | ||||
|     margin-top: 20px; | ||||
|     margin-bottom: 5px; | ||||
|     font-size: 1.8em; | ||||
| } | ||||
| 
 | ||||
| h3 { | ||||
|     margin-top: 20px; | ||||
|     margin-bottom: 5px; | ||||
|     font-size: 1.3em; | ||||
| } | ||||
| 
 | ||||
| p { | ||||
|     margin-top: 0; | ||||
|     font-size: 1.1em; | ||||
| } | ||||
| 
 | ||||
| tt { | ||||
|     background: #eee; | ||||
|     padding: 2px 7px; | ||||
|     border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| code { | ||||
|     display: block; | ||||
|     background: #eee; | ||||
|     font-family: monospace; | ||||
|     padding: 20px; | ||||
|     border-radius: 3px; | ||||
| } | ||||
| 
 | ||||
| /* Lato font (OFL), https://fonts.google.com/specimen/Lato#about, | ||||
|    embedded with the help of https://google-webfonts-helper.herokuapp.com/fonts/lato?subsets=latin */ | ||||
| 
 | ||||
| @font-face { | ||||
|     font-family: 'Lato'; | ||||
|     font-style: normal; | ||||
|     font-weight: 400; | ||||
|     src: local(''), | ||||
|     url('../font/lato-v17-latin-ext_latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */ | ||||
|     url('../font/lato-v17-latin-ext_latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */ | ||||
| } | ||||
| 
 | ||||
| /* Main page */ | ||||
| 
 | ||||
| #main { | ||||
|     max-width: 900px; | ||||
|     margin: 0 auto 50px auto; | ||||
| } | ||||
| 
 | ||||
| #error { | ||||
|     color: darkred; | ||||
|     font-style: italic; | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								server/static/font/lato-v17-latin-ext_latin-regular.woff
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/font/lato-v17-latin-ext_latin-regular.woff
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								server/static/font/lato-v17-latin-ext_latin-regular.woff2
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/font/lato-v17-latin-ext_latin-regular.woff2
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								server/static/img/favicon.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/img/favicon.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										
											BIN
										
									
								
								server/static/img/ntfy.png
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								server/static/img/ntfy.png
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
							
								
								
									
										128
									
								
								server/static/js/app.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								server/static/js/app.js
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,128 @@ | |||
| 
 | ||||
| /** | ||||
|  * Hello, dear curious visitor. I am not a web-guy, so please don't judge my horrible JS code. | ||||
|  * In fact, please do tell me about all the things I did wrong and that I could improve. I've been trying | ||||
|  * to read up on modern JS, but it's just a little much. | ||||
|  * | ||||
|  * Feel free to open tickets at https://github.com/binwiederhier/ntfy/issues. Thank you!
 | ||||
|  */ | ||||
| 
 | ||||
| /* All the things */ | ||||
| 
 | ||||
| let topics = {}; | ||||
| 
 | ||||
| const topicsHeader = document.getElementById("topicsHeader"); | ||||
| const topicsList = document.getElementById("topicsList"); | ||||
| const topicField = document.getElementById("topicField"); | ||||
| const subscribeButton = document.getElementById("subscribeButton"); | ||||
| const subscribeForm = document.getElementById("subscribeForm"); | ||||
| const errorField = document.getElementById("error"); | ||||
| 
 | ||||
| const subscribe = (topic) => { | ||||
|     if (Notification.permission !== "granted") { | ||||
|         Notification.requestPermission().then((permission) => { | ||||
|             if (permission === "granted") { | ||||
|                 subscribeInternal(topic, 0); | ||||
|             } else { | ||||
|                 showNotificationDeniedError(); | ||||
|             } | ||||
|         }); | ||||
|     } else { | ||||
|         subscribeInternal(topic, 0); | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const subscribeInternal = (topic, delaySec) => { | ||||
|     setTimeout(() => { | ||||
|         // Render list entry
 | ||||
|         let topicEntry = document.getElementById(`topic-${topic}`); | ||||
|         if (!topicEntry) { | ||||
|             topicEntry = document.createElement('li'); | ||||
|             topicEntry.id = `topic-${topic}`; | ||||
|             topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|             topicsList.appendChild(topicEntry); | ||||
|         } | ||||
|         topicsHeader.style.display = ''; | ||||
| 
 | ||||
|         // Open event source
 | ||||
|         let eventSource = new EventSource(`${topic}/sse`); | ||||
|         eventSource.onopen = () => { | ||||
|             topicEntry.innerHTML = `${topic} <button onclick="test('${topic}')">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|             delaySec = 0; // Reset on successful connection
 | ||||
|         }; | ||||
|         eventSource.onerror = (e) => { | ||||
|             const newDelaySec = (delaySec + 5 <= 15) ? delaySec + 5 : 15; | ||||
|             topicEntry.innerHTML = `${topic} <i>(Reconnecting in ${newDelaySec}s ...)</i> <button disabled="disabled">Test</button> <button onclick="unsubscribe('${topic}')">Unsubscribe</button>`; | ||||
|             eventSource.close() | ||||
|             subscribeInternal(topic, newDelaySec); | ||||
|         }; | ||||
|         eventSource.onmessage = (e) => { | ||||
|             const event = JSON.parse(e.data); | ||||
|             new Notification(event.message); | ||||
|         }; | ||||
|         topics[topic] = eventSource; | ||||
|         localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); | ||||
|     }, delaySec * 1000); | ||||
| }; | ||||
| 
 | ||||
| const unsubscribe = (topic) => { | ||||
|     topics[topic].close(); | ||||
|     delete topics[topic]; | ||||
|     localStorage.setItem('topics', JSON.stringify(Object.keys(topics))); | ||||
|     document.getElementById(`topic-${topic}`).remove(); | ||||
|     if (Object.keys(topics).length === 0) { | ||||
|         topicsHeader.style.display = 'none'; | ||||
|     } | ||||
| }; | ||||
| 
 | ||||
| const test = (topic) => { | ||||
|     fetch(`/${topic}`, { | ||||
|         method: 'PUT', | ||||
|         body: `This is a test notification for topic ${topic}!` | ||||
|     }) | ||||
| }; | ||||
| 
 | ||||
| const showError = (msg) => { | ||||
|     errorField.innerHTML = msg; | ||||
|     topicField.disabled = true; | ||||
|     subscribeButton.disabled = true; | ||||
| }; | ||||
| 
 | ||||
| const showBrowserIncompatibleError = () => { | ||||
|     showError("Your browser is not compatible to use the web-based desktop notifications."); | ||||
| }; | ||||
| 
 | ||||
| const showNotificationDeniedError = () => { | ||||
|     showError("You have blocked desktop notifications for this website. Please unblock them and refresh to use the web-based desktop notifications."); | ||||
| }; | ||||
| 
 | ||||
| subscribeForm.onsubmit = function () { | ||||
|     if (!topicField.value) { | ||||
|         return false; | ||||
|     } | ||||
|     subscribe(topicField.value); | ||||
|     topicField.value = ""; | ||||
|     return false; | ||||
| }; | ||||
| 
 | ||||
| // Disable Web UI if notifications of EventSource are not available
 | ||||
| if (!window["Notification"] || !window["EventSource"]) { | ||||
|     showBrowserIncompatibleError(); | ||||
| } else if (Notification.permission === "denied") { | ||||
|     showNotificationDeniedError(); | ||||
| } | ||||
| 
 | ||||
| // Reset UI
 | ||||
| topicField.value = ""; | ||||
| 
 | ||||
| // Restore topics
 | ||||
| const storedTopics = localStorage.getItem('topics'); | ||||
| if (storedTopics && Notification.permission === "granted") { | ||||
|     const storedTopicsArray = JSON.parse(storedTopics) | ||||
|     storedTopicsArray.forEach((topic) => { subscribeInternal(topic, 0); }); | ||||
|     if (storedTopicsArray.length === 0) { | ||||
|         topicsHeader.style.display = 'none'; | ||||
|     } | ||||
| } else { | ||||
|     topicsHeader.style.display = 'none'; | ||||
| } | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue