From 9d5184693287d6271d99f529d19eed1ac45dc244 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 25 Mar 2026 15:17:12 -0700 Subject: [PATCH 1/7] bugfix: Call modal close() on nav clicks so onClose callback fires --- src/client/Navigation.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/client/Navigation.ts b/src/client/Navigation.ts index 95f720092..7878155dd 100644 --- a/src/client/Navigation.ts +++ b/src/client/Navigation.ts @@ -19,11 +19,20 @@ export function initNavigation() { // Close mobile sidebar if a nav item was clicked closeMobileSidebar(); - // Hide only the currently visible modal + // Close the currently visible modal properly const visibleModal = document.querySelector(".page-content:not(.hidden)"); if (visibleModal) { - visibleModal.classList.add("hidden"); - visibleModal.classList.remove("block"); + // If it's an open modal component, call close() for proper cleanup (onClose callback, etc.) + if ( + typeof (visibleModal as any).isOpen === "function" && + (visibleModal as any).isOpen() && + typeof (visibleModal as any).close === "function" + ) { + (visibleModal as any).close(); + } else { + visibleModal.classList.add("hidden"); + visibleModal.classList.remove("block"); + } } // Handle page-play separately (it's not a page-content element) From d99359c4567f2f521dc54feec61e6d80bdb48333 Mon Sep 17 00:00:00 2001 From: evanpelle Date: Wed, 25 Mar 2026 15:24:43 -0700 Subject: [PATCH 2/7] Add a store button to the patterns modal and flag modal --- src/client/FlagInputModal.ts | 11 +++++++++++ src/client/TerritoryPatternsModal.ts | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts index 950b9afcd..03a19b69a 100644 --- a/src/client/FlagInputModal.ts +++ b/src/client/FlagInputModal.ts @@ -118,6 +118,17 @@ export class FlagInputModal extends BaseModal { /> +
+ +
+
+ +
From 1c2bd5df315e4c037a045b96bfc75af2ee28349b Mon Sep 17 00:00:00 2001 From: Evan Date: Thu, 26 Mar 2026 16:45:13 -0700 Subject: [PATCH 3/7] add not logged in warning to flags modal, refactored to its own lit component (#3521) ## Description: So players know they are logged out and don't think their purchased flags dissappeared. ## 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: evan --- resources/lang/en.json | 2 +- src/client/FlagInputModal.ts | 2 + src/client/Store.ts | 20 +-------- src/client/TerritoryPatternsModal.ts | 20 +-------- src/client/components/NotLoggedInWarning.ts | 49 +++++++++++++++++++++ 5 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 src/client/components/NotLoggedInWarning.ts diff --git a/resources/lang/en.json b/resources/lang/en.json index 931304a3e..341dfaf6e 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -6,6 +6,7 @@ "lang_code": "en" }, "common": { + "not_logged_in": "Not logged in", "close": "Close", "copy": "Copy", "paste": "Paste", @@ -912,7 +913,6 @@ "territory_patterns": { "title": "Skins", "purchase": "Purchase", - "not_logged_in": "Not logged in", "pattern": { "default": "Default" }, diff --git a/src/client/FlagInputModal.ts b/src/client/FlagInputModal.ts index 03a19b69a..ff27763f8 100644 --- a/src/client/FlagInputModal.ts +++ b/src/client/FlagInputModal.ts @@ -9,6 +9,7 @@ import { fetchCosmetics, flagRelationship } from "./Cosmetics"; import { translateText } from "./Utils"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; +import "./components/NotLoggedInWarning"; import { modalHeader } from "./components/ui/ModalHeader"; @customElement("flag-input-modal") @@ -103,6 +104,7 @@ export class FlagInputModal extends BaseModal { title: translateText("flag_input.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), + rightContent: html``, })}
diff --git a/src/client/Store.ts b/src/client/Store.ts index 5cf67cdc8..d6d469518 100644 --- a/src/client/Store.ts +++ b/src/client/Store.ts @@ -5,9 +5,9 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { ColorPalette, Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { UserSettings } from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; -import { hasLinkedAccount } from "./Api"; import { BaseModal } from "./components/BaseModal"; import "./components/FlagButton"; +import "./components/NotLoggedInWarning"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; import { @@ -77,11 +77,7 @@ export class StoreModal extends BaseModal { title: translateText("store.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), - rightContent: !hasLinkedAccount(this.userMeResponse) - ? html`
- ${this.renderNotLoggedInWarning()} -
` - : undefined, + rightContent: html``, })}
`; - } - render() { if (!this.isActive && !this.inline) return html``; diff --git a/src/client/TerritoryPatternsModal.ts b/src/client/TerritoryPatternsModal.ts index 5a13266b0..9d9c995ed 100644 --- a/src/client/TerritoryPatternsModal.ts +++ b/src/client/TerritoryPatternsModal.ts @@ -5,8 +5,8 @@ import { UserMeResponse } from "../core/ApiSchemas"; import { Cosmetics, Pattern } from "../core/CosmeticSchemas"; import { UserSettings } from "../core/game/UserSettings"; import { PlayerPattern } from "../core/Schemas"; -import { hasLinkedAccount } from "./Api"; import { BaseModal } from "./components/BaseModal"; +import "./components/NotLoggedInWarning"; import "./components/PatternButton"; import { modalHeader } from "./components/ui/ModalHeader"; import { @@ -123,18 +123,6 @@ export class TerritoryPatternsModal extends BaseModal { `; } - private renderNotLoggedInWarning(): TemplateResult { - return html``; - } - render() { const content = html`
@@ -145,11 +133,7 @@ export class TerritoryPatternsModal extends BaseModal { title: translateText("territory_patterns.title"), onBack: () => this.close(), ariaLabel: translateText("common.back"), - rightContent: !hasLinkedAccount(this.userMeResponse) - ? html`
- ${this.renderNotLoggedInWarning()} -
` - : undefined, + rightContent: html``, })}
diff --git a/src/client/components/NotLoggedInWarning.ts b/src/client/components/NotLoggedInWarning.ts new file mode 100644 index 000000000..6b972b8f0 --- /dev/null +++ b/src/client/components/NotLoggedInWarning.ts @@ -0,0 +1,49 @@ +import { LitElement, html } from "lit"; +import { customElement, state } from "lit/decorators.js"; +import { UserMeResponse } from "../../core/ApiSchemas"; +import { hasLinkedAccount } from "../Api"; + +@customElement("not-logged-in-warning") +export class NotLoggedInWarning extends LitElement { + @state() private linked = false; + + private _onUserMe = (event: CustomEvent) => { + this.linked = hasLinkedAccount(event.detail); + }; + + createRenderRoot() { + return this; + } + + connectedCallback() { + super.connectedCallback(); + document.addEventListener( + "userMeResponse", + this._onUserMe as EventListener, + ); + } + + disconnectedCallback() { + super.disconnectedCallback(); + document.removeEventListener( + "userMeResponse", + this._onUserMe as EventListener, + ); + } + + render() { + if (this.linked) return html``; + + return html`
+ +
`; + } +} From 7d1ff910781657e9219dce406e13a16ebc684a3b Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Fri, 27 Mar 2026 02:04:33 +0000 Subject: [PATCH 4/7] playerstats to go with infra (#3520) ## Description: https://github.com/openfrontio/infra/pull/279 to go with this, splits out 1v1 ## 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: w.o.n --- resources/lang/en.json | 2 + .../baseComponents/stats/PlayerStatsTree.ts | 96 ++++++++++++++++--- src/core/ApiSchemas.ts | 16 ++-- 3 files changed, 96 insertions(+), 18 deletions(-) diff --git a/resources/lang/en.json b/resources/lang/en.json index 341dfaf6e..e32ce0f9f 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -991,6 +991,8 @@ "public": "Public", "private": "Private", "solo": "Solo", + "ranked": "Ranked", + "ranked_1v1": "1v1", "mode": "Mode", "stats_wins": "Wins", "stats_losses": "Losses", diff --git a/src/client/components/baseComponents/stats/PlayerStatsTree.ts b/src/client/components/baseComponents/stats/PlayerStatsTree.ts index 37166997e..809e482c0 100644 --- a/src/client/components/baseComponents/stats/PlayerStatsTree.ts +++ b/src/client/components/baseComponents/stats/PlayerStatsTree.ts @@ -5,6 +5,7 @@ import { Difficulty, GameMode, GameType, + RankedType, isDifficulty, isGameMode, isGameType, @@ -17,11 +18,12 @@ import "./PlayerStatsTable"; @customElement("player-stats-tree-view") export class PlayerStatsTreeView extends LitElement { @property({ type: Object }) statsTree?: PlayerStatsTree; - @state() selectedType: GameType = GameType.Public; + @state() selectedType: GameType | "Ranked" = GameType.Public; @state() selectedMode: GameMode = GameMode.FFA; @state() selectedDifficulty: Difficulty = Difficulty.Medium; - + @state() selectedRankedType: RankedType = RankedType.OneVOne; private get typeNode() { + if (this.selectedType === "Ranked") return undefined; return this.statsTree?.[this.selectedType]; } @@ -33,9 +35,20 @@ export class PlayerStatsTreeView extends LitElement { return this.selectedType === GameType.Public; } - private get availableTypes(): GameType[] { + private get availableTypes(): (GameType | "Ranked")[] { if (!this.statsTree) return []; - return Object.keys(this.statsTree).filter(isGameType); + const types: (GameType | "Ranked")[] = Object.keys(this.statsTree).filter( + (k): k is GameType => + isGameType(k) && + Object.keys(this.statsTree![k as GameType] ?? {}).length > 0, + ); + if ( + this.statsTree.Ranked && + Object.keys(this.statsTree.Ranked).length > 0 + ) { + types.push("Ranked"); + } + return types; } private get availableModes(): GameMode[] { @@ -43,6 +56,13 @@ export class PlayerStatsTreeView extends LitElement { return Object.keys(this.typeNode).filter(isGameMode); } + private get availableRankedTypes(): RankedType[] { + if (!this.statsTree?.Ranked) return []; + return Object.keys(this.statsTree.Ranked).filter((k): k is RankedType => + Object.values(RankedType).includes(k as RankedType), + ); + } + private get availableDifficulties(): Difficulty[] { if (!this.modeNode) return []; return Object.keys(this.modeNode).filter(isDifficulty); @@ -54,11 +74,22 @@ export class PlayerStatsTreeView extends LitElement { : translateText("game_mode.teams"); } + private labelForRankedType(r: RankedType) { + switch (r) { + case RankedType.OneVOne: + return translateText("player_stats_tree.ranked_1v1"); + } + } + createRenderRoot() { return this; } private getSelectedLeaf(): PlayerStatsLeaf | null { + if (this.selectedType === "Ranked") { + return this.statsTree?.Ranked?.[this.selectedRankedType] ?? null; + } + const modeNode = this.modeNode; if (!modeNode) return null; @@ -91,9 +122,19 @@ export class PlayerStatsTreeView extends LitElement { private syncSelection(): void { const types = this.availableTypes; - if (types.length && !types.includes(this.selectedType)) { + if (types.length && !types.includes(this.selectedType as GameType)) { this.selectedType = types[0]; } + if (this.selectedType === "Ranked") { + const rankedTypes = this.availableRankedTypes; + if ( + rankedTypes.length && + !rankedTypes.includes(this.selectedRankedType) + ) { + this.selectedRankedType = rankedTypes[0]; + } + return; + } const modes = this.availableModes; if (modes.length && !modes.includes(this.selectedMode)) { this.selectedMode = modes[0]; @@ -113,13 +154,14 @@ export class PlayerStatsTreeView extends LitElement { changedProperties.has("statsTree") || changedProperties.has("selectedType") || changedProperties.has("selectedMode") || - changedProperties.has("selectedDifficulty") + changedProperties.has("selectedDifficulty") || + changedProperties.has("selectedRankedType") ) { this.syncSelection(); } } - private setGameType(t: GameType) { + private setGameType(t: GameType | "Ranked") { if (this.selectedType === t) return; this.selectedType = t; this.requestUpdate(); @@ -131,6 +173,12 @@ export class PlayerStatsTreeView extends LitElement { this.requestUpdate(); } + private setRankedType(r: RankedType) { + if (this.selectedRankedType === r) return; + this.selectedRankedType = r; + this.requestUpdate(); + } + private setDifficulty(d: Difficulty) { if (this.selectedDifficulty === d) return; this.selectedDifficulty = d; @@ -215,6 +263,7 @@ export class PlayerStatsTreeView extends LitElement { const types = this.availableTypes; const modes = this.availableModes; const diffs = this.availableDifficulties; + const rankedTypes = this.availableRankedTypes; const leaf = this.getSelectedLeaf(); const wlr = leaf ? leaf.losses === 0n @@ -239,17 +288,40 @@ export class PlayerStatsTreeView extends LitElement { : "bg-white/5 border-white/10 text-gray-400 hover:bg-white/10 hover:text-white"}" @click=${() => this.setGameType(t)} > - ${t === GameType.Public - ? translateText("player_stats_tree.public") - : t === GameType.Private - ? translateText("player_stats_tree.private") - : translateText("player_stats_tree.solo")} + ${t === "Ranked" + ? translateText("player_stats_tree.ranked") + : t === GameType.Public + ? translateText("player_stats_tree.public") + : t === GameType.Private + ? translateText("player_stats_tree.private") + : translateText("player_stats_tree.solo")} `, )}
+ + ${this.selectedType === "Ranked" && rankedTypes.length + ? html`
+ ${rankedTypes.map( + (r) => html` + + `, + )} +
` + : html``} + ${modes.length ? html`
; -export const PlayerStatsTreeSchema = z.partialRecord( - z.enum(GameType), - z.partialRecord( - z.enum(GameMode), - z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema), - ), +const GameModeStatsSchema = z.partialRecord( + z.enum(GameMode), + z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema), ); + +export const PlayerStatsTreeSchema = z.object({ + Singleplayer: GameModeStatsSchema.optional(), + Public: GameModeStatsSchema.optional(), + Private: GameModeStatsSchema.optional(), + Ranked: z.partialRecord(z.enum(RankedType), PlayerStatsLeafSchema).optional(), +}); export type PlayerStatsTree = z.infer; export const PlayerGameSchema = z.object({ From 73016bb56bcc2bc5b0df250e7e4ca06d7ad41e6d Mon Sep 17 00:00:00 2001 From: Evan Date: Fri, 27 Mar 2026 11:57:42 -0700 Subject: [PATCH 5/7] Add bottom left ad in crazygames (#3526) ## Description: If on crazy games, shows an in-game ad on the bottom left corner ## 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: evan --- src/client/CrazyGamesSDK.ts | 76 ++++++++++++++++++++++- src/client/graphics/layers/InGamePromo.ts | 40 +++++++++++- 2 files changed, 113 insertions(+), 3 deletions(-) diff --git a/src/client/CrazyGamesSDK.ts b/src/client/CrazyGamesSDK.ts index 1933074ec..ae2d5bf1d 100644 --- a/src/client/CrazyGamesSDK.ts +++ b/src/client/CrazyGamesSDK.ts @@ -25,6 +25,16 @@ declare global { }, ) => void; }; + banner: { + requestBanner: (options: { + id: string; + width: number; + height: number; + }) => Promise; + requestResponsiveBanner: (containerId: string) => Promise; + clearBanner: (containerId: string) => void; + clearAllBanners: () => void; + }; game: { gameplayStart: () => Promise; gameplayStop: () => Promise; @@ -76,11 +86,9 @@ export class CrazyGamesSDK { } return false; } catch (e) { - console.log("[CrazyGames]: ", e); // If we get a cross-origin error, we're definitely iframed // Check our own referrer as fallback const isCrazyGames = document.referrer.includes("crazygames"); - console.log("[CrazyGames], contains referrer: ", isCrazyGames); if (isCrazyGames) { return true; } @@ -323,6 +331,70 @@ export class CrazyGamesSDK { } } + private bottomLeftContainerId = "cg-bottom-left-ad"; + private bottomLeftAdVisible = false; + + createBottomLeftAd(): void { + console.log( + `[CrazyGames] createBottomLeftAd called, isReady=${this.isReady()}`, + ); + if (!this.isReady()) { + console.log("[CrazyGames] SDK not ready, skipping bottom-left ad"); + return; + } + + if (this.bottomLeftAdVisible) { + console.log("[CrazyGames] Bottom-left ad already visible"); + return; + } + + // Remove existing container if any + document.getElementById(this.bottomLeftContainerId)?.remove(); + + const container = document.createElement("div"); + container.id = this.bottomLeftContainerId; + container.style.cssText = ` + position: fixed; + bottom: 0; + left: 0; + width: 300px; + height: 250px; + z-index: 9999; + pointer-events: auto; + `; + document.body.appendChild(container); + console.log("[CrazyGames] Created bottom-left ad container"); + + (async () => { + try { + await window.CrazyGames!.SDK.banner.requestBanner({ + id: this.bottomLeftContainerId, + width: 300, + height: 250, + }); + console.log("[CrazyGames] Bottom-left banner loaded"); + } catch (e) { + console.log("[CrazyGames] Bottom-left banner error:", e); + } + })(); + + this.bottomLeftAdVisible = true; + } + + clearBottomLeftAd(): void { + if (!this.bottomLeftAdVisible) return; + + try { + window.CrazyGames!.SDK.banner.clearBanner(this.bottomLeftContainerId); + } catch (e) { + console.error("[CrazyGames] Error clearing bottom-left banner:", e); + } + + document.getElementById(this.bottomLeftContainerId)?.remove(); + this.bottomLeftAdVisible = false; + console.log("[CrazyGames] Bottom-left ad cleared"); + } + requestMidgameAd(): Promise { return new Promise((resolve) => { if (!this.isReady()) { diff --git a/src/client/graphics/layers/InGamePromo.ts b/src/client/graphics/layers/InGamePromo.ts index 097bf5bd6..b4d58e237 100644 --- a/src/client/graphics/layers/InGamePromo.ts +++ b/src/client/graphics/layers/InGamePromo.ts @@ -1,6 +1,7 @@ import { LitElement, html } from "lit"; import { customElement } from "lit/decorators.js"; import { GameView } from "../../../core/game/GameView"; +import { crazyGamesSDK } from "../../CrazyGamesSDK"; import { Layer } from "./Layer"; const AD_TYPE = "standard_iab_left1"; @@ -30,6 +31,7 @@ export class InGamePromo extends LitElement implements Layer { } if (!this.cornerAdShown) { this.cornerAdShown = true; + console.log("[InGamePromo] Spawn phase ended, triggering showAd"); this.showAd(); } } @@ -73,10 +75,19 @@ export class InGamePromo extends LitElement implements Layer { } private showAd(): void { - if (!window.adsEnabled) return; + console.log( + `[InGamePromo] showAd called, isOnCrazyGames=${crazyGamesSDK.isOnCrazyGames()}`, + ); if (window.innerWidth < 1100) return; if (window.innerHeight < 750) return; + if (crazyGamesSDK.isOnCrazyGames()) { + this.showCrazyGamesAd(); + return; + } + + if (!window.adsEnabled) return; + this.shouldShow = true; this.requestUpdate(); @@ -85,6 +96,25 @@ export class InGamePromo extends LitElement implements Layer { }); } + private showCrazyGamesAd(): void { + console.log( + `[InGamePromo] showCrazyGamesAd called, isReady=${crazyGamesSDK.isReady()}, width=${window.innerWidth}, height=${window.innerHeight}`, + ); + if (!crazyGamesSDK.isReady()) { + console.log( + "[InGamePromo] CrazyGames SDK not ready, skipping in-game ad", + ); + return; + } + + this.requestUpdate(); + + this.updateComplete.then(() => { + console.log("[InGamePromo] DOM updated, calling createBottomLeftAd"); + crazyGamesSDK.createBottomLeftAd(); + }); + } + private loadAd(): void { if (!window.ramp) { console.warn("Playwire RAMP not available for in-game ad"); @@ -112,6 +142,14 @@ export class InGamePromo extends LitElement implements Layer { public hideAd(): void { this.destroyBottomRail(); + + if (crazyGamesSDK.isOnCrazyGames()) { + crazyGamesSDK.clearBottomLeftAd(); + this.shouldShow = false; + this.requestUpdate(); + return; + } + if (!window.ramp) { console.warn("Playwire RAMP not available for in-game ad"); return; From fabd1a5fa94951663550e8f6186446a4a66442e6 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 30 Mar 2026 12:18:04 -0700 Subject: [PATCH 6/7] Update achievement schema (#3542) ## Description: Update the schema for achievements ## 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: evan --- src/client/SinglePlayerModal.ts | 9 +-------- src/core/ApiSchemas.ts | 11 +++-------- 2 files changed, 4 insertions(+), 16 deletions(-) diff --git a/src/client/SinglePlayerModal.ts b/src/client/SinglePlayerModal.ts index 37f023ca4..4fe22a0d4 100644 --- a/src/client/SinglePlayerModal.ts +++ b/src/client/SinglePlayerModal.ts @@ -144,14 +144,7 @@ export class SinglePlayerModal extends BaseModal { return; } - const achievements = Array.isArray(userMe.player.achievements) - ? userMe.player.achievements - : []; - - const completions = - achievements.find( - (achievement) => achievement?.type === "singleplayer-map", - )?.data ?? []; + const completions = userMe.player.achievements.singleplayerMap; const winsMap = new Map>(); for (const entry of completions) { diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 787bcd802..ca82eb1d9 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -56,14 +56,9 @@ export const UserMeResponseSchema = z.object({ publicId: z.string(), roles: z.string().array().optional(), flares: z.string().array().optional(), - achievements: z - .array( - z.object({ - type: z.literal("singleplayer-map"), // TODO: change the shape to be more flexible when we have more achievements - data: z.array(SingleplayerMapAchievementSchema), - }), - ) - .optional(), + achievements: z.object({ + singleplayerMap: z.array(SingleplayerMapAchievementSchema), + }), leaderboard: z .object({ oneVone: z From 3876967f21e2a71682f3ca5596401abdc8e97983 Mon Sep 17 00:00:00 2001 From: Evan Date: Mon, 30 Mar 2026 12:59:04 -0700 Subject: [PATCH 7/7] Fall back to default Discord avatar when profile image fails to load (#3543) ## Description: The api only refreshes user info every week or two, so when a user changes their profile it image, the api had the reference to the existing image. So for now just load in a default discord icon. ## 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: evan --- src/client/Main.ts | 12 ++---------- .../baseComponents/stats/DiscordUserHeader.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/client/Main.ts b/src/client/Main.ts index 6a134573f..a8331927c 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -101,17 +101,9 @@ function updateAccountNavButton(userMeResponse: UserMeResponse | false) { // If the avatar fails to load (bad URL / CDN issue / offline), fall back // to the default sign-in UI instead of leaving a broken image. avatarEl.onerror = () => { - // Only handle if this is the latest update if (avatarEl._navToken !== navToken) return; - avatarEl.src = ""; - // If the user is still logged in via email, show the email badge state. - const email = - userMeResponse !== false ? userMeResponse.user.email : undefined; - if (email) { - showEmailLoggedIn(); - } else { - showSignIn(); - } + avatarEl.onerror = null; + avatarEl.src = "https://cdn.discordapp.com/embed/avatars/0.png"; }; avatarEl.onload = () => { // Only handle if this is the latest update diff --git a/src/client/components/baseComponents/stats/DiscordUserHeader.ts b/src/client/components/baseComponents/stats/DiscordUserHeader.ts index 526835203..f223a0805 100644 --- a/src/client/components/baseComponents/stats/DiscordUserHeader.ts +++ b/src/client/components/baseComponents/stats/DiscordUserHeader.ts @@ -31,15 +31,20 @@ export class DiscordUserHeader extends LitElement { } render() { + const defaultAvatar = "https://cdn.discordapp.com/embed/avatars/0.png"; + const imgSrc = this.avatarUrl ?? defaultAvatar; return html`
- ${this.avatarUrl + ${this._data ? html`
${translateText( { + (e.target as HTMLImageElement).src = defaultAvatar; + }} />
`