From 2fd8757e6696deb2beb5b5a1fe151189c6fd0cfd Mon Sep 17 00:00:00 2001 From: FloPinguin <25036848+FloPinguin@users.noreply.github.com> Date: Sun, 22 Feb 2026 04:15:36 +0100 Subject: [PATCH] =?UTF-8?q?Notification=20dot=20for=20new=20versions=20(+?= =?UTF-8?q?=20mobile=20dot=20improvements)=20=E2=9C=A8=20(#3265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: - **News notification dot (desktop + mobile)**: Added a red pinging dot on the "News" nav entry that appears when a new version is released. The current app version is saved to localStorage (`newsSeenVersion`) on first visit. On subsequent visits, if the version has changed, the dot appears. Clicking "News" dismisses it by updating the stored version. - **Mobile Store**: Replaced the static "NEW" text badge on the Store nav item with a red pinging dot (matching the desktop navbar style). The dot is conditionally shown based on cosmetics hash changes tracked in localStorage, and dismissed when the user clicks Store. - **Help dot on mobile**: Added the yellow help dot (already present on desktop) to the mobile navbar for consistency, shown for users with fewer than 10 games played. ### Screenshots: Screenshot 2026-02-21 174029 Screenshot 2026-02-21 174333 ## Please complete the following: - [X] I have added screenshots for all UI updates - [X] I process any text displayed to the user through translateText() and I've added it to the en.json file - [X] I have added relevant tests to the test directory - [X] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced ## Please put your Discord username so you can be contacted if a bug or regression is found: FloPinguin --- resources/lang/en.json | 1 - src/client/components/DesktopNavBar.ts | 76 ++++++---------- src/client/components/MobileNavBar.ts | 63 +++++++++---- .../components/NavNotificationsController.ts | 91 +++++++++++++++++++ 4 files changed, 165 insertions(+), 66 deletions(-) create mode 100644 src/client/components/NavNotificationsController.ts 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(); + }; +}