mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-07-07 09:05:59 +00:00
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:
@@ -5,6 +5,7 @@ import {
|
||||
PlayerStatsTree,
|
||||
UserMeResponse,
|
||||
} from "../core/ApiSchemas";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { fetchPlayerById, getUserMe } from "./Api";
|
||||
import { discordLogin, logOut, sendMagicLink } from "./Auth";
|
||||
import "./components/baseComponents/stats/DiscordUserHeader";
|
||||
@@ -198,7 +199,7 @@ export class AccountModal extends BaseModal {
|
||||
</h3>
|
||||
<game-list
|
||||
.games=${this.recentGames}
|
||||
.onViewGame=${(id: string) => this.viewGame(id)}
|
||||
.onViewGame=${(id: string) => void this.viewGame(id)}
|
||||
></game-list>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,15 +230,16 @@ export class AccountModal extends BaseModal {
|
||||
return html``;
|
||||
}
|
||||
|
||||
private viewGame(gameId: string): void {
|
||||
private async viewGame(gameId: string): Promise<void> {
|
||||
this.close();
|
||||
const path = location.pathname;
|
||||
const { search } = location;
|
||||
const hash = `#join=${encodeURIComponent(gameId)}`;
|
||||
const newUrl = `${path}${search}${hash}`;
|
||||
const config = await getServerConfigFromClient();
|
||||
const encodedGameId = encodeURIComponent(gameId);
|
||||
const newUrl = `/${config.workerPath(gameId)}/game/${encodedGameId}`;
|
||||
|
||||
history.pushState({ join: gameId }, "", newUrl);
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("join-changed", { detail: { gameId: encodedGameId } }),
|
||||
);
|
||||
}
|
||||
|
||||
private renderLogoutButton(): TemplateResult {
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, query, state } from "lit/decorators.js";
|
||||
import { copyToClipboard, translateText } from "../client/Utils";
|
||||
import {
|
||||
ClientInfo,
|
||||
GAME_ID_REGEX,
|
||||
GameConfig,
|
||||
GameInfo,
|
||||
GameRecordSchema,
|
||||
@@ -32,6 +33,8 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
private playersInterval: NodeJS.Timeout | null = null;
|
||||
private userSettings: UserSettings = new UserSettings();
|
||||
|
||||
private leaveLobbyOnClose = true;
|
||||
|
||||
updated(changedProperties: Map<string | number | symbol, unknown>) {
|
||||
super.updated(changedProperties);
|
||||
}
|
||||
@@ -354,21 +357,10 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
}
|
||||
}
|
||||
|
||||
protected onClose(): void {
|
||||
if (this.lobbyIdInput) this.lobbyIdInput.value = "";
|
||||
this.currentLobbyId = "";
|
||||
this.gameConfig = null;
|
||||
this.players = [];
|
||||
if (this.playersInterval) {
|
||||
clearInterval(this.playersInterval);
|
||||
this.playersInterval = null;
|
||||
private leaveLobby() {
|
||||
if (!this.currentLobbyId || !this.hasJoined) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
public closeAndLeave() {
|
||||
this.close();
|
||||
this.hasJoined = false;
|
||||
this.message = "";
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("leave-lobby", {
|
||||
detail: { lobby: this.currentLobbyId },
|
||||
@@ -378,16 +370,43 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
);
|
||||
}
|
||||
|
||||
protected onClose(): void {
|
||||
if (this.lobbyIdInput) this.lobbyIdInput.value = "";
|
||||
this.gameConfig = null;
|
||||
this.players = [];
|
||||
if (this.playersInterval) {
|
||||
clearInterval(this.playersInterval);
|
||||
this.playersInterval = null;
|
||||
}
|
||||
if (this.leaveLobbyOnClose) {
|
||||
this.leaveLobby();
|
||||
// Reset URL to base when modal closes
|
||||
history.replaceState(null, "", window.location.origin + "/");
|
||||
}
|
||||
|
||||
this.hasJoined = false;
|
||||
this.message = "";
|
||||
this.currentLobbyId = "";
|
||||
|
||||
this.leaveLobbyOnClose = true;
|
||||
}
|
||||
|
||||
public closeAndLeave() {
|
||||
this.leaveLobbyOnClose = true;
|
||||
this.close();
|
||||
}
|
||||
|
||||
private async copyToClipboard() {
|
||||
const config = await getServerConfigFromClient();
|
||||
await copyToClipboard(
|
||||
`${location.origin}/#join=${this.currentLobbyId}`,
|
||||
`${location.origin}/${config.workerPath(this.currentLobbyId)}/game/${this.currentLobbyId}`,
|
||||
() => (this.copySuccess = true),
|
||||
() => (this.copySuccess = false),
|
||||
);
|
||||
}
|
||||
|
||||
private isValidLobbyId(value: string): boolean {
|
||||
return /^[a-zA-Z0-9]{8}$/.test(value);
|
||||
return GAME_ID_REGEX.test(value);
|
||||
}
|
||||
|
||||
private normalizeLobbyId(input: string): string | null {
|
||||
@@ -403,16 +422,19 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
}
|
||||
|
||||
private extractLobbyIdFromUrl(input: string): string {
|
||||
if (input.startsWith("http")) {
|
||||
if (input.includes("#join=")) {
|
||||
const params = new URLSearchParams(input.split("#")[1]);
|
||||
return params.get("join") ?? input;
|
||||
} else if (input.includes("join/")) {
|
||||
return input.split("join/")[1];
|
||||
} else {
|
||||
return input;
|
||||
}
|
||||
} else {
|
||||
if (!input.startsWith("http")) {
|
||||
return input;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(input);
|
||||
const match = url.pathname.match(/game\/([^/]+)/);
|
||||
const candidate = match?.[1];
|
||||
if (candidate && GAME_ID_REGEX.test(candidate)) return candidate;
|
||||
|
||||
return input;
|
||||
} catch (error) {
|
||||
console.warn("Failed to parse lobby URL", error);
|
||||
return input;
|
||||
}
|
||||
}
|
||||
@@ -502,6 +524,9 @@ export class JoinPrivateLobbyModal extends BaseModal {
|
||||
this.message = "";
|
||||
this.hasJoined = true;
|
||||
|
||||
// If the modal closes as part of joining the game, do not leave the lobby
|
||||
this.leaveLobbyOnClose = false;
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("join-lobby", {
|
||||
detail: {
|
||||
|
||||
+36
-14
@@ -1,7 +1,7 @@
|
||||
import version from "resources/version.txt?raw";
|
||||
import { UserMeResponse } from "../core/ApiSchemas";
|
||||
import { EventBus } from "../core/EventBus";
|
||||
import { GameRecord, GameStartInfo, ID } from "../core/Schemas";
|
||||
import { GAME_ID_REGEX, GameRecord, GameStartInfo } from "../core/Schemas";
|
||||
import { GameEnv } from "../core/configuration/Config";
|
||||
import { getServerConfigFromClient } from "../core/configuration/ConfigLoader";
|
||||
import { GameType } from "../core/game/Game";
|
||||
@@ -193,6 +193,7 @@ declare global {
|
||||
interface DocumentEventMap {
|
||||
"join-lobby": CustomEvent<JoinLobbyEvent>;
|
||||
"kick-player": CustomEvent;
|
||||
"join-changed": CustomEvent;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -607,6 +608,7 @@ class Client {
|
||||
onHashUpdate();
|
||||
});
|
||||
window.addEventListener("hashchange", onHashUpdate);
|
||||
window.addEventListener("join-changed", onHashUpdate);
|
||||
|
||||
function updateSliderProgress(slider: HTMLInputElement) {
|
||||
const percent =
|
||||
@@ -632,7 +634,7 @@ class Client {
|
||||
// Check if CrazyGames SDK is enabled first (no hash needed in CrazyGames)
|
||||
if (crazyGamesSDK.isOnCrazyGames()) {
|
||||
const lobbyId = crazyGamesSDK.getInviteGameId();
|
||||
if (lobbyId && ID.safeParse(lobbyId).success) {
|
||||
if (lobbyId && GAME_ID_REGEX.test(lobbyId)) {
|
||||
window.showPage?.("page-join-private-lobby");
|
||||
this.joinModal?.open(lobbyId);
|
||||
console.log(`CrazyGames: joining lobby ${lobbyId} from invite param`);
|
||||
@@ -708,14 +710,16 @@ class Client {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback to hash-based join for non-CrazyGames environments
|
||||
if (decodedHash.startsWith("#join=")) {
|
||||
const lobbyId = decodedHash.substring(6); // Remove "#join="
|
||||
if (lobbyId && ID.safeParse(lobbyId).success) {
|
||||
window.showPage?.("page-join-private-lobby");
|
||||
this.joinModal?.open(lobbyId);
|
||||
console.log(`joining lobby ${lobbyId}`);
|
||||
}
|
||||
const pathMatch = window.location.pathname.match(
|
||||
/^\/(?:w\d+\/)?game\/([^/]+)/,
|
||||
);
|
||||
const lobbyId =
|
||||
pathMatch && GAME_ID_REGEX.test(pathMatch[1]) ? pathMatch[1] : null;
|
||||
if (lobbyId) {
|
||||
window.showPage?.("page-join-private-lobby");
|
||||
this.joinModal.open(lobbyId);
|
||||
console.log(`joining lobby ${lobbyId}`);
|
||||
return;
|
||||
}
|
||||
if (decodedHash.startsWith("#affiliate=")) {
|
||||
const affiliateCode = decodedHash.replace("#affiliate=", "");
|
||||
@@ -738,6 +742,7 @@ class Client {
|
||||
document.body.classList.remove("in-game");
|
||||
}
|
||||
const config = await getServerConfigFromClient();
|
||||
this.updateJoinUrlForShare(lobby.gameID, config);
|
||||
|
||||
const pattern = this.userSettings.getSelectedPatternName(
|
||||
await fetchCosmetics(),
|
||||
@@ -778,15 +783,16 @@ class Client {
|
||||
"host-lobby-modal",
|
||||
"join-private-lobby-modal",
|
||||
"game-starting-modal",
|
||||
"game-top-bar",
|
||||
"help-modal",
|
||||
"user-setting",
|
||||
|
||||
"territory-patterns-modal",
|
||||
"language-modal",
|
||||
"news-modal",
|
||||
"flag-input-modal",
|
||||
"account-button",
|
||||
"stats-button",
|
||||
"token-login",
|
||||
|
||||
"matchmaking-modal",
|
||||
"lang-selector",
|
||||
].forEach((tag) => {
|
||||
@@ -817,7 +823,7 @@ class Client {
|
||||
this.gutterAds.hide();
|
||||
},
|
||||
() => {
|
||||
this.joinModal?.close();
|
||||
this.joinModal.close();
|
||||
this.publicLobby.stop();
|
||||
incrementGamesPlayed();
|
||||
|
||||
@@ -833,11 +839,27 @@ class Client {
|
||||
if (window.location.hash === "" || window.location.hash === "#") {
|
||||
history.replaceState(null, "", window.location.origin + "#refresh");
|
||||
}
|
||||
history.pushState(null, "", `#join=${lobby.gameID}`);
|
||||
history.pushState(
|
||||
null,
|
||||
"",
|
||||
`/${config.workerPath(lobby.gameID)}/game/${lobby.gameID}?live`,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private updateJoinUrlForShare(
|
||||
lobbyId: string,
|
||||
config: Awaited<ReturnType<typeof getServerConfigFromClient>>,
|
||||
) {
|
||||
const targetUrl = `/${config.workerPath(lobbyId)}/game/${lobbyId}`;
|
||||
const currentUrl = window.location.pathname;
|
||||
|
||||
if (currentUrl !== targetUrl) {
|
||||
history.replaceState(null, "", targetUrl);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleLeaveLobby(/* event: CustomEvent */) {
|
||||
if (this.gameStop === null) {
|
||||
return;
|
||||
|
||||
@@ -73,7 +73,7 @@ export class OModal extends LitElement {
|
||||
? html`
|
||||
<aside
|
||||
class="${backdropClass}"
|
||||
@click=${this.inline ? null : this.close}
|
||||
@click=${this.inline ? null : () => this.close()}
|
||||
>
|
||||
<div
|
||||
@click=${(e: Event) => e.stopPropagation()}
|
||||
@@ -83,7 +83,7 @@ export class OModal extends LitElement {
|
||||
? html``
|
||||
: html`<div
|
||||
class="absolute top-4 right-4 z-10 text-white cursor-pointer"
|
||||
@click=${this.close}
|
||||
@click=${() => this.close()}
|
||||
>
|
||||
✕
|
||||
</div>`}
|
||||
|
||||
@@ -302,6 +302,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
});
|
||||
this.isWin = false;
|
||||
}
|
||||
history.replaceState(null, "", `${window.location.pathname}?replay`);
|
||||
this.show();
|
||||
} else if (wu.winner[0] === "nation") {
|
||||
this._title = translateText("win_modal.nation_won", {
|
||||
@@ -331,6 +332,7 @@ export class WinModal extends LitElement implements Layer {
|
||||
});
|
||||
this.isWin = false;
|
||||
}
|
||||
history.replaceState(null, "", `${window.location.pathname}?replay`);
|
||||
this.show();
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user