diff --git a/pages/notifications/api.go b/pages/notifications/api.go index 11c4e0ab..9ea5b346 100644 --- a/pages/notifications/api.go +++ b/pages/notifications/api.go @@ -38,6 +38,12 @@ func MarkNotificationsAsSeen(ctx *aero.Context) string { notification.Save() } + // Update the counter on all clients + user.BroadcastEvent(&aero.Event{ + Name: "notificationCount", + Data: 0, + }) + return "ok" } diff --git a/pages/sse/sse.go b/pages/sse/sse.go index 0e231c60..b4cbc7d0 100644 --- a/pages/sse/sse.go +++ b/pages/sse/sse.go @@ -4,10 +4,19 @@ import ( "fmt" "net/http" + "github.com/animenotifier/notify.moe/components/css" + "github.com/animenotifier/notify.moe/components/js" + "github.com/aerogo/aero" "github.com/animenotifier/notify.moe/utils" ) +var ( + scriptsETag = aero.ETagString(js.Bundle()) + stylesETag = aero.ETagString(css.Bundle()) + streams = map[string][]*aero.EventStream{} +) + // Events streams server events to the client. func Events(ctx *aero.Context) string { user := utils.GetUser(ctx) @@ -17,26 +26,47 @@ func Events(ctx *aero.Context) string { } fmt.Println(user.Nick, "receiving live events") - - events := make(chan *aero.Event) - disconnected := make(chan struct{}) + stream := aero.NewEventStream() + user.AddEventStream(stream) go func() { defer fmt.Println(user.Nick, "disconnected, stop sending events") + stream.Events <- &aero.Event{ + Name: "etag", + Data: struct { + URL string `json:"url"` + ETag string `json:"etag"` + }{ + URL: "/scripts", + ETag: scriptsETag, + }, + } + + stream.Events <- &aero.Event{ + Name: "etag", + Data: struct { + URL string `json:"url"` + ETag string `json:"etag"` + }{ + URL: "/styles", + ETag: stylesETag, + }, + } + for { select { - case <-disconnected: - close(events) + case <-stream.Closed: + user.RemoveEventStream(stream) return // case <-time.After(10 * time.Second): - // events <- &aero.Event{ + // stream.Events <- &aero.Event{ // Name: "ping", // } } } }() - return ctx.EventStream(events, disconnected) + return ctx.EventStream(stream) } diff --git a/scripts/Actions/Notifications.ts b/scripts/Actions/Notifications.ts index d87fb284..4e1fb076 100644 --- a/scripts/Actions/Notifications.ts +++ b/scripts/Actions/Notifications.ts @@ -31,18 +31,6 @@ export async function markNotificationsAsSeen(arn: AnimeNotifier) { credentials: "same-origin" }) - // Update notification counter - if("serviceWorker" in navigator) { - // If we have service worker support, broadcast the "notifications marked as seen" message to all open tabs - arn.serviceWorkerManager.postMessage({ - type: "broadcast", - realType: "notifications marked as seen" - }) - } else { - // If there is no service worker, update the counter on this tab - arn.notificationManager.update() - } - // Update notifications arn.reloadContent() } \ No newline at end of file diff --git a/scripts/AnimeNotifier.ts b/scripts/AnimeNotifier.ts index 41e01485..2d58b5f1 100644 --- a/scripts/AnimeNotifier.ts +++ b/scripts/AnimeNotifier.ts @@ -10,7 +10,6 @@ import SideBar from "./SideBar" import InfiniteScroller from "./InfiniteScroller" import ServiceWorkerManager from "./ServiceWorkerManager" import ServerEvents from "./ServerEvents" -import { checkNewVersionDelayed } from "./NewVersionCheck" import { displayAiringDate, displayDate, displayTime } from "./DateView" import { findAll, canUseWebP, requestIdleCallback, swapElements, delay, findAllInside } from "./Utils" import * as actions from "./Actions" @@ -209,18 +208,11 @@ export default class AnimeNotifier { // Notification manager if(this.user) { this.notificationManager.update() - - // Periodically check notifications - setInterval(() => this.notificationManager.update(), 300000) } // Bind unload event window.addEventListener("beforeunload", this.onBeforeUnload.bind(this)) - // Periodically check etags of scripts and styles to let the user know about page updates - checkNewVersionDelayed("/scripts", this.statusMessage) - checkNewVersionDelayed("/styles", this.statusMessage) - // Show microphone icon if speech input is available if(window["SpeechRecognition"] || window["webkitSpeechRecognition"]) { document.getElementsByClassName("speech-input")[0].classList.add("speech-input-available") @@ -239,7 +231,7 @@ export default class AnimeNotifier { // Server sent events if(this.user && EventSource) { - this.serverEvents = new ServerEvents() + this.serverEvents = new ServerEvents(this) } // // Download popular anime titles for the search diff --git a/scripts/NewVersionCheck.ts b/scripts/NewVersionCheck.ts deleted file mode 100644 index fbcf3c12..00000000 --- a/scripts/NewVersionCheck.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { delay, requestIdleCallback } from "./Utils" -import StatusMessage from "./StatusMessage" - -const newVersionCheckDelay = location.hostname === "notify.moe" ? 60000 : 3000 - -let etags = new Map() -let hasNewVersion = false - -async function checkNewVersion(url: string, statusMessage: StatusMessage) { - if(hasNewVersion) { - return - } - - try { - let headers = {} - - if(etags.has(url)) { - headers["If-None-Match"] = etags.get(url) - } - - let response = await fetch(url, { - headers, - credentials: "omit" - }) - - // Not modified response - if(response.status === 304) { - return - } - - if(!response.ok) { - console.warn("Error fetching", url, response.status) - return - } - - let newETag = response.headers.get("ETag") - let oldETag = etags.get(url) - - if(newETag) { - etags.set(url, newETag) - } - - if(oldETag && newETag && oldETag !== newETag) { - statusMessage.showInfo("A new version of the website is available. Please refresh the page.", -1) - - // Do not check for new versions again. - hasNewVersion = true - return - } - } catch(err) { - console.warn("Error fetching", url + "\n", err) - } finally { - checkNewVersionDelayed(url, statusMessage) - } -} - -export function checkNewVersionDelayed(url: string, statusMessage: StatusMessage) { - return delay(newVersionCheckDelay).then(() => { - requestIdleCallback(() => checkNewVersion(url, statusMessage)) - }) -} diff --git a/scripts/NotificationManager.ts b/scripts/NotificationManager.ts index ae7ba85e..86d4e248 100644 --- a/scripts/NotificationManager.ts +++ b/scripts/NotificationManager.ts @@ -16,7 +16,11 @@ export default class NotificationManager { }) let body = await response.text() - this.unseen = parseInt(body) + this.setCounter(parseInt(body)) + } + + setCounter(unseen: number) { + this.unseen = unseen if(isNaN(this.unseen)) { this.unseen = 0 diff --git a/scripts/ServerEvents.ts b/scripts/ServerEvents.ts index 60c2bc77..09f957cc 100644 --- a/scripts/ServerEvents.ts +++ b/scripts/ServerEvents.ts @@ -1,3 +1,7 @@ +import AnimeNotifier from "./AnimeNotifier" + +const reconnectDelay = 3000 + class ServerEvent { data: string } @@ -5,22 +9,56 @@ class ServerEvent { export default class ServerEvents { supported: boolean eventSource: EventSource + arn: AnimeNotifier + etags: Map - constructor() { + constructor(arn: AnimeNotifier) { this.supported = ("EventSource" in window) if(!this.supported) { return } + this.arn = arn + this.etags = new Map() + this.connect() + } + + connect() { + if(this.eventSource) { + this.eventSource.close() + } + this.eventSource = new EventSource("/api/sse/events", { withCredentials: true }) this.eventSource.addEventListener("ping", (e: any) => this.ping(e)) + this.eventSource.addEventListener("etag", (e: any) => this.etag(e)) + this.eventSource.addEventListener("notificationCount", (e: any) => this.notificationCount(e)) + + this.eventSource.onerror = () => { + setTimeout(() => this.connect(), reconnectDelay) + } } ping(e: ServerEvent) { - console.log("sse: ping") + console.log("ping") + } + + etag(e: ServerEvent) { + let data = JSON.parse(e.data) + let oldETag = this.etags.get(data.url) + let newETag = data.etag + + if(oldETag && newETag && oldETag != newETag) { + this.arn.statusMessage.showInfo("A new version of the website is available. Please refresh the page.", -1) + } + + this.etags.set(data.url, newETag) + } + + notificationCount(e: ServerEvent) { + this.arn.notificationManager.setCounter(parseInt(e.data)) } } \ No newline at end of file diff --git a/scripts/ServiceWorkerManager.ts b/scripts/ServiceWorkerManager.ts index 019e2756..0374f134 100644 --- a/scripts/ServiceWorkerManager.ts +++ b/scripts/ServiceWorkerManager.ts @@ -31,7 +31,6 @@ export default class ServiceWorkerManager { // A reloadContent call should never trigger another reload if(this.arn.app.currentPath === this.arn.lastReloadContentPath) { - console.log("reload finished.") this.arn.lastReloadContentPath = "" return } @@ -68,32 +67,27 @@ export default class ServiceWorkerManager { } onMessage(evt: MessageEvent) { - let message = JSON.parse(evt.data) + // let message = JSON.parse(evt.data) - switch(message.type) { - case "new notification": - case "notifications marked as seen": - this.arn.notificationManager.update() - break + // switch(message.type) { + // case "new content": + // if(message.url.includes("/_/")) { + // // Content reload + // this.arn.contentLoadedActions.then(() => { + // this.arn.reloadContent(true) + // }) + // } else { + // // Full page reload + // this.arn.contentLoadedActions.then(() => { + // this.arn.reloadPage() + // }) + // } - case "new content": - if(message.url.includes("/_/")) { - // Content reload - this.arn.contentLoadedActions.then(() => { - this.arn.reloadContent(true) - }) - } else { - // Full page reload - this.arn.contentLoadedActions.then(() => { - this.arn.reloadPage() - }) - } + // break - break - - // case "offline": - // this.arn.statusMessage.showError("You are viewing an offline version of the site now.") - // break - } + // // case "offline": + // // this.arn.statusMessage.showError("You are viewing an offline version of the site now.") + // // break + // } } } \ No newline at end of file