diff --git a/frontend/__tests__/controllers/url-handler.spec.ts b/frontend/__tests__/controllers/url-handler.spec.ts
index 10b5804969cf..e55ec6304172 100644
--- a/frontend/__tests__/controllers/url-handler.spec.ts
+++ b/frontend/__tests__/controllers/url-handler.spec.ts
@@ -4,7 +4,7 @@ import { compressToURI } from "lz-ts";
import * as UpdateConfig from "../../src/ts/config/setters";
import * as Notifications from "../../src/ts/states/notifications";
import * as TestLogic from "../../src/ts/test/test-logic";
-import * as TestState from "../../src/ts/test/test-state";
+import * as TestState from "../../src/ts/states/test";
import * as Misc from "../../src/ts/utils/misc";
import { FunboxName } from "@monkeytype/schemas/configs";
import { CustomTextSettings } from "@monkeytype/schemas/results";
diff --git a/frontend/__tests__/test/events/stats.spec.ts b/frontend/__tests__/test/events/stats.spec.ts
index b09bd8053068..8a089301488b 100644
--- a/frontend/__tests__/test/events/stats.spec.ts
+++ b/frontend/__tests__/test/events/stats.spec.ts
@@ -4,10 +4,6 @@ vi.mock("../../../src/ts/test/test-stats", () => ({
start: 1000,
}));
-vi.mock("../../../src/ts/test/test-state", () => ({
- koreanStatus: false,
-}));
-
const mockState = vi.hoisted(() => ({ activeWordIndex: 0 }));
vi.mock("../../../src/ts/config/store", () => ({
@@ -53,6 +49,7 @@ vi.mock("../../../src/ts/states/test", () => ({
getActiveWordIndex: () => mockState.activeWordIndex,
isResultCalculating: () => false,
getBailedOut: () => false,
+ getKoreanStatus: () => false,
}));
import {
diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html
index 69f5e412ebcc..419d735b4582 100644
--- a/frontend/src/html/pages/test.html
+++ b/frontend/src/html/pages/test.html
@@ -21,7 +21,7 @@
-
+
diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts
index 40a09f4a5db7..c2ddcb8b3e39 100644
--- a/frontend/src/ts/commandline/commandline.ts
+++ b/frontend/src/ts/commandline/commandline.ts
@@ -880,6 +880,7 @@ const modal = new AnimatedModal({
input.on(
"input",
debounce(50, async (e) => {
+ if (isAnimating) return;
inputValue = ((e as InputEvent).target as HTMLInputElement).value;
if (subgroupOverride === null) {
if (Config.singleListCommandLine === "on") {
@@ -898,6 +899,11 @@ const modal = new AnimatedModal({
);
input.on("keydown", async (e) => {
+ //the commandline is on its way out - swallow everything
+ if (isAnimating) {
+ e.preventDefault();
+ return;
+ }
mouseMode = false;
if (
e.key === "ArrowUp" ||
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index 948e9f98b01c..326f9cfdfb06 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -35,7 +35,7 @@ import {
showFpsCounter,
} from "../components/layout/overlays/FpsCounter";
import { applyConfigFromJson } from "../config/lifecycle";
-import { lastEventLog } from "../test/test-state";
+import { getLastEventLog } from "../states/test";
const adsCommands = buildCommands("ads");
@@ -277,11 +277,11 @@ export const commands: CommandsSubgroup = {
icon: "fa-cog",
visible: false,
available: (): boolean => {
- return lastEventLog !== null;
+ return getLastEventLog() !== null;
},
exec: async (): Promise
=> {
navigator.clipboard
- .writeText(JSON.stringify(lastEventLog))
+ .writeText(JSON.stringify(getLastEventLog()))
.then(() => {
showSuccessNotification("Copied to clipboard");
})
diff --git a/frontend/src/ts/commandline/lists/result-screen.ts b/frontend/src/ts/commandline/lists/result-screen.ts
index ae60d8f2bd3c..a0600c37426d 100644
--- a/frontend/src/ts/commandline/lists/result-screen.ts
+++ b/frontend/src/ts/commandline/lists/result-screen.ts
@@ -5,14 +5,13 @@ import {
showErrorNotification,
showSuccessNotification,
} from "../../states/notifications";
-import * as TestState from "../../test/test-state";
import * as TestWords from "../../test/test-words";
import { Config } from "../../config/store";
import * as PractiseWords from "../../test/practise-words";
import { Command, CommandsSubgroup } from "../types";
import * as TestScreenshot from "../../test/test-screenshot";
import { getInputHistory } from "../../test/events/stats";
-import { getResultVisible } from "../../states/test";
+import { getLastEventLog, getResultVisible } from "../../states/test";
const practiceSubgroup: CommandsSubgroup = {
title: "Practice words...",
@@ -140,12 +139,13 @@ const commands: Command[] = [
display: "Copy words to clipboard",
icon: "fa-copy",
exec: (): void => {
- if (TestState.lastEventLog === null) {
+ const eventLog = getLastEventLog();
+ if (eventLog === null) {
showErrorNotification("No event log found!");
return;
}
- const inputHistory = getInputHistory(TestState.lastEventLog);
+ const inputHistory = getInputHistory(eventLog);
const words =
Config.mode === "zen"
? inputHistory.join("")
diff --git a/frontend/src/ts/components/modals/QuoteSearchModal.tsx b/frontend/src/ts/components/modals/QuoteSearchModal.tsx
index 97812c5c5363..e2a72bacd100 100644
--- a/frontend/src/ts/components/modals/QuoteSearchModal.tsx
+++ b/frontend/src/ts/components/modals/QuoteSearchModal.tsx
@@ -28,8 +28,8 @@ import {
} from "../../states/notifications";
import { showQuoteReportModal } from "../../states/quote-report";
import { showSimpleModal } from "../../states/simple-modal";
+import { setSelectedQuoteId } from "../../states/test";
import * as TestLogic from "../../test/test-logic";
-import * as TestState from "../../test/test-state";
import { cn } from "../../utils/cn";
import { getLanguage } from "../../utils/json-data";
import * as Misc from "../../utils/misc";
@@ -398,7 +398,7 @@ export function QuoteSearchModal(): JSXElement {
showNoticeNotification("Quote ID must be at least 1");
return;
}
- TestState.setSelectedQuoteId(quoteId);
+ setSelectedQuoteId(quoteId);
setConfig("quoteLength", [-2]);
void TestLogic.restart();
hideModalAndClearChain("QuoteSearch");
diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx
index eaeedb234b8c..d09beb7eb738 100644
--- a/frontend/src/ts/components/mount.tsx
+++ b/frontend/src/ts/components/mount.tsx
@@ -32,6 +32,7 @@ import { LiveStatsTextTop } from "./pages/test/live-stats/LiveStatsTextTop";
import { TestModesNotice } from "./pages/test/modes-notice/TestModesNotice";
import { Monkey } from "./pages/test/Monkey";
import { OutOfFocusWarning } from "./pages/test/OutOfFocusWarning";
+import { Premid } from "./pages/test/Premid";
import { TestConfig } from "./pages/test/TestConfig";
import { Popups } from "./popups/Popups";
@@ -66,6 +67,7 @@ const components: Record JSXElement> = {
livestatstexttop: () => ,
livestatstextbottom: () => ,
bartimerprogress: () => ,
+ premid: () => ,
};
function mountToMountpoint(name: string, component: () => JSXElement): void {
diff --git a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx
index f610f7d50792..36cb30878f5b 100644
--- a/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx
+++ b/frontend/src/ts/components/pages/settings/custom-setting/AnimationFpsLimit.tsx
@@ -17,7 +17,7 @@ export function AnimationFpsLimit(): JSXElement {
},
onSubmit: ({ value }) => {
const val = parseFloat(String(value.fpsLimit));
- if (val === getfpsLimit()) return;
+ if (isNaN(val) || val === getfpsLimit()) return;
setfpsLimit(val);
savedIndicator.flash();
},
diff --git a/frontend/src/ts/components/pages/test/Premid.tsx b/frontend/src/ts/components/pages/test/Premid.tsx
new file mode 100644
index 000000000000..df7f2cfb5b15
--- /dev/null
+++ b/frontend/src/ts/components/pages/test/Premid.tsx
@@ -0,0 +1,32 @@
+import { getConfig } from "../../../config/store";
+import { currentLiveStats, getCurrentQuote } from "../../../states/test";
+import { getMode2 } from "../../../utils/misc";
+import { getLanguageDisplayString } from "../../../utils/strings";
+
+/**
+ * Hidden elements read by the PreMiD browser extension to show Discord rich
+ * presence. Not visible to the user.
+ */
+export function Premid() {
+ const testMode = () => {
+ const mode2 = getMode2(getConfig, getCurrentQuote());
+ const funbox =
+ getConfig.funbox.length > 0 ? ` ${getConfig.funbox.join(" ")}` : "";
+ return `${getConfig.mode} ${mode2} ${getLanguageDisplayString(
+ getConfig.language,
+ )}${funbox}`;
+ };
+
+ const secondsLeft = () => getConfig.time - (currentLiveStats.seconds ?? 0);
+
+ return (
+ <>
+
+ {testMode()}
+
+
+ {secondsLeft()}
+
+ >
+ );
+}
diff --git a/frontend/src/ts/components/pages/test/live-stats/LiveStatsMini.tsx b/frontend/src/ts/components/pages/test/live-stats/LiveStatsMini.tsx
index 8479d1d713fc..9237d540f517 100644
--- a/frontend/src/ts/components/pages/test/live-stats/LiveStatsMini.tsx
+++ b/frontend/src/ts/components/pages/test/live-stats/LiveStatsMini.tsx
@@ -25,7 +25,7 @@ export function LiveStatsMini() {
style={{
"font-size": `${getConfig.fontSize}rem`,
opacity: getConfig.timerOpacity,
- "margin-left": isTape() ? `${getConfig.tapeMargin}%` : "-0.25em",
+ "margin-left": isTape() ? `${getConfig.tapeMargin}%` : "0.25em",
}}
>
{
if (
!options.force &&
- (TestState.testRestarting || isResultCalculating() || PageTransition.get())
+ (isTestRestarting() || isResultCalculating() || PageTransition.get())
) {
console.debug(
- `navigate: ${url} ignored, page is busy (testRestarting: ${
- TestState.testRestarting
- }, resultCalculating: ${isResultCalculating()}, pageTransition: ${PageTransition.get()})`,
+ `navigate: ${url} ignored, page is busy (testRestarting: ${isTestRestarting()}, resultCalculating: ${isResultCalculating()}, pageTransition: ${PageTransition.get()})`,
);
return;
}
diff --git a/frontend/src/ts/controllers/url-handler.tsx b/frontend/src/ts/controllers/url-handler.tsx
index 247c11137030..f87365c62521 100644
--- a/frontend/src/ts/controllers/url-handler.tsx
+++ b/frontend/src/ts/controllers/url-handler.tsx
@@ -32,9 +32,9 @@ import {
showNoticeNotification,
showSuccessNotification,
} from "../states/notifications";
+import { setSelectedQuoteId } from "../states/test";
import * as CustomText from "../test/custom-text";
import { restart as restartTest } from "../test/test-logic";
-import * as TestState from "../test/test-state";
import * as Misc from "../utils/misc";
import * as ChallengeController from "./challenge-controller";
@@ -205,7 +205,7 @@ export function loadTestSettingsFromUrl(getOverride?: string): void {
});
} else if (mode === "quote") {
setConfig("quoteLength", [-2]);
- TestState.setSelectedQuoteId(parseInt(de[1], 10));
+ setSelectedQuoteId(parseInt(de[1], 10));
}
applied["mode2"] = de[1];
}
diff --git a/frontend/src/ts/elements/result-word-highlight.ts b/frontend/src/ts/elements/result-word-highlight.ts
index f3ae7aad29a3..44c831dc1b0c 100644
--- a/frontend/src/ts/elements/result-word-highlight.ts
+++ b/frontend/src/ts/elements/result-word-highlight.ts
@@ -4,7 +4,7 @@
// Constants for padding around the highlights
import * as Misc from "../utils/misc";
-import * as TestState from "../test/test-state";
+import { isLanguageRightToLeft } from "../states/test";
import { qsr } from "../utils/dom";
const PADDING_X = 16;
@@ -103,7 +103,7 @@ export async function highlightWordsInRange(
const newHighlightElementPositions = getHighlightElementPositions(
firstWordIndex,
lastWordIndex,
- TestState.isLanguageRightToLeft,
+ isLanguageRightToLeft(),
);
// For each line...
@@ -304,7 +304,7 @@ async function init(): Promise {
// For RTL languages, account for difference between highlightContainer left and RWH_el left
let RTL_offset;
- if (TestState.isLanguageRightToLeft) {
+ if (isLanguageRightToLeft()) {
RTL_offset = line.rect.left - RWH_rect.left + PADDING_X;
} else {
RTL_offset = 0;
diff --git a/frontend/src/ts/index.ts b/frontend/src/ts/index.ts
index 37b901d0db34..01bb5238867a 100644
--- a/frontend/src/ts/index.ts
+++ b/frontend/src/ts/index.ts
@@ -39,7 +39,7 @@ import { loadFromLocalStorage } from "./config/lifecycle";
import "./input/hotkeys";
import { showModal } from "./states/modals";
-import { lastEventLog } from "./test/test-state";
+import { getLastEventLog } from "./states/test";
import { buildEventLog } from "./test/events/data";
// Lock Math.random
@@ -92,7 +92,7 @@ addToGlobal({
qs: qs,
qsa: qsa,
qsr: qsr,
- lastEventLog: () => lastEventLog,
+ lastEventLog: () => getLastEventLog(),
currentEventLog: buildEventLog,
});
diff --git a/frontend/src/ts/input/handlers/before-delete.ts b/frontend/src/ts/input/handlers/before-delete.ts
index 06477cd79ddf..073cc9bc8604 100644
--- a/frontend/src/ts/input/handlers/before-delete.ts
+++ b/frontend/src/ts/input/handlers/before-delete.ts
@@ -1,11 +1,11 @@
import { Config } from "../../config/store";
-import * as TestState from "../../test/test-state";
import * as TestWords from "../../test/test-words";
import { getInputElementValue } from "../input-element";
import * as TestUI from "../../test/test-ui";
import { isAwaitingNextWord } from "../state";
import { getInputForWord } from "../../test/events/data";
import {
+ isTestRestarting,
getActiveWordIndex,
isResultCalculating,
isTestActive,
@@ -16,7 +16,7 @@ export function onBeforeDelete(event: InputEvent): void {
event.preventDefault();
return;
}
- if (TestState.testRestarting) {
+ if (isTestRestarting()) {
event.preventDefault();
return;
}
diff --git a/frontend/src/ts/input/handlers/before-insert-text.ts b/frontend/src/ts/input/handlers/before-insert-text.ts
index 400a27603cba..82c9d0823e54 100644
--- a/frontend/src/ts/input/handlers/before-insert-text.ts
+++ b/frontend/src/ts/input/handlers/before-insert-text.ts
@@ -1,5 +1,4 @@
import { Config } from "../../config/store";
-import * as TestState from "../../test/test-state";
import * as TestUI from "../../test/test-ui";
import * as TestWords from "../../test/test-words";
import { isFunboxActiveWithProperty } from "../../test/funbox/list";
@@ -7,6 +6,7 @@ import { getInputElementValue } from "../input-element";
import { isAwaitingNextWord } from "../state";
import * as SlowTimer from "../../legacy-states/slow-timer";
import {
+ isTestRestarting,
getActiveWordIndex,
isResultCalculating,
wordsHaveNewline,
@@ -22,7 +22,7 @@ import { isSpace } from "../../utils/strings";
* @returns Whether to prevent the default insertion behavior.
*/
export function onBeforeInsertText(data: string): boolean {
- if (TestState.testRestarting) {
+ if (isTestRestarting()) {
return true;
}
diff --git a/frontend/src/ts/input/listeners/composition.ts b/frontend/src/ts/input/listeners/composition.ts
index 13132c2496c4..49eb62ab3070 100644
--- a/frontend/src/ts/input/listeners/composition.ts
+++ b/frontend/src/ts/input/listeners/composition.ts
@@ -1,11 +1,11 @@
import { getInputElement } from "../input-element";
import * as CompositionState from "../../legacy-states/composition";
-import * as TestState from "../../test/test-state";
import * as TestLogic from "../../test/test-logic";
import { setLastInsertCompositionTextData } from "../state";
import { onInsertText } from "../handlers/insert-text";
import { logTestEvent } from "../../test/events/data";
import {
+ isTestRestarting,
getActiveWordIndex,
isResultCalculating,
isTestActive,
@@ -22,7 +22,7 @@ inputEl.addEventListener("compositionstart", (event) => {
const now = performance.now();
- if (TestState.testRestarting || isResultCalculating()) return;
+ if (isTestRestarting() || isResultCalculating()) return;
CompositionState.setComposing(true);
CompositionState.setData("");
setLastInsertCompositionTextData("");
@@ -42,7 +42,7 @@ inputEl.addEventListener("compositionupdate", (event) => {
data: event.data,
});
- if (TestState.testRestarting || isResultCalculating()) return;
+ if (isTestRestarting() || isResultCalculating()) return;
CompositionState.setData(event.data);
setCompositionText(event.data);
@@ -58,7 +58,7 @@ inputEl.addEventListener("compositionupdate", (event) => {
inputEl.addEventListener("compositionend", async (event) => {
console.debug("wordsInput event compositionend", { event, data: event.data });
- if (TestState.testRestarting || isResultCalculating()) return;
+ if (isTestRestarting() || isResultCalculating()) return;
CompositionState.setComposing(false);
CompositionState.setData("");
setCompositionText("");
diff --git a/frontend/src/ts/input/listeners/input.ts b/frontend/src/ts/input/listeners/input.ts
index e764708e570d..b7ebb0d38972 100644
--- a/frontend/src/ts/input/listeners/input.ts
+++ b/frontend/src/ts/input/listeners/input.ts
@@ -11,8 +11,11 @@ import { onBeforeInsertText } from "../handlers/before-insert-text";
import { onBeforeDelete } from "../handlers/before-delete";
import * as TestWords from "../../test/test-words";
import * as CompositionState from "../../legacy-states/composition";
-import * as TestState from "../../test/test-state";
-import { getActiveWordIndex, isResultCalculating } from "../../states/test";
+import {
+ isTestRestarting,
+ getActiveWordIndex,
+ isResultCalculating,
+} from "../../states/test";
import { getCurrentInput } from "../../test/events/data";
import { areAllWordsGenerated } from "../../test/words-generator";
@@ -96,7 +99,7 @@ inputEl.addEventListener("input", async (event) => {
}
// just in case before input doesn't catch this
- if (isResultCalculating() || TestState.testRestarting) return;
+ if (isResultCalculating() || isTestRestarting()) return;
const now = performance.now();
diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts
index e87d6d0afee7..6776c170feb8 100644
--- a/frontend/src/ts/states/test.ts
+++ b/frontend/src/ts/states/test.ts
@@ -1,5 +1,8 @@
import { createEffect, createMemo, createSignal } from "solid-js";
+import { z } from "zod";
import { getConfig } from "../config/store";
+import { useLocalStorage } from "../hooks/useLocalStorage";
+import { EventLog } from "../test/events/types";
import { Challenge } from "@monkeytype/challenges";
import { LayoutObject } from "@monkeytype/schemas/layouts";
@@ -228,3 +231,19 @@ export const __nonReactive = {
return result;
},
};
+
+export const [getSelectedQuoteId, setSelectedQuoteId] = useLocalStorage({
+ key: "selectedQuoteId",
+ schema: z.number().int().min(1),
+ fallback: 1,
+});
+
+export const [isLanguageRightToLeft, setIsLanguageRightToLeft] =
+ createSignal(false);
+export const [isDirectionReversed, setIsDirectionReversed] =
+ createSignal(false);
+export const [isTestRestarting, setIsTestRestarting] = createSignal(false);
+export const [getKoreanStatus, setKoreanStatus] = createSignal(false);
+export const [getLastEventLog, setLastEventLog] = createSignal(
+ null,
+);
diff --git a/frontend/src/ts/test/caret.ts b/frontend/src/ts/test/caret.ts
index 5649e5c7dbfb..14716a75c5d2 100644
--- a/frontend/src/ts/test/caret.ts
+++ b/frontend/src/ts/test/caret.ts
@@ -1,7 +1,10 @@
import { Config } from "../config/store";
import { getCurrentInput } from "./events/data";
-import * as TestState from "../test/test-state";
-import { getActiveWordIndex } from "../states/test";
+import {
+ isDirectionReversed,
+ isLanguageRightToLeft,
+ getActiveWordIndex,
+} from "../states/test";
import { configEvent } from "../events/config";
import { Caret } from "../elements/caret";
import * as CompositionState from "../legacy-states/composition";
@@ -25,8 +28,8 @@ export function resetPosition(): void {
caret.goTo({
wordIndex: 0,
letterIndex: 0,
- isLanguageRightToLeft: TestState.isLanguageRightToLeft,
- isDirectionReversed: TestState.isDirectionReversed,
+ isLanguageRightToLeft: isLanguageRightToLeft(),
+ isDirectionReversed: isDirectionReversed(),
animate: false,
});
}
@@ -35,8 +38,8 @@ export function updatePosition(noAnim = false): void {
caret.goTo({
wordIndex: getActiveWordIndex(),
letterIndex: getCurrentInput().length + CompositionState.getData().length,
- isLanguageRightToLeft: TestState.isLanguageRightToLeft,
- isDirectionReversed: TestState.isDirectionReversed,
+ isLanguageRightToLeft: isLanguageRightToLeft(),
+ isDirectionReversed: isDirectionReversed(),
animate: Config.smoothCaret !== "off" && !noAnim,
});
}
diff --git a/frontend/src/ts/test/events/data.ts b/frontend/src/ts/test/events/data.ts
index 5e393977e8ac..6c6e81b61b17 100644
--- a/frontend/src/ts/test/events/data.ts
+++ b/frontend/src/ts/test/events/data.ts
@@ -20,12 +20,12 @@ import { getEventsForWord, getInputFromDom, keysToTrack } from "./helpers";
import { recordEventForCache, resetLiveCache } from "./live-cache";
import { Keycode } from "../../constants/keys";
import { isSafeNumber, mean, roundTo2 } from "@monkeytype/util/numbers";
-import { koreanStatus } from "../test-state";
import * as TestWords from "../test-words";
import { Config } from "../../config/store";
import * as CustomText from "../../test/custom-text";
import { getMode2 } from "../../utils/misc";
import {
+ getKoreanStatus,
getActiveWordIndex,
getCurrentQuote,
getBailedOut,
@@ -38,7 +38,7 @@ export function buildEventLog(): EventLog {
targetWords: [...TestWords.words.get().map((w) => w.textWithCommit)],
mode: Config.mode,
mode2: getMode2(Config, getCurrentQuote()),
- koreanStatus: koreanStatus,
+ koreanStatus: getKoreanStatus(),
bailedOut: getBailedOut(),
...(Config.mode === "custom" && {
customTextLimitMode: CustomText.getLimit().mode,
diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts
index 4a02d10701c2..8fc2aa9dbb03 100644
--- a/frontend/src/ts/test/pace-caret.ts
+++ b/frontend/src/ts/test/pace-caret.ts
@@ -3,7 +3,6 @@ import { Config } from "../config/store";
import * as DB from "../db";
import { getActiveTagsPB } from "../collections/tags";
import * as Misc from "../utils/misc";
-import * as TestState from "./test-state";
import { configEvent } from "../events/config";
import { getActiveFunboxes } from "./funbox/list";
import { Caret } from "../elements/caret";
@@ -13,6 +12,8 @@ import {
getUserDailyBestOnce,
} from "../collections/results";
import {
+ isDirectionReversed,
+ isLanguageRightToLeft,
getActiveWordIndex,
getCurrentQuote,
getResultVisible,
@@ -57,8 +58,8 @@ export function resetCaretPosition(): void {
caret.goTo({
wordIndex: 0,
letterIndex: 0,
- isLanguageRightToLeft: TestState.isLanguageRightToLeft,
- isDirectionReversed: TestState.isDirectionReversed,
+ isLanguageRightToLeft: isLanguageRightToLeft(),
+ isDirectionReversed: isDirectionReversed(),
animate: false,
});
}
@@ -141,8 +142,8 @@ export async function update(expectedStepEnd: number): Promise {
caret.goTo({
wordIndex: currentSettings.currentWordIndex,
letterIndex: currentSettings.currentLetterIndex,
- isLanguageRightToLeft: TestState.isLanguageRightToLeft,
- isDirectionReversed: TestState.isDirectionReversed,
+ isLanguageRightToLeft: isLanguageRightToLeft(),
+ isDirectionReversed: isDirectionReversed(),
animate: true,
animationOptions: {
duration,
diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts
index ecc2a1162474..112f111cb8ff 100644
--- a/frontend/src/ts/test/practise-words.ts
+++ b/frontend/src/ts/test/practise-words.ts
@@ -13,7 +13,7 @@ import {
getWordBurstHistory,
} from "./events/stats";
import { setCustomTextIndicator } from "../states/core";
-import { lastEventLog } from "./test-state";
+import { getLastEventLog } from "../states/test";
type Before = {
mode: Mode | null;
@@ -33,7 +33,8 @@ export function init(
missed: "off" | "words" | "biwords",
slow: boolean,
): boolean {
- if (lastEventLog === null) return false;
+ const eventLog = getLastEventLog();
+ if (eventLog === null) return false;
if (Config.mode === "zen") return false;
let limit;
if ((missed === "words" && !slow) || (missed === "off" && slow)) {
@@ -43,7 +44,7 @@ export function init(
limit = 10;
}
- const missedWords = getMissedWords(lastEventLog);
+ const missedWords = getMissedWords(eventLog);
// missed word, previous word, count
let sortableMissedWords: [string, number][] = [];
@@ -95,10 +96,10 @@ export function init(
if (slow) {
const typedWords = TestWords.words
.get()
- .slice(0, getInputHistory(lastEventLog).length - 1)
+ .slice(0, getInputHistory(eventLog).length - 1)
.map((word) => word.text);
- const burstHistory = getWordBurstHistory(lastEventLog);
+ const burstHistory = getWordBurstHistory(eventLog);
sortableSlowWords = typedWords.map((e, i) => [e, burstHistory[i] ?? 0]);
sortableSlowWords.sort((a, b) => {
diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts
index 58fa45356670..d163a15cb5a8 100644
--- a/frontend/src/ts/test/result.ts
+++ b/frontend/src/ts/test/result.ts
@@ -52,12 +52,12 @@ import { Language } from "@monkeytype/schemas/languages";
import { canQuickRestart as canQuickRestartFn } from "../utils/quick-restart";
import { LocalStorageWithSchema } from "../utils/local-storage-with-schema";
import { z } from "zod";
-import * as TestState from "./test-state";
import { blurInputElement } from "../input/input-element";
import * as ConnectionState from "../legacy-states/connection";
import { qs, qsa } from "../utils/dom";
import { getTheme } from "../states/theme";
import {
+ getLastEventLog,
getCurrentQuote,
getResultVisible,
isTestInvalid,
@@ -102,7 +102,8 @@ export function toggleUserFakeChartData(): void {
let resultAnnotation: AnnotationOptions<"line">[] = [];
async function updateChartData(): Promise {
- if (result.chartData === "toolong" || TestState.lastEventLog === null) {
+ const eventLog = getLastEventLog();
+ if (result.chartData === "toolong" || eventLog === null) {
ChartController.result.getDataset("wpm").data = [];
ChartController.result.getDataset("raw").data = [];
ChartController.result.getDataset("burst").data = [];
@@ -114,7 +115,7 @@ async function updateChartData(): Promise {
ChartController.result.getScale("wpm").title.text =
typingSpeedUnit.fullUnitString;
- const labels = getTimerBoundaryLabels(TestState.lastEventLog, false);
+ const labels = getTimerBoundaryLabels(eventLog, false);
const chartData1 = [
...result.chartData.wpm.map((a) =>
@@ -122,7 +123,7 @@ async function updateChartData(): Promise {
),
];
- const chartData2 = getRawHistory(TestState.lastEventLog).map((a) =>
+ const chartData2 = getRawHistory(eventLog).map((a) =>
Numbers.roundTo2(typingSpeedUnit.fromWpm(a)),
);
@@ -344,8 +345,9 @@ function updateWpmAndAcc(): void {
result.acc === 100 ? "100%" : Format.accuracy(result.acc),
);
- if (TestState.lastEventLog !== null) {
- const acc = getAccuracy(TestState.lastEventLog);
+ const accEventLog = getLastEventLog();
+ if (accEventLog !== null) {
+ const acc = getAccuracy(accEventLog);
if (Config.alwaysShowDecimalPlaces) {
if (Config.typingSpeedUnit !== "wpm") {
qs("#result .stats .wpm .bottom")?.setAttribute(
diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts
index ce7605357647..553f403a7dd6 100644
--- a/frontend/src/ts/test/test-logic.ts
+++ b/frontend/src/ts/test/test-logic.ts
@@ -27,6 +27,12 @@ import {
isAuthenticated,
} from "../states/core";
import {
+ setIsDirectionReversed,
+ setIsLanguageRightToLeft,
+ setKoreanStatus,
+ setLastEventLog,
+ setIsTestRestarting,
+ isTestRestarting,
getCurrentQuote,
getIncompleteSeconds,
getIncompleteTests,
@@ -57,7 +63,6 @@ import {
import { restartTestEvent } from "../events/test";
import * as TestWords from "./test-words";
import * as WordsGenerator from "./words-generator";
-import * as TestState from "./test-state";
import * as PageTransition from "../legacy-states/page-transition";
import { configEvent } from "../events/config";
import { timerEvent } from "../events/timer";
@@ -193,7 +198,7 @@ export async function restart(options = {} as RestartOptions): Promise {
return;
}
- if (TestState.testRestarting || isResultCalculating()) {
+ if (isTestRestarting() || isResultCalculating()) {
options.event?.preventDefault();
return;
}
@@ -294,7 +299,7 @@ export async function restart(options = {} as RestartOptions): Promise {
Replay.pauseReplay();
setBailedOut(false);
PaceCaret.reset();
- TestState.setKoreanStatus(false);
+ setKoreanStatus(false);
clearQuoteStats();
CompositionState.setComposing(false);
CompositionState.setData("");
@@ -314,7 +319,7 @@ export async function restart(options = {} as RestartOptions): Promise {
: "testPage";
const noAnim = options.noAnim ?? false;
- TestState.setTestRestarting(true);
+ setIsTestRestarting(true);
await TestUI.fadeOutForRestart(source, noAnim);
@@ -326,7 +331,7 @@ export async function restart(options = {} as RestartOptions): Promise {
const initResult = await init();
if (!initResult) {
- TestState.setTestRestarting(false);
+ setIsTestRestarting(false);
return;
}
@@ -339,7 +344,7 @@ export async function restart(options = {} as RestartOptions): Promise {
TestUI.onTestRestart(source);
await TestUI.fadeInAfterRestart(noAnim);
- TestState.setTestRestarting(false);
+ setIsTestRestarting(false);
}
let lastInitError: Error | null = null;
@@ -357,7 +362,7 @@ async function init(): Promise {
);
}
TestInitFailed.show();
- TestState.setTestRestarting(false);
+ setIsTestRestarting(false);
return false;
}
@@ -522,7 +527,7 @@ async function init(): Promise {
/[\uac00-\ud7af]|[\u1100-\u11ff]|[\u3130-\u318f]|[\ua960-\ua97f]|[\ud7b0-\ud7ff]/g,
)
) {
- TestState.setKoreanStatus(true);
+ setKoreanStatus(true);
}
for (let i = 0; i < generatedWords.length; i++) {
@@ -551,10 +556,8 @@ async function init(): Promise {
TestUI.setJoiningClass(allJoiningScript ?? language.joiningScript ?? false);
const isLanguageRTL = allRightToLeft ?? language.rightToLeft ?? false;
- TestState.setIsLanguageRightToLeft(isLanguageRTL);
- TestState.setIsDirectionReversed(
- isFunboxActiveWithProperty("reverseDirection"),
- );
+ setIsLanguageRightToLeft(isLanguageRTL);
+ setIsDirectionReversed(isFunboxActiveWithProperty("reverseDirection"));
console.debug("Test initialized with words", TestWords.words.get());
console.debug(
@@ -872,7 +875,7 @@ export async function finish(difficultyFailed = false): Promise {
const completedEvent = structuredClone(ce) as CompletedEvent;
- TestState.setLastEventLog(eventLog);
+ setLastEventLog(eventLog);
setLastResult(structuredClone(completedEvent));
///////// completed event ready
diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts
deleted file mode 100644
index e3a77facfd07..000000000000
--- a/frontend/src/ts/test/test-state.ts
+++ /dev/null
@@ -1,48 +0,0 @@
-import { promiseWithResolvers } from "../utils/misc";
-import { EventLog } from "./events/types";
-
-export let selectedQuoteId =
- parseInt(localStorage.getItem("selectedQuoteId") ?? "1", 10) || 1;
-export let isLanguageRightToLeft = false;
-export let isDirectionReversed = false;
-export let testRestarting = false;
-export let koreanStatus = false;
-export let lastEventLog: EventLog | null = null;
-
-export function setLastEventLog(log: EventLog): void {
- lastEventLog = log;
-}
-
-export function setKoreanStatus(val: boolean): void {
- koreanStatus = val;
-}
-
-export function setSelectedQuoteId(id: number): void {
- selectedQuoteId = id;
- localStorage.setItem("selectedQuoteId", id.toString());
-}
-
-export function setIsLanguageRightToLeft(rtl: boolean): void {
- isLanguageRightToLeft = rtl;
-}
-
-export function setIsDirectionReversed(val: boolean): void {
- isDirectionReversed = val;
-}
-
-const {
- promise: testRestartingPromise,
- resolve: restartingResolve,
- reset: resetTestRestarting,
-} = promiseWithResolvers();
-
-export { testRestartingPromise };
-
-export function setTestRestarting(val: boolean): void {
- testRestarting = val;
- if (val) {
- resetTestRestarting();
- } else {
- restartingResolve();
- }
-}
diff --git a/frontend/src/ts/test/test-timer.ts b/frontend/src/ts/test/test-timer.ts
index c53ecd44b2b8..79318f0c7246 100644
--- a/frontend/src/ts/test/test-timer.ts
+++ b/frontend/src/ts/test/test-timer.ts
@@ -19,7 +19,6 @@ import { KeymapLayout, Layout } from "@monkeytype/schemas/configs";
import * as SoundController from "../controllers/sound-controller";
import { clearLowFpsMode, setLowFpsMode } from "../anim";
import { createTimer } from "animejs";
-import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame";
import { buildEventLog, getCurrentInput, logTestEvent } from "./events/data";
import { roundTo2 } from "@monkeytype/util/numbers";
import {
@@ -144,16 +143,6 @@ export function clear(logEnd = false, now = performance.now()): void {
}
}
-function premid(testTime: number): void {
- if (timerDebug) console.time("premid");
- const premidSecondsLeft = document.querySelector("#premidSecondsLeft");
-
- if (premidSecondsLeft !== null) {
- premidSecondsLeft.innerHTML = (Config.time - testTime).toString();
- }
- if (timerDebug) console.timeEnd("premid");
-}
-
function layoutfluid(time: number): void {
if (timerDebug) console.time("layoutfluid");
@@ -310,11 +299,6 @@ function timerStep(now: number, catchingUp: boolean): void {
),
};
- //ui updates
- requestDebouncedAnimationFrame("test-timer.timerStep", () => {
- premid(testTime);
- });
-
setCurrentLiveStats({
wpm: wpmAndRaw.wpm,
acc,
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 7f93e6657028..506238388f96 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -22,7 +22,6 @@ import { getActivePage } from "../states/core";
import Format from "../singletons/format";
import { convertRemToPixels } from "../utils/numbers";
import { findSingleActiveFunboxWithFunction } from "./funbox/list";
-import * as TestState from "./test-state";
import * as PaceCaret from "./pace-caret";
import {
cancelPendingAnimationFramesStartingWith,
@@ -55,8 +54,11 @@ import {
import { getTheme } from "../states/theme";
import { skipBreakdownEvent } from "../states/header";
import {
+ isDirectionReversed,
+ isLanguageRightToLeft,
+ getKoreanStatus,
+ getLastEventLog,
getActiveWordIndex,
- getCurrentQuote,
isTestActive,
setCompositionText,
setCurrentLiveStats,
@@ -244,8 +246,8 @@ async function joinOverlappingHints(
const [isWordRightToLeft] = Strings.isWordRightToLeft(
currentWord.text,
- TestState.isLanguageRightToLeft,
- TestState.isDirectionReversed,
+ isLanguageRightToLeft(),
+ isDirectionReversed(),
);
let previousBlocksAdjacent = false;
@@ -466,7 +468,7 @@ function updateWordWrapperClasses(): void {
fontSize: `${Config.fontSize}rem`,
});
- if (TestState.isLanguageRightToLeft) {
+ if (isLanguageRightToLeft()) {
wordsEl.addClass("rightToLeftTest");
qs("#resultWordsHistory .words")?.addClass("rightToLeftTest");
qs("#resultReplay .words")?.addClass("rightToLeftTest");
@@ -538,9 +540,9 @@ export function appendEmptyWordElement(index: number): void {
export function updateWordsInputPosition(): void {
if (getActivePage() !== "test") return;
- const isTestRightToLeft = TestState.isDirectionReversed
- ? !TestState.isLanguageRightToLeft
- : TestState.isLanguageRightToLeft;
+ const isTestRightToLeft = isDirectionReversed()
+ ? !isLanguageRightToLeft()
+ : isLanguageRightToLeft();
const el = getInputElement();
@@ -959,9 +961,9 @@ export async function scrollTape(noAnimation = false): Promise {
await centeringActiveLine;
- const isTestRightToLeft = TestState.isDirectionReversed
- ? !TestState.isLanguageRightToLeft
- : TestState.isLanguageRightToLeft;
+ const isTestRightToLeft = isDirectionReversed()
+ ? !isLanguageRightToLeft()
+ : isLanguageRightToLeft();
const wordsWrapperWidth = wordsWrapperEl.getOffsetWidth();
const wordsChildrenArr = wordsEl.getChildren();
@@ -1156,20 +1158,6 @@ export async function scrollTape(noAnimation = false): Promise {
}
}
-export function updatePremid(): void {
- const mode2 = Misc.getMode2(Config, getCurrentQuote());
- let fbtext = "";
- if (Config.funbox.length > 0) {
- fbtext = ` ${Config.funbox.join(" ")}`;
- }
- qs(".pageTest #premidTestMode")?.setText(
- `${Config.mode} ${mode2} ${Strings.getLanguageDisplayString(
- Config.language,
- )}${fbtext}`,
- );
- qs(".pageTest #premidSecondsLeft")?.setText(`${Config.time}`);
-}
-
function removeTestElements(lastElementIndexToRemove: number): void {
const wordsChildren = wordsEl.getChildren();
@@ -1293,7 +1281,7 @@ function buildWordLettersHTML(
let correctedChar = correctedChars[c];
let extraCorrected = "";
- const historyWord: string = !TestState.koreanStatus
+ const historyWord: string = !getKoreanStatus()
? (corrected ?? "")
: Hangul.assemble((corrected ?? "").split(""));
if (
@@ -1338,19 +1326,20 @@ async function loadWordsHistory(): Promise {
const wordsContainer = qs("#resultWordsHistory .words");
wordsContainer?.empty();
- if (TestState.lastEventLog === null) {
+ const eventLog = getLastEventLog();
+ if (eventLog === null) {
return false;
}
- const inputHistory = getInputHistory(TestState.lastEventLog);
- const burstHistory = getWordBurstHistory(TestState.lastEventLog);
+ const inputHistory = getInputHistory(eventLog);
+ const burstHistory = getWordBurstHistory(eventLog);
- const correctedHistory = getCorrectedWordsHistory(TestState.lastEventLog);
+ const correctedHistory = getCorrectedWordsHistory(eventLog);
const inputHistoryLength = inputHistory.length;
for (let i = 0; i < inputHistoryLength + 2; i++) {
const input = inputHistory[i];
const target = TestWords.words.get(i)?.textWithCommit ?? "";
- const corrected = TestState.koreanStatus
+ const corrected = getKoreanStatus()
? Hangul.assemble((correctedHistory[i] ?? "").split(""))
: correctedHistory[i];
@@ -1458,12 +1447,13 @@ export async function toggleResultWords(noAnimation = false): Promise {
}
export async function applyBurstHeatmap(): Promise {
- if (TestState.lastEventLog === null) return;
+ const eventLog = getLastEventLog();
+ if (eventLog === null) return;
if (Config.burstHeatmap) {
qsa("#resultWordsHistory .heatmapLegend")?.show();
- const burstHistory = getWordBurstHistory(TestState.lastEventLog);
+ const burstHistory = getWordBurstHistory(eventLog);
let burstlist = [...burstHistory];
burstlist = burstlist.map((x) => (x >= 1000 ? Infinity : x));
@@ -1860,7 +1850,6 @@ export function onTestRestart(source: "testPage" | "resultPage"): void {
seconds: undefined,
});
LayoutfluidFunboxTimer.instantHide();
- updatePremid();
focusWords(true);
ResultWordHighlight.destroy();
MonkeyPower.reset();
@@ -1901,14 +1890,15 @@ export function onTestFinish(): void {
}
qs(".pageTest #copyWordsListButton")?.on("click", async () => {
- if (TestState.lastEventLog === null) return;
+ const eventLog = getLastEventLog();
+ if (eventLog === null) return;
let words;
if (Config.mode === "zen") {
- words = getInputHistory(TestState.lastEventLog).join("");
+ words = getInputHistory(eventLog).join("");
} else {
words = TestWords.words
.get()
- .slice(0, getInputHistory(TestState.lastEventLog).length)
+ .slice(0, getInputHistory(eventLog).length)
.map((w) => w.textWithCommit)
.join("");
}
@@ -1916,12 +1906,13 @@ qs(".pageTest #copyWordsListButton")?.on("click", async () => {
});
qs(".pageTest #copyMissedWordsListButton")?.on("click", async () => {
- if (TestState.lastEventLog === null) return;
+ const eventLog = getLastEventLog();
+ if (eventLog === null) return;
let words;
if (Config.mode === "zen") {
- words = getInputHistory(TestState.lastEventLog).join("");
+ words = getInputHistory(eventLog).join("");
} else {
- words = Object.keys(getMissedWords(TestState.lastEventLog)).join(" ");
+ words = Object.keys(getMissedWords(eventLog)).join(" ");
}
await copyToClipboard(words);
});
diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts
index e0786537b26d..f2f0a0448729 100644
--- a/frontend/src/ts/test/words-generator.ts
+++ b/frontend/src/ts/test/words-generator.ts
@@ -13,7 +13,6 @@ import * as PractiseWords from "./practise-words";
import * as Misc from "../utils/misc";
import * as Strings from "../utils/strings";
import * as Arrays from "../utils/arrays";
-import * as TestState from "../test/test-state";
import * as GetText from "../utils/generate";
import { FunboxWordOrder } from "../utils/json-data";
import {
@@ -28,7 +27,12 @@ import { WordGenError } from "../utils/word-gen-error";
import { showLoaderBar, hideLoaderBar } from "../states/loader-bar";
import { PolyglotWordset } from "./funbox/funbox-functions";
import { LanguageObject } from "@monkeytype/schemas/languages";
-import { getCurrentQuote, isRepeated, setCurrentQuote } from "../states/test";
+import {
+ getSelectedQuoteId,
+ getCurrentQuote,
+ isRepeated,
+ setCurrentQuote,
+} from "../states/test";
import * as TestWords from "./test-words";
//pin implementation
@@ -539,14 +543,10 @@ async function getQuoteWordList(
let rq: Quote;
if (Config.quoteLength.includes(-2) && Config.quoteLength.length === 1) {
- const targetQuote = QuotesController.getQuoteById(
- TestState.selectedQuoteId,
- );
+ const targetQuote = QuotesController.getQuoteById(getSelectedQuoteId());
if (targetQuote === undefined) {
setQuoteLengthAll();
- throw new WordGenError(
- `Quote ${TestState.selectedQuoteId} does not exist`,
- );
+ throw new WordGenError(`Quote ${getSelectedQuoteId()} does not exist`);
}
rq = targetQuote;
} else if (Config.quoteLength.includes(-3)) {
diff --git a/frontend/src/ts/utils/animated-modal.ts b/frontend/src/ts/utils/animated-modal.ts
index 85b55827e438..18a8ed90989e 100644
--- a/frontend/src/ts/utils/animated-modal.ts
+++ b/frontend/src/ts/utils/animated-modal.ts
@@ -270,10 +270,33 @@ export default class AnimatedModal<
this.wrapperEl.native.showModal();
}
+ const wrapperAnimation = options?.customAnimation?.wrapper ??
+ this.customShowAnimations?.wrapper ?? {
+ opacity: [0, 1],
+ };
+
+ //the wrapper is still hidden (display:none) at this point - the show
+ //animation's onBegin is what normally reveals it, and that only runs on
+ //the first animation frame. focus() silently does nothing on a hidden
+ //element, so reveal it now and let the animation fade it in. only force
+ //it transparent if the animation actually drives opacity, otherwise it
+ //would never be brought back up
+ this.wrapperEl.show();
+ if (wrapperAnimation["opacity"] !== undefined) {
+ this.wrapperEl.setStyle({ opacity: "0" });
+ }
+
+ //focus before beforeAnimation runs - it can be slow (dynamic imports,
+ //list building) and any keypress in that gap would otherwise be lost.
+ //never selects here, the value to select is only set up in beforeAnimation
+ if (options?.focusFirstInput !== undefined) {
+ this.focusFirstInput(true);
+ }
+
await options?.beforeAnimation?.(this.modalEl, options?.modalChainData);
//wait until the next event loop to allow the dialog to start animating
- setTimeout(async () => {
+ setTimeout(() => {
this.focusFirstInput(options?.focusFirstInput);
}, 1);
@@ -286,10 +309,6 @@ export default class AnimatedModal<
opacity: [0, 1],
marginTop: ["1rem", 0],
};
- const wrapperAnimation = options?.customAnimation?.wrapper ??
- this.customShowAnimations?.wrapper ?? {
- opacity: [0, 1],
- };
const wrapperAnimationDuration = applyReducedMotion(
options?.customAnimation?.wrapper?.duration ??
this.customShowAnimations?.wrapper?.duration ??
@@ -318,7 +337,6 @@ export default class AnimatedModal<
this.wrapperEl.show();
},
onComplete: async () => {
- this.focusFirstInput(options?.focusFirstInput);
await options?.afterAnimation?.(
this.modalEl,
options?.modalChainData,
@@ -333,7 +351,6 @@ export default class AnimatedModal<
...modalAnimation,
duration: modalAnimationDuration,
onComplete: async () => {
- this.focusFirstInput(options?.focusFirstInput);
await options?.afterAnimation?.(
this.modalEl,
options?.modalChainData,