Discord(et al.) embedded URLs (#2740)

## Description:

Changes URL embeds within other platforms, e.g. Discord, WhatsApp & X.

Updates game URLs to `/game/<code>` instead of `/#join=<code>` (required
for embedded URLs). An added benefit of this is that you would be able
to change a url from `openfront.io/game/RQDUy8nP?replay` to
`api.openfront.io/game/RQDUy8nP?replay` (add api. In front) and be in
the right place for the API data.

Updates URLs when joining/leaving private lobbies

Appends a random string to the end of the URL when inside a private
lobby and options change - this is to force discord to update the
embedded details.

Updates URL in different game states to ?lobby / ?live and ?replay.
These do nothing other than being used as a _cache-busting_ solution.


-----------------------------------------------
### **Lobby Info**

Discord:
<img width="556" height="487" alt="image"
src="https://github.com/user-attachments/assets/efd4a06d-506c-4036-9403-ee7c9a669e21"
/>

WhatsApp:
<img width="353" height="339" alt="image"
src="https://github.com/user-attachments/assets/3b2d0c69-988c-424f-9dee-f4e6a6868f6b"
/>


x.com:
<img width="588" height="325" alt="image"
src="https://github.com/user-attachments/assets/d9e78169-20be-4a3e-8df4-8ad41d08a750"
/>


-------------------------
### **Game Win Details**
Discord:
<img width="506" height="468" alt="image"
src="https://github.com/user-attachments/assets/69947774-c943-4a50-b470-5634ed3bf3d7"
/>

WhatsApp:
<img width="770" height="132" alt="image"
src="https://github.com/user-attachments/assets/eec28bf8-bf64-4ab8-954e-03dfdd1aae40"
/>

x.com
<img width="584" height="350" alt="image"
src="https://github.com/user-attachments/assets/168063e2-b707-422b-b7a1-0025f3ebeb92"
/>


## 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
This commit is contained in:
Ryan
2026-01-13 19:48:00 -08:00
committed by GitHub
parent c80ccaece9
commit 247c78151c
15 changed files with 832 additions and 345 deletions
+51 -3
View File
@@ -21,6 +21,7 @@ import {
GameConfig,
GameInfo,
TeamCountConfig,
isValidGameID,
} from "../core/Schemas";
import { generateID } from "../core/Util";
import "./components/baseComponents/Modal";
@@ -61,6 +62,7 @@ export class HostLobbyModal extends BaseModal {
@state() private compactMap: boolean = false;
@state() private lobbyId = "";
@state() private copySuccess = false;
@state() private lobbyUrlSuffix = "";
@state() private clients: ClientInfo[] = [];
@state() private useRandomMap: boolean = false;
@state() private disabledUnits: UnitType[] = [];
@@ -74,6 +76,8 @@ export class HostLobbyModal extends BaseModal {
private userSettings: UserSettings = new UserSettings();
private mapLoader = terrainMapFileLoader;
private leaveLobbyOnClose = true;
private renderOptionToggle(
labelKey: string,
checked: boolean,
@@ -100,6 +104,28 @@ export class HostLobbyModal extends BaseModal {
`;
}
private getRandomString(): string {
const chars = "abcdefghijklmnopqrstuvwxyz0123456789";
return Array.from(
{ length: 5 },
() => chars[Math.floor(Math.random() * chars.length)],
).join("");
}
private async buildLobbyUrl(): Promise<string> {
const config = await getServerConfigFromClient();
return `${window.location.origin}/${config.workerPath(this.lobbyId)}/game/${this.lobbyId}?lobby&s=${encodeURIComponent(this.lobbyUrlSuffix)}`;
}
private async constructUrl(): Promise<string> {
this.lobbyUrlSuffix = this.getRandomString();
return await this.buildLobbyUrl();
}
private updateHistory(url: string): void {
history.replaceState(null, "", url);
}
render() {
const content = html`
<div
@@ -109,7 +135,7 @@ export class HostLobbyModal extends BaseModal {
${modalHeader({
title: translateText("host_modal.title"),
onBack: () => {
this.leaveLobby();
this.leaveLobbyOnClose = true;
this.close();
},
ariaLabel: translateText("common.back"),
@@ -821,9 +847,14 @@ export class HostLobbyModal extends BaseModal {
);
createLobby(this.lobbyCreatorClientID)
.then((lobby) => {
.then(async (lobby) => {
this.lobbyId = lobby.gameID;
if (!isValidGameID(this.lobbyId)) {
throw new Error(`Invalid lobby ID format: ${this.lobbyId}`);
}
crazyGamesSDK.showInviteButton(this.lobbyId);
const url = await this.constructUrl();
this.updateHistory(url);
})
.then(() => {
this.dispatchEvent(
@@ -895,6 +926,10 @@ export class HostLobbyModal extends BaseModal {
protected onClose(): void {
console.log("Closing host lobby modal");
if (this.leaveLobbyOnClose) {
this.leaveLobby();
this.updateHistory("/"); // Reset URL to base
}
crazyGamesSDK.hideInviteButton();
// Clean up timers and resources
@@ -933,6 +968,8 @@ export class HostLobbyModal extends BaseModal {
this.lobbyCreatorClientID = "";
this.lobbyIdVisible = true;
this.nationCount = 0;
this.leaveLobbyOnClose = true;
}
private async handleSelectRandomMap() {
@@ -1075,6 +1112,8 @@ export class HostLobbyModal extends BaseModal {
const spawnImmunityTicks = this.spawnImmunityDurationMinutes
? this.spawnImmunityDurationMinutes * 60 * 10
: 0;
const url = await this.constructUrl();
this.updateHistory(url);
this.dispatchEvent(
new CustomEvent("update-game-config", {
detail: {
@@ -1134,6 +1173,10 @@ export class HostLobbyModal extends BaseModal {
console.log(
`Starting private game with map: ${GameMapType[this.selectedMap as keyof typeof GameMapType]} ${this.useRandomMap ? " (Randomly selected)" : ""}`,
);
// If the modal closes as part of starting the game, do not leave the lobby
this.leaveLobbyOnClose = false;
const config = await getServerConfigFromClient();
const response = await fetch(
`${window.location.origin}/${config.workerPath(this.lobbyId)}/api/start_game/${this.lobbyId}`,
@@ -1144,12 +1187,17 @@ export class HostLobbyModal extends BaseModal {
},
},
);
if (!response.ok) {
this.leaveLobbyOnClose = true;
}
return response;
}
private async copyToClipboard() {
const url = await this.buildLobbyUrl();
await copyToClipboard(
`${location.origin}/#join=${this.lobbyId}`,
url,
() => (this.copySuccess = true),
() => (this.copySuccess = false),
);