From ec4ef01d99f306ea3a15d39a2b29342305053522 Mon Sep 17 00:00:00 2001 From: Scott Anderson Date: Mon, 16 Jun 2025 21:45:53 -0400 Subject: [PATCH] Hash-based routing (#1198) ## Description: Implement hash-based routing. Fixes #1111 ## 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 - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com> --- package-lock.json | 6 ----- package.json | 1 - src/client/HostLobbyModal.ts | 2 +- src/client/Main.ts | 43 ++++++++++++++++++++++++------------ src/client/jwt.ts | 6 ++++- src/core/Schemas.ts | 2 +- 6 files changed, 36 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 74acea471..0f177807b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,6 @@ "node-addon-api": "^8.1.0", "node-gyp": "^10.2.0", "obscenity": "^0.4.3", - "page": "^1.3.7", "pg": "^8.13.3", "priority-queue-typescript": "^1.0.1", "prom-client": "^15.1.3", @@ -17919,11 +17918,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/page": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/page/-/page-1.3.7.tgz", - "integrity": "sha512-1MzNKSvcVePQDErGsfK22xmtdD8AQNj5g8U3OWUJJdlP5wd7yVxCLFbJutMkI5j9pRT/ZCn5kS8Rr6em6LIXsA==" - }, "node_modules/pako": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", diff --git a/package.json b/package.json index 1ab7dee0f..5852cbac5 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,6 @@ "node-addon-api": "^8.1.0", "node-gyp": "^10.2.0", "obscenity": "^0.4.3", - "page": "^1.3.7", "pg": "^8.13.3", "priority-queue-typescript": "^1.0.1", "prom-client": "^15.1.3", diff --git a/src/client/HostLobbyModal.ts b/src/client/HostLobbyModal.ts index 41447146f..2a71ec7be 100644 --- a/src/client/HostLobbyModal.ts +++ b/src/client/HostLobbyModal.ts @@ -538,7 +538,7 @@ export class HostLobbyModal extends LitElement { try { //TODO: Convert id to url and copy await navigator.clipboard.writeText( - `${location.origin}/join/${this.lobbyId}`, + `${location.origin}#join=${this.lobbyId}`, ); this.copySuccess = true; setTimeout(() => { diff --git a/src/client/Main.ts b/src/client/Main.ts index cab14d6a0..f3e0598ac 100644 --- a/src/client/Main.ts +++ b/src/client/Main.ts @@ -1,6 +1,5 @@ -import page from "page"; import favicon from "../../resources/images/Favicon.svg"; -import { GameRecord, GameStartInfo } from "../core/Schemas"; +import { GameRecord, GameStartInfo, ID } from "../core/Schemas"; import { getServerConfigFromClient } from "../core/configuration/ConfigLoader"; import { GameType } from "../core/game/Game"; import { UserSettings } from "../core/game/UserSettings"; @@ -247,20 +246,25 @@ class Client { } else { document.documentElement.classList.remove("dark"); } - page("/join/:lobbyId", (ctx) => { - if (ctx.init && sessionStorage.getItem("inLobby")) { - // On page reload, go back home - page("/"); - return; + + // Attempt to join lobby + this.handleHash(); + + const onHashUpdate = () => { + // Reset the UI to its initial state + this.joinModal.close(); + if (this.gameStop !== null) { + this.handleLeaveLobby(); } - const lobbyId = ctx.params.lobbyId; - this.joinModal.open(lobbyId); + // Attempt to join lobby + this.handleHash(); + }; - console.log(`joining lobby ${lobbyId}`); - }); + // Handle browser navigation & manual hash edits + window.addEventListener("popstate", onHashUpdate); + window.addEventListener("hashchange", onHashUpdate); - page(); function updateSliderProgress(slider) { const percent = ((slider.value - slider.min) / (slider.max - slider.min)) * 100; @@ -275,6 +279,18 @@ class Client { }); } + private handleHash() { + const { hash } = window.location; + if (hash.startsWith("#")) { + const params = new URLSearchParams(hash.slice(1)); + const lobbyId = params.get("join"); + if (lobbyId && ID.safeParse(lobbyId).success) { + this.joinModal.open(lobbyId); + console.log(`joining lobby ${lobbyId}`); + } + } + } + private async handleJoinLobby(event: CustomEvent) { const lobby = event.detail as JoinLobbyEvent; console.log(`joining lobby ${lobby.gameID}`); @@ -343,8 +359,7 @@ class Client { }); if (event.detail.gameConfig?.gameType !== GameType.Singleplayer) { - window.history.pushState({}, "", `/join/${lobby.gameID}`); - sessionStorage.setItem("inLobby", "true"); + history.pushState(null, "", `#join=${lobby.gameID}`); } }, ); diff --git a/src/client/jwt.ts b/src/client/jwt.ts index fcb3b8dac..684a737dc 100644 --- a/src/client/jwt.ts +++ b/src/client/jwt.ts @@ -28,12 +28,16 @@ function getToken(): string | null { const token = params.get("token"); if (token) { localStorage.setItem("token", token); + params.delete("token"); + params.toString(); } // Clean the URL history.replaceState( null, "", - window.location.pathname + window.location.search, + window.location.pathname + + window.location.search + + (params.size > 0 ? "#" + params.toString() : ""), ); } return localStorage.getItem("token"); diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index bc6903bf5..471dd647f 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -161,7 +161,7 @@ const EmojiSchema = z .number() .nonnegative() .max(flattenedEmojiTable.length - 1); -const ID = z +export const ID = z .string() .regex(/^[a-zA-Z0-9]+$/) .length(8);