Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding back button & reload support #495

Open
wants to merge 9 commits into
base: localisation
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/loadz0r-loader.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,20 @@ if (!self.define) {
if (registry[name]) {
return;
}

const url = '/' + name;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Had to do this to prevent 404s, when it tried to request stuff like /game/index-abcdef.js

Copy link
Contributor

Choose a reason for hiding this comment

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

Do you mean 'game/index-abcdef.js'?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah yes


if ("document" in self) {
return new Promise(resolve => {
const script = document.createElement("script");
script.src = name;
script.src = url;
// Ya never know
script.defer = true;
document.head.appendChild(script);
script.onload = resolve;
})
} else {
importScripts(name);
importScripts(url);
}
}).then(() => {
if (!registry[name]) {
Expand Down
5 changes: 5 additions & 0 deletions src/_redirects.ejs
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
<%_ for (lang of langs) { _%>
/game/* /<%-lang %>/index.html 200 Language=<%-lang %>
/about/ /<%-lang %>/index.html 200 Language=<%-lang %>
/* /<%-lang %>/:splat 200 Language=<%-lang %>
<% } _%>

/game/* /index.html 200
/about/ /index.html 200
10 changes: 5 additions & 5 deletions src/index.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<meta name="theme-color" content="<%= nebulaSafeDark %>">
<link rel="shortcut icon" href="<%=favicon%>" type="image/png">
<link rel="shortcut icon" href="/<%=favicon%>" type="image/png">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta charset="utf-8">
Expand Down Expand Up @@ -41,16 +41,16 @@
<meta name="og:locale" content="<%=lang.replace(/-/g, '_')%>">
<meta name="og:type" content="website">

<link rel="apple-touch-icon" sizes="1024x1024" href="<%=icon%>">
<link rel="apple-touch-icon" sizes="1024x1024" href="/<%=icon%>">
<link rel="manifest" href="/manifest.json">
<link rel="prefetch" href="<%=workerFile%>">
<link rel="prefetch" href="/<%=workerFile%>">
<% for (const { asset, weight, inline, inlineRange } of fonts) { %>
<link rel="prefetch" as="font" href="<%= asset %>" crossorigin>
<link rel="prefetch" as="font" href="/<%= asset %>" crossorigin>
<style>
@font-face {
font-family: 'Space Mono';
font-weight: <%= weight %>;
src: url('<%= asset %>') format('woff2');
src: url('/<%= asset %>') format('woff2');
}

<% if (inline) { %>
Expand Down
15 changes: 6 additions & 9 deletions src/main/services/preact-canvas/components/game/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface Props {
useMotion: boolean;
bestTime?: number;
useVibration: boolean;
onBack: () => void;
}

interface State {
Expand Down Expand Up @@ -97,7 +98,8 @@ export default class Game extends Component<Props, State> {
gameChangeUnsubscribe,
toRevealTotal,
useMotion,
bestTime: previousBestTime
bestTime: previousBestTime,
onBack
}: Props,
{ playMode, toReveal, animator, renderer, completeTime, bestTime }: State
) {
Expand All @@ -116,7 +118,7 @@ export default class Game extends Component<Props, State> {
/>
{playMode === PlayMode.Won ? (
<Win
onMainMenu={this.onReset}
onMainMenu={onBack}
onRestart={this.onRestart}
time={completeTime}
bestTime={bestTime}
Expand Down Expand Up @@ -153,7 +155,7 @@ export default class Game extends Component<Props, State> {
)}{" "}
{strTryAgain}
</button>
<button class={mainButton} onClick={this.onReset}>
<button class={mainButton} onClick={onBack}>
{isFeaturePhone ? <span class={shortcutKey}>*</span> : ""}{" "}
{strMainMenu}
</button>
Expand Down Expand Up @@ -236,15 +238,10 @@ export default class Game extends Component<Props, State> {
this.onRestart();
}
} else if (event.key === "*") {
this.onReset();
this.props.onBack();
}
}

@bind
private onReset() {
this.props.stateService.reset();
}

@bind
private onRestart() {
this.props.stateService.restart();
Expand Down
157 changes: 117 additions & 40 deletions src/main/services/preact-canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,15 @@ const stateServicePromise: Promise<StateService> = (async () => {
return remoteServices.stateService;
})() as any;

function gameTypeToURL(
width: number,
height: number,
mines: number,
usedKeyboard: boolean
) {
return `/game/${width}:${height}:${mines}:${usedKeyboard ? "k" : "m"}`;
}

const nebulaDangerDark: Color = [40, 0, 0];
const nebulaDangerLight: Color = [131, 23, 71];
// Looking for nebulaSafeDark? It's defined in lib/nebula-safe-dark.js
Expand Down Expand Up @@ -157,16 +166,39 @@ export default class Root extends Component<Props, State> {
});
});

// Is this the reload after an update?
// Is this the reload after an old update?
// TODO: we should be able to remove this after a few months.
const instantGameDataStr = prerender
? false
: sessionStorage.getItem(immedateGameSessionKey);

if (instantGameDataStr) {
sessionStorage.removeItem(immedateGameSessionKey);
const { width, height, mines, usedKeyboard } = JSON.parse(
instantGameDataStr
) as {
width: number;
height: number;
mines: number;
usedKeyboard: boolean;
};

history.pushState(
{},
"",
gameTypeToURL(width, height, mines, usedKeyboard) + location.search
);
}
// /TODO
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We can remove this once we're confident few people are upgrading from versions that used immedateGameSessionKey.

I'll create an issue for that.


// We're going to try to jump straight into a game.
if (location.pathname.startsWith("/game/")) {
this.setState({ awaitingGame: true });
}

if (!prerender) {
this._handleCurrentURL();
}

stateServicePromise.then(async stateService => {
this._stateService = stateService;

Expand Down Expand Up @@ -206,34 +238,19 @@ export default class Root extends Component<Props, State> {
}
}
});

if (instantGameDataStr) {
await gamePerquisites;
const { width, height, mines, usedKeyboard } = JSON.parse(
instantGameDataStr
) as {
width: number;
height: number;
mines: number;
usedKeyboard: boolean;
};

if (!usedKeyboard) {
// This is a horrible hack to tell focus-visible.js not to initially show focus styles.
document.body.dispatchEvent(
new MouseEvent("mousemove", { bubbles: true })
);
}

this._stateService.initGame(width, height, mines);
}
});
}

componentDidMount() {
if (prerender) {
prerenderDone();
}

addEventListener("popstate", this._onPopState);
}

componentWillUnmount() {
removeEventListener("popstate", this._onPopState);
}

render(
Expand Down Expand Up @@ -303,6 +320,7 @@ export default class Root extends Component<Props, State> {
useMotion={motionPreference}
bestTime={bestTime}
useVibration={vibrationPreference}
onBack={this._onBackClick}
/>
)}
/>
Expand Down Expand Up @@ -404,19 +422,71 @@ export default class Root extends Component<Props, State> {

@bind
private _onSettingsCloseClick() {
this.setState({ settingsOpen: false }, () => {
this.previousFocus!.focus();
});
this._pushPath("/");
}

@bind
private _onSettingsClick() {
this.previousFocus = document.activeElement as HTMLElement;
this.setState({ settingsOpen: true, allowIntroAnim: false });
this._pushPath("/about/");
this._handleCurrentURL();
}

@bind
private async _onStartGame(width: number, height: number, mines: number) {
private _onPopState() {
this._handleCurrentURL();
}

private _pushPath(path: string) {
history.pushState({}, "", path + location.search);
this._handleCurrentURL();
}

private async _resetToStartScreen() {
history.replaceState({}, "", "/" + location.search);
this.setState(
{
awaitingGame: false,
dangerMode: false,
settingsOpen: false
},
() => {
if (this.previousFocus) {
this.previousFocus.focus();
this.previousFocus = null;
}
}
);
const stateService = await stateServicePromise;
stateService.reset();
}

private async _handleCurrentURL() {
if (location.pathname === "/") {
this._resetToStartScreen();
return;
}

if (location.pathname === "/about/") {
this.setState({ settingsOpen: true, allowIntroAnim: false });
return;
}

if (!location.pathname.startsWith("/game/")) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like the 2 if and the one inverted if are easy to trip over. Would a classic switch be more readable here?

const [,component] = location.pathname.split("/");
switch(component) {
  case "": // start screen
	// ...
    break;
  case "about":
    // ...
    break;
  case "game":
    // ...
    break;
  default:
    this._resetToStartScreen()
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah I'll find a better pattern here

this._resetToStartScreen();
return;
}

// The rest of this function handles /game/etc-etc
const gameStr = location.pathname.slice("/game/".length);
const gameStrParts = gameStr.split(":");
const [width, height, mines] = gameStrParts.slice(0, 3).map(n => Number(n));

if (!width || !height || !mines) {
this._resetToStartScreen();
return;
}

this._awaitingGameTimeout = setTimeout(() => {
this.setState({ awaitingGame: true });
}, loadingScreenTimeout);
Expand All @@ -426,28 +496,35 @@ export default class Root extends Component<Props, State> {
if (updateReady) {
// There's an update available. Let's load it as part of starting the game…
await skipWaiting();

// Did the user click the start button using keyboard?
const usedKeyboard = !!document.querySelector(".focus-visible");

sessionStorage.setItem(
immedateGameSessionKey,
JSON.stringify({ width, height, mines, usedKeyboard })
);

location.reload();
return;
}

// Wait for everything to be ready:
await gamePerquisites;

const usedKeyboard = gameStrParts[3] === "k";

if (!usedKeyboard) {
// This is a horrible hack to tell focus-visible.js not to initially show focus styles.
document.body.dispatchEvent(
new MouseEvent("mousemove", { bubbles: true })
Copy link
Contributor

Choose a reason for hiding this comment

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

lol works for me :D (But also cc @robdodson)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This code exists already, I've just moved it. WICG/focus-visible#198

Choose a reason for hiding this comment

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

just checking, is there anything I need to do on my end?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh no, sorry. I just wanted to make sure you are aware of this hack in case we were missing something. I didn’t know that Jake already filed an issue ages ago.

);
}

const stateService = await stateServicePromise;
stateService.initGame(width, height, mines);
}

@bind
private async _onStartGame(width: number, height: number, mines: number) {
// Did the user click the start button using keyboard?
// This is important if the page is reloaded (eg, if updateReady)
const usedKeyboard = !!document.querySelector(".focus-visible");
this._pushPath(gameTypeToURL(width, height, mines, usedKeyboard));
}

@bind
private _onBackClick() {
this.setState({ dangerMode: false });
this._stateService!.reset();
this._resetToStartScreen();
}
}
19 changes: 18 additions & 1 deletion src/sw/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,26 @@ self.addEventListener("fetch", event => {
}
event.respondWith(
(async function() {
const cachedResponse = await caches.match(event.request, {
let cachedResponse: Response | undefined;

// Handle the URLs that just go to the root page
if (event.request.mode === "navigate") {
const url = new URL(event.request.url);
if (
url.pathname === "/" ||
url.pathname === "/about/" ||
url.pathname.startsWith("/game/")
Copy link
Contributor

Choose a reason for hiding this comment

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

I worry about having to keeping the pages the router knows about in sync with the pages the service worker knows about. We don't need to solve it in this PR, but maybe we should just switch to always delivering index.html on 404 for requests then end in /, .html or have no file extension?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That won't work offline though (and has the lifi problem). I agree it's a problem though. I guess we should create a seperate file of rewrites that feeds into this and _redirects.

) {
cachedResponse = await caches.match("/");
}
}

if (!cachedResponse) {
cachedResponse = await caches.match(event.request, {
ignoreSearch: true
});
}

return cachedResponse || fetch(event.request);
})()
);
Expand Down