diff --git a/resources/lang/en.json b/resources/lang/en.json
index 83160eb11..1e671caa2 100644
--- a/resources/lang/en.json
+++ b/resources/lang/en.json
@@ -42,7 +42,6 @@
"play": "Play",
"news": "News",
"store": "Store",
- "store_new_badge": "NEW",
"settings": "Settings",
"leaderboard": "Leaderboard",
"account": "Account",
diff --git a/src/client/components/DesktopNavBar.ts b/src/client/components/DesktopNavBar.ts
index 77bd1e0e6..45e1d0ea0 100644
--- a/src/client/components/DesktopNavBar.ts
+++ b/src/client/components/DesktopNavBar.ts
@@ -1,15 +1,10 @@
import { LitElement, html } from "lit";
-import { customElement, state } from "lit/decorators.js";
-import { getCosmeticsHash } from "../Cosmetics";
-import { getGamesPlayed } from "../Utils";
-
-const HELP_SEEN_KEY = "helpSeen";
-const STORE_SEEN_HASH_KEY = "storeSeenHash";
+import { customElement } from "lit/decorators.js";
+import { NavNotificationsController } from "./NavNotificationsController";
@customElement("desktop-nav-bar")
export class DesktopNavBar extends LitElement {
- @state() private _helpSeen = localStorage.getItem(HELP_SEEN_KEY) === "true";
- @state() private _hasNewCosmetics = false;
+ private _notifications = new NavNotificationsController(this);
createRenderRoot() {
return this;
@@ -26,12 +21,6 @@ export class DesktopNavBar extends LitElement {
this._updateActiveState(current);
});
}
-
- // Check if cosmetics have changed
- getCosmeticsHash().then((hash: string | null) => {
- const seenHash = localStorage.getItem(STORE_SEEN_HASH_KEY);
- this._hasNewCosmetics = hash !== null && hash !== seenHash;
- });
}
disconnectedCallback() {
@@ -54,30 +43,6 @@ export class DesktopNavBar extends LitElement {
});
}
- private showHelpDot(): boolean {
- // Only show one dot at a time to prevent
- // overwhelming users.
- return getGamesPlayed() < 10 && !this._helpSeen;
- }
-
- private showStoreDot(): boolean {
- return this._hasNewCosmetics && !this.showHelpDot();
- }
-
- private onHelpClick = () => {
- localStorage.setItem(HELP_SEEN_KEY, "true");
- this._helpSeen = true;
- };
-
- private onStoreClick = () => {
- this._hasNewCosmetics = false;
- getCosmeticsHash().then((hash: string | null) => {
- if (hash !== null) {
- localStorage.setItem(STORE_SEEN_HASH_KEY, hash);
- }
- });
- };
-
render() {
window.currentPageId ??= "page-play";
const currentPage = window.currentPageId;
@@ -142,13 +107,26 @@ export class DesktopNavBar extends LitElement {
data-i18n="main.play"
>
-
+
+
+ ${this._notifications.showNewsDot()
+ ? html`
+
+
+ `
+ : ""}
+
- ${this.showStoreDot()
+ ${this._notifications.showStoreDot()
? html`
- ${this.showHelpDot()
+ ${this._notifications.showHelpDot()
? html`
{
+ const inner = el.querySelector("button");
if ((el as HTMLElement).dataset.page === pageId) {
el.classList.add("active");
+ inner?.classList.add("active");
} else {
el.classList.remove("active");
+ inner?.classList.remove("active");
}
});
}
+ private _renderDot(color: string): TemplateResult {
+ return html`
+
+
+ `;
+ }
+
render() {
window.currentPageId ??= "page-play";
const currentPage = window.currentPageId;
@@ -120,26 +133,36 @@ export class MobileNavBar extends LitElement {
data-page="page-play"
data-i18n="main.play"
>
-
+ @click=${this._notifications.onNewsClick}
+ >
+
+ ${this._notifications.showNewsDot()
+ ? this._renderDot("bg-red-500")
+ : ""}
+
-
+
-
+ @click=${this._notifications.onHelpClick}
+ >
+
+ ${this._notifications.showHelpDot()
+ ? this._renderDot("bg-yellow-400")
+ : ""}
+
diff --git a/src/client/components/NavNotificationsController.ts b/src/client/components/NavNotificationsController.ts
new file mode 100644
index 000000000..1e5937a07
--- /dev/null
+++ b/src/client/components/NavNotificationsController.ts
@@ -0,0 +1,91 @@
+import { ReactiveController, ReactiveControllerHost } from "lit";
+import version from "resources/version.txt?raw";
+import { getCosmeticsHash } from "../Cosmetics";
+import { getGamesPlayed } from "../Utils";
+
+const HELP_SEEN_KEY = "helpSeen";
+const STORE_SEEN_HASH_KEY = "storeSeenHash";
+const NEWS_SEEN_VERSION_KEY = "newsSeenVersion";
+
+export class NavNotificationsController implements ReactiveController {
+ private host: ReactiveControllerHost;
+
+ private _helpSeen = localStorage.getItem(HELP_SEEN_KEY) === "true";
+ private _hasNewCosmetics = false;
+ private _hasNewVersion = false;
+
+ private get normalizedVersion(): string {
+ const trimmed = version.trim();
+ return trimmed.startsWith("v") ? trimmed : `v${trimmed}`;
+ }
+
+ constructor(host: ReactiveControllerHost) {
+ this.host = host;
+ host.addController(this);
+ }
+
+ hostConnected(): void {
+ // Check if cosmetics have changed
+ getCosmeticsHash()
+ .then((hash: string | null) => {
+ const seenHash = localStorage.getItem(STORE_SEEN_HASH_KEY);
+ this._hasNewCosmetics = hash !== null && hash !== seenHash;
+ this.host.requestUpdate();
+ })
+ .catch(() => {});
+
+ // Check if version has changed
+ const currentVersion = this.normalizedVersion;
+ const seenVersion = localStorage.getItem(NEWS_SEEN_VERSION_KEY);
+ this._hasNewVersion =
+ seenVersion !== null && seenVersion !== currentVersion;
+ if (seenVersion === null) {
+ localStorage.setItem(NEWS_SEEN_VERSION_KEY, currentVersion);
+ }
+ }
+
+ hostDisconnected(): void {}
+
+ // Only show one dot at a time to prevent
+ // overwhelming users. Priority: News > Store > Help.
+ showNewsDot(): boolean {
+ return this._hasNewVersion;
+ }
+
+ showStoreDot(): boolean {
+ return this._hasNewCosmetics && !this.showNewsDot();
+ }
+
+ showHelpDot(): boolean {
+ return (
+ getGamesPlayed() < 10 &&
+ !this._helpSeen &&
+ !this.showNewsDot() &&
+ !this.showStoreDot()
+ );
+ }
+
+ onNewsClick = (): void => {
+ this._hasNewVersion = false;
+ localStorage.setItem(NEWS_SEEN_VERSION_KEY, this.normalizedVersion);
+ this.host.requestUpdate();
+ };
+
+ onStoreClick = (): void => {
+ this._hasNewCosmetics = false;
+ getCosmeticsHash()
+ .then((hash: string | null) => {
+ if (hash !== null) {
+ localStorage.setItem(STORE_SEEN_HASH_KEY, hash);
+ }
+ })
+ .catch(() => {});
+ this.host.requestUpdate();
+ };
+
+ onHelpClick = (): void => {
+ localStorage.setItem(HELP_SEEN_KEY, "true");
+ this._helpSeen = true;
+ this.host.requestUpdate();
+ };
+}