Skip to content

Refactor modal system: BaseModal renders shell, unified open(args) API#3923

Merged
evanpelle merged 1 commit into
mainfrom
modal-refactor
May 14, 2026
Merged

Refactor modal system: BaseModal renders shell, unified open(args) API#3923
evanpelle merged 1 commit into
mainfrom
modal-refactor

Conversation

@evanpelle
Copy link
Copy Markdown
Collaborator

@evanpelle evanpelle commented May 14, 2026

Description

Refactors the modal system so that BaseModal owns the <o-modal> shell rendering, tab state, and lifecycle. Modal subclasses now provide content via small hook methods (renderHeaderSlot(), renderBody(tab), modalConfig()) instead of each rebuilding the <o-modal> template and inline-mode branching.

This sets up the foundation for a future modal URL router (e.g. #modal=store&tab=flags), which will be a follow-up PR.

What changed

BaseModalsrc/client/components/BaseModal.ts

  • Now renders the <o-modal> shell itself; subclasses no longer duplicate it
  • Owns activeTab state and dispatches per-tab rendering via renderBody(tab)
  • Single modalConfig() method returns { title?, tabs?, hideHeader?, hideCloseButton?, alwaysMaximized?, maxWidth? }
  • Uniform open(args?) / close(args?) interface; subclasses interpret args in onOpen(args) / onClose(args)
  • Tabbed modals can lazy-load via onTabEnter(tab) lifecycle hook
  • Re-entrancy guard on open() so showPage() re-invocations don't clobber state set by the outer call
  • Initial tab defaults to first entry in modalConfig().tabs so the active tab is highlighted on first open

17 modals migrated to the new shape:

  • Tabbed: Store, UserSetting, Leaderboard, Clan
  • Non-tabbed: FlagInput, Account, TokenLogin, News, TerritoryPatterns, Troubleshooting, SinglePlayer, Matchmaking, RankedModal, Help, Language
  • Lobby: JoinLobbyModal, HostLobbyModal (kept their confirmBeforeClose / closeAndLeave / closeWithoutLeaving methods)

Per-modal diffs are mostly mechanical:

  • Drop the <o-modal> wrapper template and the if (this.inline) return content branch
  • Drop the inner <div class="${this.modalContainerClass}"> wrapper (shell styling now lives on <o-modal>)
  • Move header content into renderHeaderSlot() so it lives in the sticky header area
  • Convert super.open()/super.close() overrides into onOpen(args)/onClose(args) hooks
  • For tabbed modals: drop subclass @state activeTab, manual handleTabChange, and the render() switch — all owned by BaseModal now

Other changes:

  • Store: in affiliate mode (#affiliate=X), tabs are hidden and a single combined grid of purchasable affiliate items is shown
  • Main.ts: joinModal.open(lobbyId, lobbyInfo) callsites converted to the new open({ lobbyId, lobbyInfo }) shape

Follow-up

Modal URL router (#modal=X&tab=Y&...) is a separate PR on top of this foundation.

Please complete the following:

  • I have added screenshots for all UI updates (no visual changes; smoke-tested in dev)
  • I process any text displayed to the user through translateText() and I've added it to the en.json file (no new user-visible strings)
  • I have added relevant tests to the test directory (no test coverage; tested in browser)
  • 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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 14, 2026

Review Change Stack

Walkthrough

This PR refactors multiple modal components to use BaseModal hooks (modalConfig, renderHeaderSlot, renderBody, onOpen/onClose/onTabEnter) and updates modal lifecycle/close handling and token-based opening flows. Callsites were updated to pass option objects into open(args).

Changes

Modal refactor to hook-based rendering and lifecycle

Layer / File(s) Summary
Matchmaking modal slot extraction
src/client/Matchmaking.ts
Replaces render() with renderHeaderSlot() and renderBody(); centers Elo and inner content and wires modalHeader.
Troubleshooting modal header/body and close behavior
src/client/TroubleshootingModal.ts
Removes old base modal import; adds renderHeaderSlot() and renderBody() for diagnostics UI; updates close() to unregister escape handler and handle inline navigation back to page-help.
TokenLogin modal lifecycle and slots
src/client/TokenLoginModal.ts
Adds modalConfig, renderHeaderSlot, renderBody; introduces openWithToken(); gates open(args) on token presence and moves retry interval management into onOpen/onClose with state reset.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"Modals traded their single song,
Hooks now guide the header and the throng.
Open brings an args-filled hand,
Token waits to make its stand.
Close cleans up and sends us home."

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main refactoring: centralizing modal shell rendering in BaseModal and unifying the open/close API across all modal subclasses.
Description check ✅ Passed The description is well-organized and directly related to the changeset, explaining the BaseModal refactoring, listing the 17 migrated modals, detailing mechanical changes, and noting follow-up plans.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinLobbyModal.ts (1)

108-108: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add missing modalContainerClass property to BaseModal.

The code references this.modalContainerClass but TypeScript compilation fails because this property is not defined in BaseModal or accessible to any subclass. This blocks deployment across all modal components.

The property is used in 8 files (JoinLobbyModal at lines 108 and 227, plus Matchmaking, RankedModal, SinglePlayerModal, HostLobbyModal, AccountModal). Define modalContainerClass as a protected property in BaseModal with a value containing appropriate CSS classes for modal container styling.

Also applies to: 227-227, and Matchmaking.ts:39, RankedModal.ts:83, SinglePlayerModal.ts:228, HostLobbyModal.ts:262, AccountModal.ts:76, AccountModal.ts:107

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/JoinLobbyModal.ts` at line 108, Add a protected property
modalContainerClass to the BaseModal class so subclasses (JoinLobbyModal,
Matchmaking, RankedModal, SinglePlayerModal, HostLobbyModal, AccountModal) can
access it; declare it as protected modalContainerClass: string = '<appropriate
CSS classes for modal container styling>'; update BaseModal (the class defining
common modal behavior and methods like render()/template rendering) to include
this property so references like this.modalContainerClass compile cleanly across
all modal components.
🧹 Nitpick comments (5)
src/client/JoinLobbyModal.ts (1)

289-291: ⚖️ Poor tradeoff

Track the signature migration to completion.

The @ts-expect-error suppression and comment document intentional technical debt during the modal refactor rollout. Ensure this migration to open({ lobbyId, lobbyInfo }) is tracked and completed to avoid leaving partial migration state in the codebase.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/JoinLobbyModal.ts` around lines 289 - 291, The JoinLobbyModal.open
method currently uses a legacy positional signature and suppresses TypeScript
errors; update the API to the new object signature open({ lobbyId, lobbyInfo })
by changing the JoinLobbyModal.open declaration accordingly (removing the
`@ts-expect-error`), update its parameter typing to accept a single object with
lobbyId: string and lobbyInfo?: GameInfo | PublicGameInfo, and then find and
update all callers that invoke JoinLobbyModal.open(...) to pass an object
instead of positional args; also adjust any overrides of BaseModal.open to match
the BaseModal.open(args) signature and remove this migration comment/TODO once
callers and types compile cleanly.
src/client/TroubleshootingModal.ts (1)

215-226: ⚡ Quick win

Consider using onClose() hook instead of overriding close().

The current close() override duplicates logic from BaseModal.close() (unregister handler, call onClose()). This creates maintenance risk if the base implementation changes.

Since the only difference is navigating to "page-help" instead of "page-play", consider:

  • Moving the showPage logic to onClose()
  • Adding a protected getInlineReturnPage() hook that defaults to "page-play" and returns "page-help" here
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/TroubleshootingModal.ts` around lines 215 - 226, The
TroubleshootingModal.close() override duplicates BaseModal.close() behavior
(unregisterEscapeHandler and onClose), which risks divergence; instead remove
the override and implement the navigation change in onClose() or add a protected
hook getInlineReturnPage() used by BaseModal.close(); specifically, update
BaseModal.close() to call this.getInlineReturnPage() when determing which page
to show for inline modals, add a protected getInlineReturnPage() defaulting to
"page-play", and override getInlineReturnPage() in TroubleshootingModal to
return "page-help" (or move the window.showPage("page-help") into
TroubleshootingModal.onClose()), so you only change the return page logic
without duplicating close() implementation.
src/client/LeaderboardModal.ts (1)

43-70: 💤 Low value

Note: activeTab type safety reduced.

The refactor removes the local typed declaration activeTab: "players" | "clans" and relies on BaseModal.activeTab: string. While BaseModal.setActiveTab() validates keys at runtime, TypeScript can no longer catch invalid tab references at compile time.

Consider adding a generic type parameter to BaseModal<TabKey extends string> for better type safety across modal implementations.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/LeaderboardModal.ts` around lines 43 - 70, The refactor lost
compile-time safety for the tab keys because LeaderboardModal now inherits
BaseModal.activeTab as a plain string; make BaseModal generic (e.g.
BaseModal<TabKey extends string>) and update its setActiveTab and activeTab
declarations to use that generic so modal subclasses can constrain keys; then
change LeaderboardModal to extend BaseModal<'players' | 'clans'> (and update any
other modal classes similarly) so activeTab, setActiveTab, and local checks in
functions like loadActiveTabData remain type-safe and TypeScript will catch
invalid tab references.
src/client/TokenLoginModal.ts (1)

88-105: 💤 Low value

Silent no-op when open() is called without a token may surprise callers.

If something other than openWithToken() invokes open(), it returns silently with no log or thrown error. That makes integration bugs hard to spot. A small console.warn (or an explicit throw) would make the contract clearer, since openWithToken() is the only intended entry point.

🧹 Suggested tiny diagnostic
   public open(args?: Record<string, unknown>): void {
     if (!this.token) {
+      console.warn("TokenLoginModal.open() called without a token; use openWithToken() instead.");
       return;
     }
     super.open(args);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/TokenLoginModal.ts` around lines 88 - 105, The open() method in
TokenLoginModal currently no-ops when this.token is falsy, which hides misuse;
change it to surface the issue by either logging a warning or throwing an error:
modify TokenLoginModal.open to detect missing token and call console.warn (or
throw a descriptive Error) explaining that openWithToken() must be used (include
the class/method name in the message), then return/throw; keep the existing
super.open(args) path when this.token exists and do not alter onOpen/onClose
behavior.
src/client/UserSettingModal.ts (1)

351-353: 💤 Low value

Listener lifecycle is balanced — small note on stale keySequence.

The keydown listener is added in onOpen and removed in both onClose and disconnectedCallback, which is clean. One small thing: keySequence is not reset on close, so a partial sequence (e.g. "ev") from one session bleeds into the next. Easy to clear in onClose if you want each open to start fresh.

🧹 Optional reset on close
   protected onClose(): void {
     window.removeEventListener("keydown", this.handleEasterEggKey);
+    this.keySequence = [];
   }

Also applies to: 932-935

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/UserSettingModal.ts` around lines 351 - 353, The keySequence
string used by the Easter-egg keyboard handler (handleEasterEggKey) is not reset
when the modal closes, so partial sequences persist across opens; update
UserSettingModal.onClose (and similarly in disconnectedCallback if present) to
reset keySequence to an empty string (e.g., this.keySequence = "") in addition
to removing the window keydown listener, ensuring each onOpen starts with a
fresh sequence.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/components/BaseModal.ts`:
- Around line 96-98: The render() method currently mutates state by assigning
this.activeTab when tabs exist; remove that side-effect from render() and ensure
initialization happens in a lifecycle or the existing open() method instead.
Specifically, delete the assignment block in render() that sets this.activeTab =
tabs[0].key and either add the initialization to willUpdate() (checking
tabs.length && this.activeTab === "") or rely solely on the existing logic in
open() (lines that set this.activeTab) so state changes do not occur during
render().

---

Outside diff comments:
In `@src/client/JoinLobbyModal.ts`:
- Line 108: Add a protected property modalContainerClass to the BaseModal class
so subclasses (JoinLobbyModal, Matchmaking, RankedModal, SinglePlayerModal,
HostLobbyModal, AccountModal) can access it; declare it as protected
modalContainerClass: string = '<appropriate CSS classes for modal container
styling>'; update BaseModal (the class defining common modal behavior and
methods like render()/template rendering) to include this property so references
like this.modalContainerClass compile cleanly across all modal components.

---

Nitpick comments:
In `@src/client/JoinLobbyModal.ts`:
- Around line 289-291: The JoinLobbyModal.open method currently uses a legacy
positional signature and suppresses TypeScript errors; update the API to the new
object signature open({ lobbyId, lobbyInfo }) by changing the
JoinLobbyModal.open declaration accordingly (removing the `@ts-expect-error`),
update its parameter typing to accept a single object with lobbyId: string and
lobbyInfo?: GameInfo | PublicGameInfo, and then find and update all callers that
invoke JoinLobbyModal.open(...) to pass an object instead of positional args;
also adjust any overrides of BaseModal.open to match the BaseModal.open(args)
signature and remove this migration comment/TODO once callers and types compile
cleanly.

In `@src/client/LeaderboardModal.ts`:
- Around line 43-70: The refactor lost compile-time safety for the tab keys
because LeaderboardModal now inherits BaseModal.activeTab as a plain string;
make BaseModal generic (e.g. BaseModal<TabKey extends string>) and update its
setActiveTab and activeTab declarations to use that generic so modal subclasses
can constrain keys; then change LeaderboardModal to extend BaseModal<'players' |
'clans'> (and update any other modal classes similarly) so activeTab,
setActiveTab, and local checks in functions like loadActiveTabData remain
type-safe and TypeScript will catch invalid tab references.

In `@src/client/TokenLoginModal.ts`:
- Around line 88-105: The open() method in TokenLoginModal currently no-ops when
this.token is falsy, which hides misuse; change it to surface the issue by
either logging a warning or throwing an error: modify TokenLoginModal.open to
detect missing token and call console.warn (or throw a descriptive Error)
explaining that openWithToken() must be used (include the class/method name in
the message), then return/throw; keep the existing super.open(args) path when
this.token exists and do not alter onOpen/onClose behavior.

In `@src/client/TroubleshootingModal.ts`:
- Around line 215-226: The TroubleshootingModal.close() override duplicates
BaseModal.close() behavior (unregisterEscapeHandler and onClose), which risks
divergence; instead remove the override and implement the navigation change in
onClose() or add a protected hook getInlineReturnPage() used by
BaseModal.close(); specifically, update BaseModal.close() to call
this.getInlineReturnPage() when determing which page to show for inline modals,
add a protected getInlineReturnPage() defaulting to "page-play", and override
getInlineReturnPage() in TroubleshootingModal to return "page-help" (or move the
window.showPage("page-help") into TroubleshootingModal.onClose()), so you only
change the return page logic without duplicating close() implementation.

In `@src/client/UserSettingModal.ts`:
- Around line 351-353: The keySequence string used by the Easter-egg keyboard
handler (handleEasterEggKey) is not reset when the modal closes, so partial
sequences persist across opens; update UserSettingModal.onClose (and similarly
in disconnectedCallback if present) to reset keySequence to an empty string
(e.g., this.keySequence = "") in addition to removing the window keydown
listener, ensuring each onOpen starts with a fresh sequence.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1ffd9e2-ee86-470b-aa95-afceff304ea9

📥 Commits

Reviewing files that changed from the base of the PR and between e0f7359 and 202845d.

📒 Files selected for processing (14)
  • src/client/ClanModal.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/JoinLobbyModal.ts
  • src/client/LanguageModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Main.ts
  • src/client/NewsModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/BaseModal.ts

Comment thread src/client/components/BaseModal.ts
@github-project-automation github-project-automation Bot moved this from Triage to Development in OpenFront Release Management May 14, 2026
@evanpelle evanpelle changed the title modal refactor Refactor modal system: BaseModal renders shell, unified open(args) API May 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/client/JoinLobbyModal.ts (1)

220-260: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Extra </div> in the join form template.

After the refactor that removed the outer wrapper, line 258 still has a </div> that has no matching opening <div> inside the <form>. Counting tags from line 221:

  • <form><div flex-col gap-3> (A) → <div flex gap-2> (B) → input + paste <o-button></div> closes B → submit <o-button></div> closes A → </div> (extra) → </form>.

Browsers/Lit's parser may silently absorb the stray close tag, but it's still malformed markup and easy to break the layout on a future tweak. Please remove it.

🐛 Proposed fix
       </o-button>
             </div>
-        </div>
       </form>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/JoinLobbyModal.ts` around lines 220 - 260, The template returned
in JoinLobbyModal's render contains an extra closing </div> inside the form
(after the submit <o-button>), which produces mismatched tags; remove that stray
</div> so the form structure is: <form> → div.flex-col (A) → div.flex (B) →
</div> (closes B) → submit <o-button> → </div> (closes A) → </form>; update the
template in the component (where joinLobbyFromInput, pasteFromClipboard and the
lobbyIdInput are defined) to drop the redundant closing tag.
🧹 Nitpick comments (1)
src/client/JoinLobbyModal.ts (1)

263-273: ⚡ Quick win

Create a typed interface for JoinLobbyModal.open() args to eliminate the unchecked cast.

The current code uses as GameInfo | PublicGameInfo | undefined without type safety, and Record<string, unknown> obscures the contract. Main.ts calls .open() with either { lobbyId } or { lobbyId, lobbyInfo }, so a local typed interface keeps the code self-documenting and removes the cast.

Suggested change
+interface JoinLobbyModalOpenArgs {
+  lobbyId?: string;
+  lobbyInfo?: GameInfo | PublicGameInfo;
+}
+
-  protected onOpen(args?: Record<string, unknown>): void {
-    const lobbyId = typeof args?.lobbyId === "string" ? args.lobbyId : "";
-    const lobbyInfo = args?.lobbyInfo as GameInfo | PublicGameInfo | undefined;
+  protected onOpen(args?: JoinLobbyModalOpenArgs): void {
+    const lobbyId = args?.lobbyId ?? "";
+    const lobbyInfo = args?.lobbyInfo;
     if (lobbyId) {

If other modals also use structured args (like Store.ts with affiliateCode), apply the same pattern locally to each. Making BaseModal.onOpen generic across all modals would be a larger refactor—worth considering only if this pattern becomes widespread.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/JoinLobbyModal.ts` around lines 263 - 273, Replace the untyped
args and unsafe cast in JoinLobbyModal.onOpen with a dedicated typed interface
(e.g., JoinLobbyModalOpenArgs { lobbyId?: string; lobbyInfo?: GameInfo |
PublicGameInfo }) and update the method signature to onOpen(args?:
JoinLobbyModalOpenArgs): void so you can read args.lobbyInfo without using "as";
keep the existing logic that derives lobbyId, calls startTrackingLobby(lobbyId,
lobbyInfo) and conditionally calls handleUrlJoin(lobbyId) when lobbyInfo is
absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/JoinLobbyModal.ts`:
- Around line 80-97: The header always uses "public_lobby.title" after
currentLobbyId is set; update renderHeaderSlot to pick the title based on
isPrivateLobby() when currentLobbyId exists: call modalHeader with title set to
translateText("private_lobby.title") if this.isPrivateLobby() is true, otherwise
translateText("public_lobby.title"), keeping the same onBack, ariaLabel, and
rightContent logic (rightContent stays the copy-button when this.currentLobbyId
&& this.isPrivateLobby()) so the header reflects private vs public lobbies
correctly.

---

Outside diff comments:
In `@src/client/JoinLobbyModal.ts`:
- Around line 220-260: The template returned in JoinLobbyModal's render contains
an extra closing </div> inside the form (after the submit <o-button>), which
produces mismatched tags; remove that stray </div> so the form structure is:
<form> → div.flex-col (A) → div.flex (B) → </div> (closes B) → submit <o-button>
→ </div> (closes A) → </form>; update the template in the component (where
joinLobbyFromInput, pasteFromClipboard and the lobbyIdInput are defined) to drop
the redundant closing tag.

---

Nitpick comments:
In `@src/client/JoinLobbyModal.ts`:
- Around line 263-273: Replace the untyped args and unsafe cast in
JoinLobbyModal.onOpen with a dedicated typed interface (e.g.,
JoinLobbyModalOpenArgs { lobbyId?: string; lobbyInfo?: GameInfo | PublicGameInfo
}) and update the method signature to onOpen(args?: JoinLobbyModalOpenArgs):
void so you can read args.lobbyInfo without using "as"; keep the existing logic
that derives lobbyId, calls startTrackingLobby(lobbyId, lobbyInfo) and
conditionally calls handleUrlJoin(lobbyId) when lobbyInfo is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a2760d61-da5c-4f23-9596-06d74f3258df

📥 Commits

Reviewing files that changed from the base of the PR and between 202845d and fd1b910.

📒 Files selected for processing (19)
  • src/client/AccountModal.ts
  • src/client/ClanModal.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/JoinLobbyModal.ts
  • src/client/LanguageModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/NewsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/BaseModal.ts
  • src/client/components/RankedModal.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • src/client/LanguageModal.ts
  • src/client/NewsModal.ts
  • src/client/UserSettingModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/components/BaseModal.ts

Comment thread src/client/JoinLobbyModal.ts
@evanpelle evanpelle marked this pull request as ready for review May 14, 2026 22:26
@evanpelle evanpelle requested a review from a team as a code owner May 14, 2026 22:26
@evanpelle evanpelle added this to the v32 milestone May 14, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/client/TokenLoginModal.ts (1)

95-98: ⚡ Quick win

Start first login attempt immediately on open.

Line 97 delays the first auth try by 3 seconds every time. Call tryLogin() once in onOpen() before the interval starts.

Proposed change
  protected onOpen(): void {
    clearInterval(this.retryInterval);
+   void this.tryLogin();
    this.retryInterval = setInterval(() => this.tryLogin(), 3000);
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/TokenLoginModal.ts` around lines 95 - 98, Call tryLogin
immediately when the modal opens: in the onOpen() method (which currently clears
this.retryInterval and then sets this.retryInterval = setInterval(() =>
this.tryLogin(), 3000)), invoke this.tryLogin() once before creating the
interval so the first authentication attempt runs right away, then start the
periodic retries with setInterval as before; ensure this.retryInterval is still
cleared at the start to avoid duplicate intervals.
src/client/TroubleshootingModal.ts (1)

216-225: ⚡ Quick win

Use super.close() for the non-inline path to keep lifecycle behavior centralized.

The non-inline branch reimplements modal closing directly (this.modalEl?.close()). Delegating to super.close() keeps this modal aligned with BaseModal lifecycle behavior and reduces drift risk as the shared modal API evolves.

Suggested refactor
public close(): void {
-  this.unregisterEscapeHandler();
-  this.onClose();
   if (this.inline) {
+    this.unregisterEscapeHandler();
+    this.onClose();
     this.style.pointerEvents = "none";
     window.showPage?.("page-help");
   } else {
-    this.modalEl?.close();
+    super.close();
   }
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/TroubleshootingModal.ts` around lines 216 - 225, Replace the
direct DOM close with the base-class lifecycle: in the else branch, call
super.close() instead of this.modalEl?.close(); move the
this.unregisterEscapeHandler() and this.onClose() calls so they run only for the
inline path (where you still need to set this.style.pointerEvents and
window.showPage("page-help")), avoiding duplicate lifecycle invocation if
BaseModal.close() already handles unregistering and onClose behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/client/TroubleshootingModal.ts`:
- Around line 58-60: In TroubleshootingModal.renderBody(), returning html`` when
this.loading is true leaves the modal blank; replace that early return with a
visible loading placeholder (e.g., spinner + "Loading diagnostics..." text)
rendered via the same templating approach and ensure the visible text is wrapped
in translateText("...") and a matching key/value is added to
resources/lang/en.json; update any UI element IDs/classes as needed but keep the
guard on this.loading and render the placeholder inside renderBody() rather than
returning an empty template.

---

Nitpick comments:
In `@src/client/TokenLoginModal.ts`:
- Around line 95-98: Call tryLogin immediately when the modal opens: in the
onOpen() method (which currently clears this.retryInterval and then sets
this.retryInterval = setInterval(() => this.tryLogin(), 3000)), invoke
this.tryLogin() once before creating the interval so the first authentication
attempt runs right away, then start the periodic retries with setInterval as
before; ensure this.retryInterval is still cleared at the start to avoid
duplicate intervals.

In `@src/client/TroubleshootingModal.ts`:
- Around line 216-225: Replace the direct DOM close with the base-class
lifecycle: in the else branch, call super.close() instead of
this.modalEl?.close(); move the this.unregisterEscapeHandler() and
this.onClose() calls so they run only for the inline path (where you still need
to set this.style.pointerEvents and window.showPage("page-help")), avoiding
duplicate lifecycle invocation if BaseModal.close() already handles
unregistering and onClose behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cb457bdc-60cb-4234-9748-c4ced0760d14

📥 Commits

Reviewing files that changed from the base of the PR and between fd1b910 and 57b86a0.

📒 Files selected for processing (19)
  • src/client/AccountModal.ts
  • src/client/ClanModal.ts
  • src/client/FlagInputModal.ts
  • src/client/HelpModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/JoinLobbyModal.ts
  • src/client/LanguageModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/Main.ts
  • src/client/Matchmaking.ts
  • src/client/NewsModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/Store.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/TokenLoginModal.ts
  • src/client/TroubleshootingModal.ts
  • src/client/UserSettingModal.ts
  • src/client/components/BaseModal.ts
  • src/client/components/RankedModal.ts
🚧 Files skipped from review as they are similar to previous changes (16)
  • src/client/Main.ts
  • src/client/TerritoryPatternsModal.ts
  • src/client/NewsModal.ts
  • src/client/FlagInputModal.ts
  • src/client/components/RankedModal.ts
  • src/client/Store.ts
  • src/client/UserSettingModal.ts
  • src/client/AccountModal.ts
  • src/client/LeaderboardModal.ts
  • src/client/HelpModal.ts
  • src/client/HostLobbyModal.ts
  • src/client/SinglePlayerModal.ts
  • src/client/JoinLobbyModal.ts
  • src/client/LanguageModal.ts
  • src/client/components/BaseModal.ts
  • src/client/ClanModal.ts

Comment on lines +58 to 60
protected renderBody() {
if (this.loading) return html``;
return html`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Show a visible loading state instead of an empty body.

Line 59 returns `html```, so users see a blank modal while diagnostics load. Add a small loading placeholder/spinner to avoid “stuck” perception.

As per coding guidelines, src/client/**/*.{ts,tsx} requires all user-visible text to go through translateText() with a matching entry in resources/lang/en.json.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/client/TroubleshootingModal.ts` around lines 58 - 60, In
TroubleshootingModal.renderBody(), returning html`` when this.loading is true
leaves the modal blank; replace that early return with a visible loading
placeholder (e.g., spinner + "Loading diagnostics..." text) rendered via the
same templating approach and ensure the visible text is wrapped in
translateText("...") and a matching key/value is added to
resources/lang/en.json; update any UI element IDs/classes as needed but keep the
guard on this.loading and render the placeholder inside renderBody() rather than
returning an empty template.

@evanpelle evanpelle merged commit bbe727c into main May 14, 2026
13 of 18 checks passed
@evanpelle evanpelle deleted the modal-refactor branch May 14, 2026 22:33
@github-project-automation github-project-automation Bot moved this from Development to Complete in OpenFront Release Management May 14, 2026
evanpelle added a commit that referenced this pull request May 14, 2026
## Description

Adds a modal URL router so modals can be opened, deep-linked, and
bookmarked via the hash. URLs of the form `#modal=<name>&tab=<key>&...`
open the named modal and pass remaining keys as args to `onOpen`. The
reverse direction also syncs: opening a modal via the UI updates the
URL, closing it clears the hash, and switching tabs updates `&tab=`.

Builds on the BaseModal refactor from #3923.

### What's new

**`ModalRouter.ts`** — small registry + two-way sync helper.
- `register(name, { tag, pageId? })` declares a modal as router-managed
- `routeFromHash()` parses `#modal=...` and dispatches to
`modal.open(args)`
- `syncOpened/syncClosed/syncTab` push state back into the URL via
`history.replaceState` (no history entries)
- A `routingFromUrl` flag prevents URL→modal→URL feedback loops
- Unknown modal names silently strip the hash

**`BaseModal`** — opt-in URL sync via a `routerName` property.
- When set, BaseModal calls into
`modalRouter.syncOpened/syncClosed/syncTab` from `open` / `close` /
`setActiveTab`
- Modals that own their own URL state (lobby modals) just leave
`routerName` undefined

**`Main.ts`** — registers all routable modals and wires the router.
- `handleUrl()`: adds a `modalRouter.routeFromHash()` branch after the
path-based lobby join
- `onHashUpdate`: when the hash is router-managed, routes via the router
instead of tearing down lobby state

### Routable modals

13 inline modals: store, settings, leaderboard, clan, account, help,
news, language, single-player, ranked, troubleshooting,
territory-patterns, flag-input.

Excluded by design: join-lobby, host-lobby (own URL state via
`/game/<id>`), matchmaking (no URL state).

### Example uses

- Deep link to store flags tab: `/#modal=store&tab=flags`
- Settings keybinds tab: `/#modal=settings&tab=keybinds`
- Cosmetics.ts now redirects to `#modal=store&tab=packs` when a
hard-currency purchase fails for insufficient plutonium (after the
alert), so users can top up directly

### URL behavior

- `replaceState` everywhere — no history entries added when modals open
/ close / switch tabs
- Browser back/forward still works for the existing path-based game flow
- `hashchange` events are router-aware so external hash changes (back
button, manual edit) correctly switch between routed modals without
tearing down lobby state

## Please complete the following:

- [x] I have added screenshots for all UI updates _(no visual changes;
smoke-tested in dev)_
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file _(no new user-visible strings)_
- [ ] I have added relevant tests to the test directory _(no test
coverage; manually tested URL load, UI open, tab switch, close,
hashchange, insufficient-plutonium redirect)_
- [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:

DISCORD_USERNAME
@coderabbitai coderabbitai Bot mentioned this pull request May 16, 2026
4 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Complete

Development

Successfully merging this pull request may close these issues.

1 participant