From b7b04bd33c6f045b136b05ca41e244811f00e43c Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 2 Jun 2026 15:05:04 +0200 Subject: [PATCH 1/9] refactor: solid modes notice (@fehmer) (#7964) fixes #7961 --------- Co-authored-by: Miodec Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- frontend/src/html/pages/test.html | 3 +- frontend/src/styles/media-queries-blue.scss | 3 - frontend/src/styles/test.scss | 26 -- frontend/src/ts/collections/results.ts | 48 ++- frontend/src/ts/collections/tags.ts | 10 + .../ts/commandline/commandline-metadata.ts | 4 - frontend/src/ts/commandline/commandline.ts | 15 +- frontend/src/ts/commandline/lists.ts | 12 +- frontend/src/ts/commandline/lists/bail-out.ts | 4 +- frontend/src/ts/commandline/lists/presets.ts | 2 - .../ts/commandline/lists/quote-favorites.ts | 10 +- frontend/src/ts/commandline/lists/tags.ts | 3 - frontend/src/ts/commandline/types.ts | 11 +- .../ts/components/modals/CustomTextModal.tsx | 18 +- .../ts/components/modals/QuoteRateModal.tsx | 14 +- .../components/modals/SaveCustomTextModal.tsx | 4 +- .../ts/components/modals/SavedTextsModal.tsx | 6 +- .../components/modals/ShareTestSettings.tsx | 8 +- frontend/src/ts/components/mount.tsx | 2 + .../test/modes-notice/AverageNotice.tsx | 44 ++ .../components/test/modes-notice/Notice.tsx | 56 +++ .../components/test/modes-notice/PbNotice.tsx | 55 +++ .../test/modes-notice/TestModesNotice.tsx | 383 ++++++++++++++++++ .../src/ts/controllers/preset-controller.ts | 2 - .../src/ts/controllers/quotes-controller.ts | 19 +- frontend/src/ts/db.ts | 4 +- frontend/src/ts/elements/last-10-average.ts | 27 -- frontend/src/ts/elements/modes-notice.ts | 319 --------------- frontend/src/ts/event-handlers/test.ts | 29 +- frontend/src/ts/input/handlers/keydown.ts | 5 +- .../src/ts/legacy-states/custom-text-name.ts | 18 - frontend/src/ts/pages/test.ts | 2 - frontend/src/ts/states/core.ts | 13 + frontend/src/ts/states/quote-rate.ts | 8 +- frontend/src/ts/states/test.ts | 16 +- frontend/src/ts/test/funbox/funbox.ts | 1 - frontend/src/ts/test/pace-caret.ts | 45 +- frontend/src/ts/test/practise-words.ts | 4 +- frontend/src/ts/test/result.ts | 16 +- frontend/src/ts/test/test-logic.ts | 70 ++-- frontend/src/ts/test/test-state.ts | 10 - frontend/src/ts/test/test-ui.ts | 14 +- frontend/src/ts/test/test-words.ts | 6 - frontend/src/ts/test/timer-progress.ts | 3 +- frontend/src/ts/test/words-generator.ts | 34 +- frontend/src/ts/types/quotes.ts | 11 + frontend/src/ts/ui.ts | 9 +- frontend/static/funbox/crt.css | 31 -- 48 files changed, 801 insertions(+), 656 deletions(-) create mode 100644 frontend/src/ts/components/test/modes-notice/AverageNotice.tsx create mode 100644 frontend/src/ts/components/test/modes-notice/Notice.tsx create mode 100644 frontend/src/ts/components/test/modes-notice/PbNotice.tsx create mode 100644 frontend/src/ts/components/test/modes-notice/TestModesNotice.tsx delete mode 100644 frontend/src/ts/elements/last-10-average.ts delete mode 100644 frontend/src/ts/elements/modes-notice.ts delete mode 100644 frontend/src/ts/legacy-states/custom-text-name.ts create mode 100644 frontend/src/ts/types/quotes.ts diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 49f01e1ba8a9..a3201d2a754f 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -21,7 +21,8 @@
Time left to memorise all words: 0s
Time left to memorise all words: 0s
-
+ +
diff --git a/frontend/src/styles/media-queries-blue.scss b/frontend/src/styles/media-queries-blue.scss index 4b6f11b520c9..5e0c7a251003 100644 --- a/frontend/src/styles/media-queries-blue.scss +++ b/frontend/src/styles/media-queries-blue.scss @@ -4,9 +4,6 @@ .content-grid { --content-max-width: 640px; } - #testModesNotice { - font-size: 0.8rem; - } .page404 { .content { grid-template-columns: 300px; diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index 571940da315c..681c398ac96a 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -1392,29 +1392,6 @@ pointer-events: none; } - #testModesNotice { - font-size: 1rem; - display: flex; - flex-wrap: wrap; - color: var(--sub-color); - text-align: center; - margin-bottom: 0.5rem; - transition: opacity 0.125s; - justify-content: center; - user-select: none; - - .textButton { - padding: 0.5em 1em; - align-items: center; - &.noInteraction { - pointer-events: none; - } - } - - .fas { - margin-right: 0.5rem; - } - } #liveStatsMini { width: 0; justify-content: start; @@ -1483,9 +1460,6 @@ } main.focus .pageTest { - #testModesNotice { - opacity: 0 !important; - } #restartTestButton { opacity: 0 !important; &:focus-visible { diff --git a/frontend/src/ts/collections/results.ts b/frontend/src/ts/collections/results.ts index beb1b7e8cf4b..b6be0a669c7d 100644 --- a/frontend/src/ts/collections/results.ts +++ b/frontend/src/ts/collections/results.ts @@ -21,7 +21,7 @@ import { useLiveQuery, } from "@tanstack/solid-db"; import { queryOptions } from "@tanstack/solid-query"; -import { Accessor } from "solid-js"; +import { Accessor, createMemo } from "solid-js"; import Ape from "../ape"; import { SnapshotResult } from "../constants/default-snapshot"; import { createEffectOn } from "../hooks/effects"; @@ -34,8 +34,12 @@ import { getTagsOnce, reconcileLocalTagPB, saveLocalTagPB, + useActiveTagsLiveQuery, } from "./tags"; import { applyIdWorkaround } from "./utils/misc"; +import { getConfig } from "../config/store"; +import { getMode2 } from "../utils/misc"; +import { getCurrentQuote } from "../states/test"; export type ResultsQueryState = { difficulty: SnapshotResult["difficulty"][]; @@ -216,7 +220,6 @@ const resultsCollection = createCollection( queryFn: async () => { if (!isAuthenticated()) return []; const tagIds = await getTagsOnce(); - const knownTagIds = new Set([...tagIds.map((it) => it._id)]); //const options = parseLoadSubsetOptions(ctx.meta?.loadSubsetOptions); @@ -587,12 +590,40 @@ export type CurrentSettingsFilter = { lazyMode: boolean; }; -export async function getUserAverage10( +// oxlint-disable-next-line typescript/explicit-function-return-type +export function useUserAverage10LiveQuery(options: { + isEnabled: Accessor; +}) { + const settingsFilter = createMemo(() => ({ + ...getConfig, + mode2: getMode2(getConfig, getCurrentQuote()), + })); + + const activeTagsQuery = useActiveTagsLiveQuery(); + + return useLiveQuery((q) => { + //disable query + if (!options.isEnabled()) return undefined; + + return q + .from({ + //we use sub-query to filter first and then aggregate + last10: buildSettingsResultsQuery(settingsFilter(), { + tagIds: activeTagsQuery().map((it) => it._id), + }) + .orderBy(({ r }) => r.timestamp, "desc") + .limit(10), + }) + .select(({ last10 }) => ({ wpm: avg(last10.wpm), acc: avg(last10.acc) })) + .findOne(); + }); +} + +export async function getUserAverage10Once( options: CurrentSettingsFilter, ): Promise<{ wpm: number; acc: number }> { //exit early if there is no user. Don't init the result collection if (!isAuthenticated()) return { wpm: 0, acc: 0 }; - const tagIds = (await getActiveTagsOnce()).map((it) => it._id); const result = await queryOnce((q) => @@ -603,15 +634,14 @@ export async function getUserAverage10( .orderBy(({ r }) => r.timestamp, "desc") .limit(10), }) - .select(({ last10 }) => ({ wpm: avg(last10.wpm), acc: avg(last10.acc) })), + .select(({ last10 }) => ({ wpm: avg(last10.wpm), acc: avg(last10.acc) })) + .findOne(), ); - return result.length === 1 && result[0] !== undefined - ? result[0] - : { wpm: 0, acc: 0 }; + return result ?? { wpm: 0, acc: 0 }; } -export async function getUserDailyBest( +export async function getUserDailyBestOnce( options: CurrentSettingsFilter, ): Promise<{ wpm: number; acc: number }> { //exit early if there is no user. Don't init the result collection diff --git a/frontend/src/ts/collections/tags.ts b/frontend/src/ts/collections/tags.ts index 5f4b788a377b..037f8895f0cb 100644 --- a/frontend/src/ts/collections/tags.ts +++ b/frontend/src/ts/collections/tags.ts @@ -81,6 +81,16 @@ export async function getTagsOnce() { }); } +// oxlint-disable-next-line typescript/explicit-function-return-type +export function useActiveTagsLiveQuery() { + return useLiveQuery((q) => { + return q + .from({ tag: tagsCollection }) + .where(({ tag }) => eq(tag.active, true)) + .orderBy(({ tag }) => tag.name, "asc"); + }); +} + type ActionType = { insertTag: { name: string; diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index f8e26655076e..46bcb4ac237c 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -2,7 +2,6 @@ import * as ConfigSchemas from "@monkeytype/schemas/configs"; import * as SoundController from "../controllers/sound-controller"; import * as TestLogic from "../test/test-logic"; import { getLanguageDisplayString } from "../utils/strings"; -import * as ModesNotice from "../elements/modes-notice"; import { areUnsortedArraysEqual } from "../utils/arrays"; import { Config } from "../config/store"; @@ -302,9 +301,6 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = { oppositeShiftMode: { subgroup: { options: "fromSchema", - afterExec: () => { - void ModesNotice.update(); - }, }, }, stopOnError: { diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts index 705ca53c309d..be31654b1bf2 100644 --- a/frontend/src/ts/commandline/commandline.ts +++ b/frontend/src/ts/commandline/commandline.ts @@ -13,7 +13,12 @@ import { setCommandlineSubgroup, } from "../states/core"; import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; -import { Command, CommandsSubgroup, CommandWithValidation } from "./types"; +import { + Command, + CommandlineSubgroupKey, + CommandsSubgroup, + CommandWithValidation, +} from "./types"; import { areSortedArraysEqual, areUnsortedArraysEqual } from "../utils/arrays"; import { parseIntOptional } from "../utils/numbers"; import { debounce } from "throttle-debounce"; @@ -21,7 +26,6 @@ import { intersect } from "@monkeytype/util/arrays"; import { createInputEventHandler } from "../elements/input-validation"; import { isInputElementFocused } from "../input/input-element"; import { qs } from "../utils/dom"; -import { ConfigKey } from "@monkeytype/schemas/configs"; import { createEffect } from "solid-js"; import { getModalVisibility, @@ -82,10 +86,7 @@ function addCommandlineBackground(): void { } type ShowSettings = { - subgroupOverride?: - | CommandsSubgroup - | CommandlineLists.ListsObjectKeys - | ConfigKey; + subgroupOverride?: CommandsSubgroup | CommandlineSubgroupKey; commandOverride?: string; singleListOverride?: boolean; }; @@ -123,7 +124,7 @@ export function show( if (exists) { showLoaderBar(); subgroupOverride = await CommandlineLists.getList( - overrideStringOrGroup as CommandlineLists.ListsObjectKeys, + overrideStringOrGroup as CommandlineSubgroupKey, ); hideLoaderBar(); } else { diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index e7e62a75f307..21345d8bd27f 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -29,7 +29,7 @@ import { } from "../states/notifications"; import * as VideoAdPopup from "../popups/video-ad-popup"; import * as TestStats from "../test/test-stats"; -import { Command, CommandsSubgroup } from "./types"; +import { Command, CommandlineListKey, CommandsSubgroup } from "./types"; import { buildCommandForConfigKey } from "./util"; import { CommandlineConfigMetadataObject } from "./commandline-metadata"; import { isAuthAvailable, signOut } from "../firebase"; @@ -376,7 +376,7 @@ export const commands: CommandsSubgroup = { ], }; -const lists = { +const lists: Record = { themes: ThemesCommands[0]?.subgroup, loadChallenge: LoadChallengeCommands[0]?.subgroup, minBurst: MinBurstCommands[0]?.subgroup, @@ -396,11 +396,11 @@ export function doesListExist(listName: string): boolean { return true; } - return lists[listName as ListsObjectKeys] !== undefined; + return lists[listName as CommandlineListKey] !== undefined; } export async function getList( - listName: ListsObjectKeys | ConfigKey, + listName: CommandlineListKey | ConfigKey, ): Promise { await Promise.allSettled([challengesPromise]); @@ -409,7 +409,7 @@ export async function getList( return subGroup; } - const list = lists[listName as ListsObjectKeys]; + const list = lists[listName as CommandlineListKey]; if (!list) { showErrorNotification(`List not found: ${listName}`); throw new Error(`List ${listName} not found`); @@ -425,8 +425,6 @@ export function getStackLength(): number { return stack.length; } -export type ListsObjectKeys = keyof typeof lists; - export function setStackToDefault(): void { setStack([commands]); } diff --git a/frontend/src/ts/commandline/lists/bail-out.ts b/frontend/src/ts/commandline/lists/bail-out.ts index 319701b3c6df..a650d5c1d0e0 100644 --- a/frontend/src/ts/commandline/lists/bail-out.ts +++ b/frontend/src/ts/commandline/lists/bail-out.ts @@ -1,13 +1,13 @@ import { Config } from "../../config/store"; +import { getCustomTextIndicator } from "../../states/core"; import * as CustomText from "../../test/custom-text"; import * as TestLogic from "../../test/test-logic"; import * as TestState from "../../test/test-state"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; import { Command, CommandsSubgroup } from "../types"; function canBailOut(): boolean { return ( - (Config.mode === "custom" && CustomTextState.isCustomTextLong() === true) || + (Config.mode === "custom" && getCustomTextIndicator()?.isLong === true) || (Config.mode === "custom" && (CustomText.getLimitMode() === "word" || CustomText.getLimitMode() === "section") && diff --git a/frontend/src/ts/commandline/lists/presets.ts b/frontend/src/ts/commandline/lists/presets.ts index c8483b4720b6..017d0000d99c 100644 --- a/frontend/src/ts/commandline/lists/presets.ts +++ b/frontend/src/ts/commandline/lists/presets.ts @@ -1,4 +1,3 @@ -import * as ModesNotice from "../../elements/modes-notice"; import * as PresetController from "../../controllers/preset-controller"; import { isAuthenticated } from "../../states/core"; import { Command, CommandsSubgroup } from "../types"; @@ -36,7 +35,6 @@ function update(): void { display: preset.name, exec: async (): Promise => { await PresetController.apply(preset._id); - void ModesNotice.update(); }, }); }); diff --git a/frontend/src/ts/commandline/lists/quote-favorites.ts b/frontend/src/ts/commandline/lists/quote-favorites.ts index 360b96e0d268..6ef9752a4506 100644 --- a/frontend/src/ts/commandline/lists/quote-favorites.ts +++ b/frontend/src/ts/commandline/lists/quote-favorites.ts @@ -6,8 +6,8 @@ import { } from "../../states/notifications"; import { isAuthenticated } from "../../states/core"; import { showLoaderBar, hideLoaderBar } from "../../states/loader-bar"; -import * as TestWords from "../../test/test-words"; import { Command } from "../types"; +import { getCurrentQuote } from "../../states/test"; const commands: Command[] = [ { @@ -15,7 +15,7 @@ const commands: Command[] = [ display: "Add current quote to favorite", icon: "fa-heart", available: (): boolean => { - const quote = TestWords.currentQuote; + const quote = getCurrentQuote(); return ( isAuthenticated() && quote !== null && @@ -27,7 +27,7 @@ const commands: Command[] = [ try { showLoaderBar(); await QuotesController.setQuoteFavorite( - TestWords.currentQuote as Quote, + getCurrentQuote() as Quote, true, ); hideLoaderBar(); @@ -43,7 +43,7 @@ const commands: Command[] = [ display: "Remove current quote from favorite", icon: "fa-heart-broken", available: (): boolean => { - const quote = TestWords.currentQuote; + const quote = getCurrentQuote(); return ( isAuthenticated() && quote !== null && @@ -55,7 +55,7 @@ const commands: Command[] = [ try { showLoaderBar(); await QuotesController.setQuoteFavorite( - TestWords.currentQuote as Quote, + getCurrentQuote() as Quote, false, ); hideLoaderBar(); diff --git a/frontend/src/ts/commandline/lists/tags.ts b/frontend/src/ts/commandline/lists/tags.ts index fc00f1d94eaa..f63ee1b1a3a1 100644 --- a/frontend/src/ts/commandline/lists/tags.ts +++ b/frontend/src/ts/commandline/lists/tags.ts @@ -1,4 +1,3 @@ -import * as ModesNotice from "../../elements/modes-notice"; import { clearActiveTags, toggleTagActive, @@ -49,7 +48,6 @@ function update(): void { ) { await PaceCaret.init(); } - void ModesNotice.update(); }, }); @@ -71,7 +69,6 @@ function update(): void { ) { await PaceCaret.init(); } - void ModesNotice.update(); }, }); } diff --git a/frontend/src/ts/commandline/types.ts b/frontend/src/ts/commandline/types.ts index 8fd01a9741a1..c1381ce3cc54 100644 --- a/frontend/src/ts/commandline/types.ts +++ b/frontend/src/ts/commandline/types.ts @@ -1,4 +1,4 @@ -import { Config } from "@monkeytype/schemas/configs"; +import { Config, ConfigKey } from "@monkeytype/schemas/configs"; import AnimatedModal from "../utils/animated-modal"; import { Validation } from "../types/validation"; @@ -57,6 +57,15 @@ export type CommandsSubgroup = { beforeList?: () => void; }; +export type CommandlineSubgroupKey = ConfigKey | CommandlineListKey; +export type CommandlineListKey = + | "themes" + | "loadChallenge" + | "minBurst" + | "funbox" + | "tags" + | "ads"; + export function withValidation(command: CommandWithValidation): Command { return command as unknown as Command; } diff --git a/frontend/src/ts/components/modals/CustomTextModal.tsx b/frontend/src/ts/components/modals/CustomTextModal.tsx index f5e9775d8350..f5b15c63c970 100644 --- a/frontend/src/ts/components/modals/CustomTextModal.tsx +++ b/frontend/src/ts/components/modals/CustomTextModal.tsx @@ -8,7 +8,10 @@ import type { FaSolidIcon } from "../../types/font-awesome"; import { setConfig } from "../../config/setters"; import { Config } from "../../config/store"; import { restartTestEvent } from "../../events/test"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { + getCustomTextIndicator, + setCustomTextIndicator, +} from "../../states/core"; import { hideModalAndClearChain, showModal } from "../../states/modals"; import { showNoticeNotification, @@ -276,7 +279,7 @@ export function CustomTextModal(): JSXElement { }); }); - setLongTextWarning(CustomTextState.isCustomTextLong() ?? false); + setLongTextWarning(getCustomTextIndicator()?.isLong ?? false); setChallengeWarning(getLoadedChallenge() !== null); }; @@ -285,8 +288,8 @@ export function CustomTextModal(): JSXElement { if (data === null) return; setIncomingChainedData(null); - if (data.long !== true && CustomTextState.isCustomTextLong()) { - CustomTextState.setCustomTextName("", undefined); + if (data.long !== true && getCustomTextIndicator()?.isLong) { + setCustomTextIndicator(undefined); showNoticeNotification("Disabled long custom text progress tracking", { durationMs: 5000, }); @@ -358,11 +361,8 @@ export function CustomTextModal(): JSXElement { if (e.code === "Enter" && e.ctrlKey) { void form.handleSubmit(); } - if ( - CustomTextState.isCustomTextLong() && - CustomTextState.getCustomTextName() !== "" - ) { - CustomTextState.setCustomTextName("", undefined); + if (getCustomTextIndicator()?.isLong) { + setCustomTextIndicator(undefined); setLongTextWarning(false); showNoticeNotification("Disabled long custom text progress tracking", { durationMs: 5000, diff --git a/frontend/src/ts/components/modals/QuoteRateModal.tsx b/frontend/src/ts/components/modals/QuoteRateModal.tsx index 2e79b636f5f0..4a04aa051cfb 100644 --- a/frontend/src/ts/components/modals/QuoteRateModal.tsx +++ b/frontend/src/ts/components/modals/QuoteRateModal.tsx @@ -11,7 +11,7 @@ import { showSuccessNotification, } from "../../states/notifications"; import { - currentQuote, + selectedQuote, quoteStats, getQuoteStats, updateQuoteStats, @@ -29,7 +29,7 @@ export function QuoteRateModal(): JSXElement { const [hoverRating, setHoverRating] = createSignal(0); const getLengthDesc = (): string => { - const quote = currentQuote(); + const quote = selectedQuote(); if (!quote) return "-"; if (quote.group === 0) return "short"; if (quote.group === 1) return "medium"; @@ -41,7 +41,7 @@ export function QuoteRateModal(): JSXElement { const displayRating = (): number => hoverRating() || rating(); const handleBeforeShow = (): void => { - const quote = currentQuote(); + const quote = selectedQuote(); if (!quote) return; setRating(0); setHoverRating(0); @@ -58,7 +58,7 @@ export function QuoteRateModal(): JSXElement { showNoticeNotification("Please select a rating"); return; } - const quote = currentQuote(); + const quote = selectedQuote(); if (!quote) return; hideModalAndClearChain("QuoteRate"); @@ -143,12 +143,12 @@ export function QuoteRateModal(): JSXElement {
- {currentQuote()?.text ?? "-"} + {selectedQuote()?.text ?? "-"}
id
- {currentQuote()?.id ?? "-"} + {selectedQuote()?.id ?? "-"}
length
@@ -156,7 +156,7 @@ export function QuoteRateModal(): JSXElement {
source
- {currentQuote()?.source ?? "-"} + {selectedQuote()?.source ?? "-"}
diff --git a/frontend/src/ts/components/modals/SaveCustomTextModal.tsx b/frontend/src/ts/components/modals/SaveCustomTextModal.tsx index 45574dd00434..bd8a549d986e 100644 --- a/frontend/src/ts/components/modals/SaveCustomTextModal.tsx +++ b/frontend/src/ts/components/modals/SaveCustomTextModal.tsx @@ -2,7 +2,7 @@ import { createForm } from "@tanstack/solid-form"; import { Accessor, JSXElement } from "solid-js"; import { z } from "zod"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { setCustomTextIndicator } from "../../states/core"; import { hideModal } from "../../states/modals"; import { showNoticeNotification, @@ -42,7 +42,7 @@ export function SaveCustomTextModal(props: { const saved = CustomText.setCustomText(value.name, text, value.isLong); if (saved) { - CustomTextState.setCustomTextName(value.name, value.isLong); + setCustomTextIndicator(value); showSuccessNotification("Custom text saved"); hideModal("SaveCustomText"); } else { diff --git a/frontend/src/ts/components/modals/SavedTextsModal.tsx b/frontend/src/ts/components/modals/SavedTextsModal.tsx index c8baed8a7142..a63829b5d139 100644 --- a/frontend/src/ts/components/modals/SavedTextsModal.tsx +++ b/frontend/src/ts/components/modals/SavedTextsModal.tsx @@ -1,6 +1,6 @@ import { createSignal, For, Index, JSXElement, Setter, Show } from "solid-js"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; +import { setCustomTextIndicator } from "../../states/core"; import { hideModal } from "../../states/modals"; import { showSimpleModal } from "../../states/simple-modal"; import * as CustomText from "../../test/custom-text"; @@ -40,7 +40,7 @@ export function SavedTextsModal(props: { }; const handleNameClick = (name: string, long: boolean) => { - CustomTextState.setCustomTextName(name, long); + setCustomTextIndicator({ name, isLong: long }); const text = getSavedText(name, long); props.setChainedData({ text, long }); hideModal("SavedTexts"); @@ -53,7 +53,7 @@ export function SavedTextsModal(props: { buttonText: "delete", execFn: async () => { CustomText.deleteCustomText(name, long); - CustomTextState.setCustomTextName("", undefined); + setCustomTextIndicator(undefined); refresh(); return { status: "success", diff --git a/frontend/src/ts/components/modals/ShareTestSettings.tsx b/frontend/src/ts/components/modals/ShareTestSettings.tsx index 7933ee389091..f2ba2d608272 100644 --- a/frontend/src/ts/components/modals/ShareTestSettings.tsx +++ b/frontend/src/ts/components/modals/ShareTestSettings.tsx @@ -8,8 +8,8 @@ import { JSXElement, Show } from "solid-js"; import { getConfig } from "../../config/store"; import { showSuccessNotification } from "../../states/notifications"; +import { getCurrentQuote } from "../../states/test"; import * as CustomText from "../../test/custom-text"; -import { currentQuote } from "../../test/test-words"; import { cn } from "../../utils/cn"; import { getMode2 } from "../../utils/misc"; import { capitalizeFirstLetter } from "../../utils/strings"; @@ -56,7 +56,7 @@ export function ShareTestSettings(): JSXElement { { enabled: values.mode, getValue: () => getConfig.mode }, { enabled: values.mode2, - getValue: () => getMode2(getConfig, currentQuote), + getValue: () => getMode2(getConfig, getCurrentQuote()), }, { enabled: values.customText, getValue: () => CustomText.getData() }, { enabled: values.punctuation, getValue: () => getConfig.punctuation }, @@ -81,7 +81,9 @@ export function ShareTestSettings(): JSXElement { if (getConfig.mode === "quote") { out += "Quote ID "; } - out += capitalizeFirstLetter(getMode2(getConfig, currentQuote) || "none"); + out += capitalizeFirstLetter( + getMode2(getConfig, getCurrentQuote()) || "none", + ); if (getConfig.mode === "time") { out += " seconds"; diff --git a/frontend/src/ts/components/mount.tsx b/frontend/src/ts/components/mount.tsx index 93c429d3a05e..be7e5ed6e58b 100644 --- a/frontend/src/ts/components/mount.tsx +++ b/frontend/src/ts/components/mount.tsx @@ -21,6 +21,7 @@ import { ProfileSearchPage } from "./pages/profile/ProfileSearchPage"; import { SettingsPage } from "./pages/settings/SettingsPage"; import { TestConfig } from "./pages/test/TestConfig"; import { Popups } from "./popups/Popups"; +import { TestModesNotice } from "./test/modes-notice/TestModesNotice"; const components: Record JSXElement> = { footer: () =>
, @@ -40,6 +41,7 @@ const components: Record JSXElement> = { devtools: () => , testconfig: () => , commandlinehotkey: () => , + testmodesnotice: () => , }; function mountToMountpoint(name: string, component: () => JSXElement): void { diff --git a/frontend/src/ts/components/test/modes-notice/AverageNotice.tsx b/frontend/src/ts/components/test/modes-notice/AverageNotice.tsx new file mode 100644 index 000000000000..749a3c1f4fd2 --- /dev/null +++ b/frontend/src/ts/components/test/modes-notice/AverageNotice.tsx @@ -0,0 +1,44 @@ +import { createMemo, JSXElement } from "solid-js"; + +import { useUserAverage10LiveQuery } from "../../../collections/results"; +import { getConfig } from "../../../config/store"; +import { isAuthenticated } from "../../../states/core"; +import { Formatting } from "../../../utils/format"; +import { Notice } from "./Notice"; + +export function AverageNotice(): JSXElement { + const last10 = useUserAverage10LiveQuery({ + isEnabled: () => isAuthenticated() && getConfig.showAverage !== "off", + }); + + const displayText = createMemo(() => { + if (last10() === undefined) return "no average"; + + const format = new Formatting(getConfig); + let speed = undefined; + let acc = undefined; + + if (getConfig.showAverage === "both" || getConfig.showAverage === "speed") { + speed = format.typingSpeed(last10()?.wpm ?? 0, { + suffix: ` ${getConfig.typingSpeedUnit}`, + }); + } + + if (getConfig.showAverage === "both" || getConfig.showAverage === "acc") { + acc = format.accuracy(last10()?.acc ?? 0, { + suffix: " acc", + }); + } + + return [speed, acc].filter((it) => it !== undefined).join(" "); + }); + + return ( + + ); +} diff --git a/frontend/src/ts/components/test/modes-notice/Notice.tsx b/frontend/src/ts/components/test/modes-notice/Notice.tsx new file mode 100644 index 000000000000..c2e5d42eb1e8 --- /dev/null +++ b/frontend/src/ts/components/test/modes-notice/Notice.tsx @@ -0,0 +1,56 @@ +import { ParentProps, Show } from "solid-js"; + +import { CommandlineSubgroupKey } from "../../../commandline/types"; +import { showCommandLineForConfig } from "../../../states/core"; +import { FaSolidIcon } from "../../../types/font-awesome"; +import { cn } from "../../../utils/cn"; +import { OneOf } from "../../../utils/types"; +import { Button } from "../../common/Button"; +import { Fa } from "../../common/Fa"; +export function Notice( + props: { + when: boolean | undefined; + icon?: FaSolidIcon; + class?: string; + } & OneOf<{ children: ParentProps["children"]; text: string | undefined }> & + Partial< + OneOf<{ onClick: () => void; openCommandline: CommandlineSubgroupKey }> + >, +) { + const isButton = () => + props.onClick !== undefined || props.openCommandline !== undefined; + + const ButtonNotice = () => ( + + ); + + const DivNotice = () => ( +
+ + + + {props.children ?? props.text} +
+ ); + + return ( + + }> + + + + ); +} diff --git a/frontend/src/ts/components/test/modes-notice/PbNotice.tsx b/frontend/src/ts/components/test/modes-notice/PbNotice.tsx new file mode 100644 index 000000000000..36c941bf592a --- /dev/null +++ b/frontend/src/ts/components/test/modes-notice/PbNotice.tsx @@ -0,0 +1,55 @@ +import { createMemo } from "solid-js"; + +import { getConfig } from "../../../config/store"; +import { getLocalPB } from "../../../db"; +import { isAuthenticated } from "../../../states/core"; +import { getSnapshot } from "../../../states/snapshot"; +import { getCurrentQuote } from "../../../states/test"; +import { getActiveFunboxes } from "../../../test/funbox/list"; +import { Formatting } from "../../../utils/format"; +import { getMode2 } from "../../../utils/misc"; +import { Notice } from "./Notice"; + +export function PbNotice() { + const displayText = createMemo(() => { + if (!isAuthenticated()) return ""; + const format = new Formatting(getConfig); + + //react on config.funbox + const _funbox = getConfig.funbox; + //react on new localPB + const _snapshot = getSnapshot(); + + const mode2 = getMode2(getConfig, getCurrentQuote()); + const pb = getLocalPB( + getConfig.mode, + mode2, + getConfig.punctuation, + getConfig.numbers, + getConfig.language, + getConfig.difficulty, + getConfig.lazyMode, + getActiveFunboxes(), + ); + + if (pb === undefined) return "no pb"; + + const speed = format.typingSpeed(pb.wpm, { + showDecimalPlaces: true, + suffix: ` ${getConfig.typingSpeedUnit}`, + }); + + const acc = format.accuracy(pb.acc, { suffix: ` acc` }); + + return `${speed} ${acc}`; + }); + + return ( + + ); +} diff --git a/frontend/src/ts/components/test/modes-notice/TestModesNotice.tsx b/frontend/src/ts/components/test/modes-notice/TestModesNotice.tsx new file mode 100644 index 000000000000..d2aa6bbc6973 --- /dev/null +++ b/frontend/src/ts/components/test/modes-notice/TestModesNotice.tsx @@ -0,0 +1,383 @@ +import { createMemo } from "solid-js"; + +import { useActiveTagsLiveQuery } from "../../../collections/tags"; +import * as Commandline from "../../../commandline/commandline"; +import { getConfig } from "../../../config/store"; +import { + getCustomTextIndicator, + showCommandLineForConfig, +} from "../../../states/core"; +import { hotkeys } from "../../../states/hotkeys"; +import { + getFocus, + getLoadedChallenge, + getPaceCaretWpm, + isPaceRepeat, + isRepeated, + wordsHaveNewline, + wordsHaveTab, +} from "../../../states/test"; +import { getActiveFunboxNames } from "../../../test/funbox/list"; +import { cn } from "../../../utils/cn"; +import { Formatting } from "../../../utils/format"; +import { + getLanguageDisplayString, + replaceUnderscoresWithSpaces, +} from "../../../utils/strings"; +import { Kbd } from "../../common/Kbd"; +import { AverageNotice } from "./AverageNotice"; +import { Notice } from "./Notice"; +import { PbNotice } from "./PbNotice"; + +export function TestModesNotice() { + return ( +
+ + + + + + + + + + + + + + + + + + + + + + +
+ ); +} + +function Repeated() { + return ( + + ); +} + +function ResultSaving() { + return ( + + ); +} + +function QuickRestart() { + return ( + <> + + to open commandline + + + to restart + + + + to restart + + + ); +} + +function LongText() { + return ( + + {getCustomTextIndicator()?.name} ( + to save progress) + + ); +} + +function LoadedChallenge() { + return ( + + ); +} + +function ZenMode() { + return ( + + to finish zen + + ); +} + +function Language() { + const isUsingPolyglot = createMemo(() => { + //react on config.funbox + const _ = getConfig.funbox; + return getActiveFunboxNames().includes("polyglot"); + }); + + return ( + <> + + + Commandline.show({ commandOverride: "setCustomPolyglotCustom" }) + } + text={getConfig.customPolyglot + .map((lang) => getLanguageDisplayString(lang, true)) + .join(", ")} + /> + + ); +} + +function Difficulty() { + return ( + showCommandLineForConfig("difficulty")} + text={getConfig.difficulty} + /> + ); +} + +function BlindMode() { + return ( + + ); +} + +function LazyMode() { + return ( + + ); +} + +function PaceCaretNotice() { + const displaySpeed = createMemo(() => { + let type: string = getConfig.paceCaret; + if (type === "off") type = "custom"; + else if (type === "tagPb") type = "tag pb"; + + const format = new Formatting(getConfig); + const speed = format.typingSpeed(getPaceCaretWpm() ?? 0, { + showDecimalPlaces: false, + suffix: ` ${getConfig.typingSpeedUnit}`, + }); + + return `${type} pace ${speed}`; + }); + + return ( + + ); +} + +function MinSpeed() { + const displaySpeed = createMemo(() => { + const format = new Formatting(getConfig); + const speed = format.typingSpeed(getConfig.minWpmCustomSpeed ?? 0, { + showDecimalPlaces: false, + suffix: ` ${getConfig.typingSpeedUnit}`, + }); + + return `min ${speed}`; + }); + + return ( + + ); +} + +function MinAcc() { + const displayAcc = createMemo(() => { + const format = new Formatting(getConfig); + const acc = format.accuracy(getConfig.minAccCustom, { + showDecimalPlaces: false, + suffix: " acc", + }); + + return `min ${acc}`; + }); + + return ( + + ); +} + +function MinBurst() { + const displaySpeed = createMemo(() => { + const format = new Formatting(getConfig); + const speed = format.typingSpeed(getConfig.minBurstCustomSpeed ?? 0, { + showDecimalPlaces: false, + suffix: ` ${getConfig.typingSpeedUnit}`, + }); + + return `min ${speed} burst ${getConfig.minBurst === "flex" ? "(flex)" : ""}`; + }); + + return ( + + ); +} + +function Funbox() { + const funboxes = createMemo(() => { + //getConfig.funbox doesn't work reactive, wrapping in a memo + if (getConfig.funbox.length === 0) return undefined; + return [...getConfig.funbox].map(replaceUnderscoresWithSpaces).join(", "); + }); + + return ( + + ); +} + +function ConfidenceMode() { + return ( + + ); +} + +function StopOnError() { + return ( + + ); +} + +function Layout() { + return ( + + ); +} + +function OppositeShift() { + return ( + + ); +} + +function Tags() { + const tags = useActiveTagsLiveQuery(); + + return ( + 0} + icon={tags().length === 1 ? "fa-tag" : "fa-tags"} + openCommandline="tags" + text={tags() + .map((tag) => tag.name) + .join(", ")} + /> + ); +} diff --git a/frontend/src/ts/controllers/preset-controller.ts b/frontend/src/ts/controllers/preset-controller.ts index 5ee0da7d534f..4c6ec1481a09 100644 --- a/frontend/src/ts/controllers/preset-controller.ts +++ b/frontend/src/ts/controllers/preset-controller.ts @@ -9,7 +9,6 @@ import { saveActiveToLocalStorage, } from "../collections/tags"; import { saveFullConfigToLocalStorage } from "../config/persistence"; -import * as ModesNotice from "../elements/modes-notice"; import { __nonReactive, type PresetItem } from "../collections/presets"; export async function apply(_id: string): Promise { @@ -42,7 +41,6 @@ export async function apply(_id: string): Promise { saveActiveToLocalStorage(); } } - void ModesNotice.update(); TestLogic.restart(); showSuccessNotification("Preset applied", { durationMs: 2000 }); saveFullConfigToLocalStorage(); diff --git a/frontend/src/ts/controllers/quotes-controller.ts b/frontend/src/ts/controllers/quotes-controller.ts index 53eac99b1095..4a443a425ed6 100644 --- a/frontend/src/ts/controllers/quotes-controller.ts +++ b/frontend/src/ts/controllers/quotes-controller.ts @@ -6,17 +6,14 @@ import * as DB from "../db"; import Ape from "../ape"; import { tryCatch } from "@monkeytype/util/trycatch"; import { Language } from "@monkeytype/schemas/languages"; -import { QuoteData, QuoteDataQuote } from "@monkeytype/schemas/quotes"; -import { RequiredProperties } from "../utils/misc"; - -export type Quote = QuoteDataQuote & { - group: number; - language: Language; - textSplit?: string[]; -}; - -export type QuoteWithTextSplit = RequiredProperties; - +import { QuoteData } from "@monkeytype/schemas/quotes"; +import { + Quote as QuoteType, + QuoteWithTextSplit as QuoteWithTextSplitType, +} from "../types/quotes"; + +export type Quote = QuoteType; +export type QuoteWithTextSplit = QuoteWithTextSplitType; type QuoteCollection = { quotes: Quote[]; length: number; diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 59110acbbb68..7ccc52509428 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -181,7 +181,7 @@ export async function initSnapshot(): Promise { setSolidSnapshot(dbSnapshot); } } -export async function getLocalPB( +export function getLocalPB( mode: M, mode2: Mode2, punctuation: boolean, @@ -190,7 +190,7 @@ export async function getLocalPB( difficulty: Difficulty, lazyMode: boolean, funboxes: FunboxMetadata[], -): Promise { +): PersonalBest | undefined { if (!funboxes.every((f) => f.canGetPb)) { return undefined; } diff --git a/frontend/src/ts/elements/last-10-average.ts b/frontend/src/ts/elements/last-10-average.ts deleted file mode 100644 index a40c92ce8919..000000000000 --- a/frontend/src/ts/elements/last-10-average.ts +++ /dev/null @@ -1,27 +0,0 @@ -import * as Misc from "../utils/misc"; -import * as Numbers from "@monkeytype/util/numbers"; -import { Config } from "../config/store"; -import * as TestWords from "../test/test-words"; -import { getUserAverage10 } from "../collections/results"; - -let averageWPM = 0; -let averageAcc = 0; - -export async function update(): Promise { - const mode2 = Misc.getMode2(Config, TestWords.currentQuote); - - const average = await getUserAverage10({ ...Config, mode2 }); - const wpm = Numbers.roundTo2(average.wpm); - const acc = Numbers.roundTo2(average.acc); - - averageWPM = Config.alwaysShowDecimalPlaces ? wpm : Math.round(wpm); - averageAcc = Config.alwaysShowDecimalPlaces ? acc : Math.floor(acc); -} - -export function getWPM(): number { - return averageWPM; -} - -export function getAcc(): number { - return averageAcc; -} diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts deleted file mode 100644 index 1315ef9a678c..000000000000 --- a/frontend/src/ts/elements/modes-notice.ts +++ /dev/null @@ -1,319 +0,0 @@ -import * as PaceCaret from "../test/pace-caret"; -import * as TestState from "../test/test-state"; -import * as DB from "../db"; -import * as Last10Average from "../elements/last-10-average"; -import { __nonReactive } from "../collections/tags"; -import { Config } from "../config/store"; -import * as TestWords from "../test/test-words"; -import { configEvent, type ConfigEventKey } from "../events/config"; -import { isAuthenticated } from "../states/core"; -import * as CustomTextState from "../legacy-states/custom-text-name"; -import { getLanguageDisplayString } from "../utils/strings"; -import Format from "../singletons/format"; -import { getActiveFunboxes, getActiveFunboxNames } from "../test/funbox/list"; -import { escapeHTML, getMode2 } from "../utils/misc"; -import { qsr } from "../utils/dom"; -import { - wordsHaveNewline, - wordsHaveTab, - getLoadedChallenge, -} from "../states/test"; - -configEvent.subscribe(({ key }) => { - const configKeys: ConfigEventKey[] = [ - "difficulty", - "blindMode", - "stopOnError", - "paceCaret", - "minWpm", - "minWpmCustomSpeed", - "minAcc", - "minAccCustom", - "minBurst", - "confidenceMode", - "layout", - "showAverage", - "showPb", - "typingSpeedUnit", - "quickRestart", - "customPolyglot", - "alwaysShowDecimalPlaces", - "resultSaving", - ]; - if (configKeys.includes(key)) { - void update(); - } -}); - -const testModesNotice = qsr(".pageTest #testModesNotice"); - -export async function update(): Promise { - testModesNotice.empty(); - - if (TestState.isRepeated && Config.mode !== "quote") { - testModesNotice.appendHtml( - `
repeated
`, - ); - } - - if (!Config.resultSaving) { - testModesNotice.appendHtml( - `
saving disabled
`, - ); - } - - if (wordsHaveTab()) { - if (Config.quickRestart === "esc") { - testModesNotice.appendHtml( - `
shift + tab to open commandline
`, - ); - testModesNotice.appendHtml( - `
esc to restart
`, - ); - } - if (Config.quickRestart === "tab") { - testModesNotice.appendHtml( - `
shift + tab to restart
`, - ); - } - } - - if ( - (wordsHaveNewline() || Config.funbox.includes("58008")) && - Config.quickRestart === "enter" - ) { - testModesNotice.appendHtml( - `
shift + enter to restart
`, - ); - } - - const customTextName = CustomTextState.getCustomTextName(); - const isLong = CustomTextState.isCustomTextLong(); - if (Config.mode === "custom" && customTextName !== "" && isLong) { - testModesNotice.appendHtml( - `
${escapeHTML( - customTextName, - )} (shift + enter to save progress)
`, - ); - } - - const loadedChallenge = getLoadedChallenge(); - if (loadedChallenge !== null) { - testModesNotice.appendHtml( - `
${loadedChallenge.display}
`, - ); - } - - if (Config.mode === "zen") { - testModesNotice.appendHtml( - `
shift + enter to finish zen
`, - ); - } - - const usingPolyglot = getActiveFunboxNames().includes("polyglot"); - - if (Config.mode !== "zen" && !usingPolyglot) { - testModesNotice.appendHtml( - ``, - ); - } - - if (usingPolyglot) { - const languages = Config.customPolyglot - .map((lang) => { - const langDisplay = getLanguageDisplayString(lang, true); - return langDisplay; - }) - .join(", "); - - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.difficulty === "expert") { - testModesNotice.appendHtml( - ``, - ); - } else if (Config.difficulty === "master") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.blindMode) { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.lazyMode) { - testModesNotice.appendHtml( - ``, - ); - } - - if ( - Config.paceCaret !== "off" || - (Config.repeatedPace && TestState.isPaceRepeat) - ) { - const speed = Format.typingSpeed(PaceCaret.settings?.wpm ?? 0, { - showDecimalPlaces: false, - suffix: ` ${Config.typingSpeedUnit}`, - }); - - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.showAverage !== "off") { - const avgWPM = Last10Average.getWPM(); - const avgAcc = Last10Average.getAcc(); - - if (isAuthenticated() && avgWPM > 0) { - const avgWPMText = ["speed", "both"].includes(Config.showAverage) - ? Format.typingSpeed(avgWPM, { suffix: ` ${Config.typingSpeedUnit}` }) - : ""; - - const avgAccText = ["acc", "both"].includes(Config.showAverage) - ? Format.accuracy(avgAcc, { suffix: " acc" }) - : ""; - - const text = `${avgWPMText} ${avgAccText}`.trim(); - - testModesNotice.appendHtml( - ``, - ); - } - } - - if (Config.showPb) { - if (!isAuthenticated()) { - return; - } - const mode2 = getMode2(Config, TestWords.currentQuote); - const pb = await DB.getLocalPB( - Config.mode, - mode2, - Config.punctuation, - Config.numbers, - Config.language, - Config.difficulty, - Config.lazyMode, - getActiveFunboxes(), - ); - - let str = "no pb"; - - if (pb !== undefined) { - str = `${Format.typingSpeed(pb.wpm, { - showDecimalPlaces: true, - suffix: ` ${Config.typingSpeedUnit}`, - })} ${pb?.acc}% acc`; - } - - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.minWpm !== "off") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.minAcc !== "off") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.minBurst !== "off") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.funbox.length > 0) { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.confidenceMode === "on") { - testModesNotice.appendHtml( - ``, - ); - } - if (Config.confidenceMode === "max") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.stopOnError !== "off") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.layout !== "default") { - testModesNotice.appendHtml( - ``, - ); - } - - if (Config.oppositeShiftMode !== "off") { - testModesNotice.appendHtml( - ``, - ); - } - - let tagsString = ""; - try { - __nonReactive.getActiveTags().forEach((tag) => { - tagsString += `${tag.name}, `; - }); - - if (tagsString !== "") { - testModesNotice.appendHtml( - ``, - ); - } - } catch {} -} diff --git a/frontend/src/ts/event-handlers/test.ts b/frontend/src/ts/event-handlers/test.ts index 78746fc0a4bc..d85fc367b256 100644 --- a/frontend/src/ts/event-handlers/test.ts +++ b/frontend/src/ts/event-handlers/test.ts @@ -1,8 +1,6 @@ -import * as Commandline from "../commandline/commandline"; import { Config } from "../config/store"; import * as EditResultTagsModal from "../modals/edit-result-tags"; import { __nonReactive } from "../collections/tags"; -import * as TestWords from "../test/test-words"; import { showNoticeNotification, showErrorNotification, @@ -12,26 +10,11 @@ import { showQuoteReportModal } from "../states/quote-report"; import * as PractiseWordsModal from "../modals/practise-words"; import { navigate } from "../controllers/route-controller"; import { getMode2 } from "../utils/misc"; -import { ConfigKey } from "@monkeytype/schemas/configs"; -import { ListsObjectKeys } from "../commandline/lists"; import { qs } from "../utils/dom"; +import { getCurrentQuote } from "../states/test"; const testPage = qs(".pageTest"); -testPage?.onChild("click", "#testModesNotice .textButton", async (event) => { - const target = event.childTarget as HTMLElement; - const attr = target?.getAttribute("commands"); - if (attr === null) return; - Commandline.show({ subgroupOverride: attr as ConfigKey | ListsObjectKeys }); -}); - -testPage?.onChild("click", "#testModesNotice .textButton", async (event) => { - const target = event.childTarget as HTMLElement; - const attr = target?.getAttribute("commandId"); - if (attr === null) return; - Commandline.show({ commandOverride: attr }); -}); - testPage?.onChild("click", ".tags .editTagsButton", () => { if (__nonReactive.getTags().length > 0) { const resultid = @@ -47,19 +30,21 @@ testPage?.onChild("click", ".tags .editTagsButton", () => { }); qs(".pageTest #rateQuoteButton")?.on("click", async () => { - if (TestWords.currentQuote === null) { + const currentQuote = getCurrentQuote(); + if (currentQuote === null) { showErrorNotification("Failed to show quote rating popup: no quote"); return; } - showQuoteRateModal(TestWords.currentQuote); + showQuoteRateModal(currentQuote); }); qs(".pageTest #reportQuoteButton")?.on("click", async () => { - if (TestWords.currentQuote === null) { + const currentQuote = getCurrentQuote(); + if (currentQuote === null) { showErrorNotification("Failed to show quote report popup: no quote"); return; } - showQuoteReportModal(TestWords.currentQuote?.id); + showQuoteReportModal(currentQuote?.id); }); testPage?.onChild("click", "#practiseWordsButton", () => { diff --git a/frontend/src/ts/input/handlers/keydown.ts b/frontend/src/ts/input/handlers/keydown.ts index a2beca801445..9e4a319b5a26 100644 --- a/frontend/src/ts/input/handlers/keydown.ts +++ b/frontend/src/ts/input/handlers/keydown.ts @@ -14,7 +14,6 @@ import * as KeyConverter from "../../utils/key-converter"; import * as ShiftTracker from "../../test/shift-tracker"; import { canQuickRestart } from "../../utils/quick-restart"; import * as CustomText from "../../test/custom-text"; -import * as CustomTextState from "../../legacy-states/custom-text-name"; import { getLastBailoutAttempt, setCorrectShiftUsed, @@ -26,6 +25,8 @@ import { } from "../../test/funbox/list"; import { Keycode } from "../../constants/keys"; import { wordsHaveTab } from "../../states/test"; + +import { getCustomTextIndicator } from "../../states/core"; import { logTestEvent } from "../../test/events/data"; import { getTestEventCode } from "../../test/events/helpers"; @@ -51,7 +52,7 @@ export async function handleEnter( Config.words, Config.time, CustomText.getData(), - CustomTextState.isCustomTextLong() ?? false, + getCustomTextIndicator()?.isLong ?? false, ) ) { const delay = Date.now() - getLastBailoutAttempt(); diff --git a/frontend/src/ts/legacy-states/custom-text-name.ts b/frontend/src/ts/legacy-states/custom-text-name.ts deleted file mode 100644 index dd648a093a1e..000000000000 --- a/frontend/src/ts/legacy-states/custom-text-name.ts +++ /dev/null @@ -1,18 +0,0 @@ -let customTestName = ""; // It should be empty when the text is not saved or a saved text has been modified -let isLong: boolean | undefined = false; - -export function getCustomTextName(): string { - return customTestName; -} - -export function isCustomTextLong(): boolean | undefined { - return isLong; -} - -export function setCustomTextName( - newName: string, - long: boolean | undefined, -): void { - customTestName = newName; - isLong = long; -} diff --git a/frontend/src/ts/pages/test.ts b/frontend/src/ts/pages/test.ts index 4e048adbe249..8482c7511528 100644 --- a/frontend/src/ts/pages/test.ts +++ b/frontend/src/ts/pages/test.ts @@ -2,7 +2,6 @@ import * as TestLogic from "../test/test-logic"; import * as Funbox from "../test/funbox/funbox"; import Page from "./page"; import { updateFooterAndVerticalAds } from "../controllers/ad-controller"; -import * as ModesNotice from "../elements/modes-notice"; import * as Keymap from "../elements/keymap"; import { blurInputElement } from "../input/input-element"; import { qsr } from "../utils/dom"; @@ -20,7 +19,6 @@ export const page = new Page({ noAnim: true, }); void Funbox.clear(); - void ModesNotice.update(); updateFooterAndVerticalAds(true); }, beforeShow: async (): Promise => { diff --git a/frontend/src/ts/states/core.ts b/frontend/src/ts/states/core.ts index c50a6dfdf6ee..6e500553b8ca 100644 --- a/frontend/src/ts/states/core.ts +++ b/frontend/src/ts/states/core.ts @@ -1,5 +1,7 @@ import { createSignal } from "solid-js"; +import { CommandlineSubgroupKey } from "../commandline/types"; import { PageName } from "../pages/page"; +import { showModal } from "./modals"; export const [getActivePage, setActivePage] = createSignal("loading"); export const [getVersion, setVersion] = createSignal<{ @@ -36,3 +38,14 @@ export const [isUserVerified, setUserVerified] = createSignal(false); export const [getSelectedProfileName, setSelectedProfileName] = createSignal< string | undefined >(undefined); + +export function showCommandLineForConfig( + selector: CommandlineSubgroupKey, +): void { + setCommandlineSubgroup(selector); + showModal("Commandline"); +} + +export const [getCustomTextIndicator, setCustomTextIndicator] = createSignal< + { name: string; isLong: boolean } | undefined +>(undefined); diff --git a/frontend/src/ts/states/quote-rate.ts b/frontend/src/ts/states/quote-rate.ts index 7e3275cbad94..ce030c74dbeb 100644 --- a/frontend/src/ts/states/quote-rate.ts +++ b/frontend/src/ts/states/quote-rate.ts @@ -14,12 +14,12 @@ type QuoteStats = { language?: Language; }; -const [currentQuote, setCurrentQuote] = createSignal(null); +const [selectedQuote, setSelectedQuote] = createSignal(null); const [quoteStats, setQuoteStats] = createSignal< QuoteStats | null | Record >(null); -export { currentQuote, quoteStats }; +export { selectedQuote, quoteStats }; export function clearQuoteStats(): void { setQuoteStats(null); @@ -42,7 +42,7 @@ export async function getQuoteStats( ): Promise { if (!quote) return; - setCurrentQuote(quote); + setSelectedQuote(quote); const response = await Ape.quotes.getRating({ query: { quoteId: quote.id, language: quote.language }, }); @@ -71,6 +71,6 @@ export function updateQuoteStats(stats: QuoteStats): void { } export function showQuoteRateModal(quote: Quote): void { - setCurrentQuote(quote); + setSelectedQuote(quote); showModal("QuoteRate"); } diff --git a/frontend/src/ts/states/test.ts b/frontend/src/ts/states/test.ts index 0d3347cf37c7..9654775e29f0 100644 --- a/frontend/src/ts/states/test.ts +++ b/frontend/src/ts/states/test.ts @@ -1,10 +1,11 @@ import { createSignal, createEffect, createMemo } from "solid-js"; import { Challenge } from "@monkeytype/schemas/challenges"; import { getConfig } from "../config/store"; -import { getActivePage } from "./core"; + import { canQuickRestart } from "../utils/quick-restart"; import { getData as getCustomTextData } from "../test/custom-text"; -import { isCustomTextLong } from "../legacy-states/custom-text-name"; +import { getActivePage, getCustomTextIndicator } from "./core"; +import { QuoteWithTextSplit } from "../types/quotes"; import { CompletedEvent, IncompleteTest } from "@monkeytype/schemas/results"; import { createSignalWithSetters } from "../hooks/createSignalWithSetters"; @@ -28,12 +29,19 @@ export const [ push: (set, val: IncompleteTest) => set((arr) => [...arr, val]), reset: (set) => set([]), }); - export const getRestartCount = createMemo(() => getIncompleteTests().length); export const getIncompleteSeconds = createMemo(() => getIncompleteTests().reduce((sum, test) => sum + test.seconds, 0), ); +export const [isRepeated, setIsRepeated] = createSignal(false); +export const [isPaceRepeat, setIsPaceRepeat] = createSignal(false); +export const [getPaceCaretWpm, setPaceCaretWpm] = createSignal< + number | undefined +>(undefined); +export const [getCurrentQuote, setCurrentQuote] = + createSignal(null); + createEffect(() => { getActivePage(); // depend on active page setIsLongTest( @@ -42,7 +50,7 @@ createEffect(() => { getConfig.words, getConfig.time, getCustomTextData(), - isCustomTextLong() ?? false, + getCustomTextIndicator()?.isLong ?? false, ), ); }); diff --git a/frontend/src/ts/test/funbox/funbox.ts b/frontend/src/ts/test/funbox/funbox.ts index f44b35223526..3a963e1c016a 100644 --- a/frontend/src/ts/test/funbox/funbox.ts +++ b/frontend/src/ts/test/funbox/funbox.ts @@ -184,7 +184,6 @@ export async function activate( for (const fb of getActiveFunboxesWithFunction("applyConfig")) { fb.functions.applyConfig(); } - // ModesNotice.update(); return true; } diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 80de9f36c749..27244a9b6452 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -8,7 +8,11 @@ import { configEvent } from "../events/config"; import { getActiveFunboxes } from "./funbox/list"; import { Caret } from "../elements/caret"; import { qsr } from "../utils/dom"; -import { getUserAverage10, getUserDailyBest } from "../collections/results"; +import { + getUserAverage10Once, + getUserDailyBestOnce, +} from "../collections/results"; +import { getCurrentQuote, isPaceRepeat, setPaceCaretWpm } from "../states/test"; type Settings = { wpm: number; @@ -23,23 +27,20 @@ type Settings = { let startTimestamp = 0; -export let settings: Settings | null = null; +let settings: Settings | null = null; export const caret = new Caret(qsr("#paceCaret"), Config.paceCaretStyle); let lastTestWpm = 0; export function setLastTestWpm(wpm: number): void { - if ( - !TestState.isPaceRepeat || - (TestState.isPaceRepeat && wpm > lastTestWpm) - ) { + if (!isPaceRepeat() || (isPaceRepeat() && wpm > lastTestWpm)) { lastTestWpm = wpm; } } export function resetCaretPosition(): void { - if (Config.paceCaret === "off" && !TestState.isPaceRepeat) return; + if (Config.paceCaret === "off" && !isPaceRepeat()) return; if (Config.mode === "zen") return; caret.hide(); @@ -57,21 +58,19 @@ export function resetCaretPosition(): void { export async function init(): Promise { caret.hide(); - const mode2 = Misc.getMode2(Config, TestWords.currentQuote); + const mode2 = Misc.getMode2(Config, getCurrentQuote()); let wpm = 0; if (Config.paceCaret === "pb") { wpm = - ( - await DB.getLocalPB( - Config.mode, - mode2, - Config.punctuation, - Config.numbers, - Config.language, - Config.difficulty, - Config.lazyMode, - getActiveFunboxes(), - ) + DB.getLocalPB( + Config.mode, + mode2, + Config.punctuation, + Config.numbers, + Config.language, + Config.difficulty, + Config.lazyMode, + getActiveFunboxes(), )?.wpm ?? 0; } else if (Config.paceCaret === "tagPb") { wpm = getActiveTagsPB( @@ -84,16 +83,17 @@ export async function init(): Promise { Config.lazyMode, ); } else if (Config.paceCaret === "average") { - wpm = Math.round((await getUserAverage10({ ...Config, mode2 })).wpm); + wpm = Math.round((await getUserAverage10Once({ ...Config, mode2 })).wpm); } else if (Config.paceCaret === "daily") { - wpm = Math.round((await getUserDailyBest({ ...Config, mode2 })).wpm); + wpm = Math.round((await getUserDailyBestOnce({ ...Config, mode2 })).wpm); } else if (Config.paceCaret === "custom") { wpm = Config.paceCaretCustomSpeed; - } else if (Config.paceCaret === "last" || TestState.isPaceRepeat) { + } else if (Config.paceCaret === "last" || isPaceRepeat()) { wpm = lastTestWpm; } if (wpm === undefined || wpm < 1 || Number.isNaN(wpm)) { settings = null; + setPaceCaretWpm(undefined); return; } @@ -111,6 +111,7 @@ export async function init(): Promise { wordsStatus: {}, timeout: null, }; + setPaceCaretWpm(wpm); } export async function update(expectedStepEnd: number): Promise { diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index b1ee407c3dfd..1099d4afd7cf 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -6,9 +6,9 @@ import { setConfig } from "../config/setters"; import * as CustomText from "./custom-text"; import * as TestInput from "./test-input"; import { configEvent } from "../events/config"; -import { setCustomTextName } from "../legacy-states/custom-text-name"; import { Mode } from "@monkeytype/schemas/shared"; import { CustomTextSettings } from "@monkeytype/schemas/results"; +import { setCustomTextIndicator } from "../states/core"; type Before = { mode: Mode | null; @@ -165,7 +165,7 @@ export function init( 5, ); - setCustomTextName("practise", undefined); + setCustomTextIndicator({ name: "practice", isLong: false }); before.mode = mode; before.punctuation = punctuation; diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 65e7dda1e126..8eee8ed25745 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -15,7 +15,7 @@ import { showSuccessNotification, addNotificationWithLevel, } from "../states/notifications"; -import { isAuthenticated } from "../states/core"; +import { getCustomTextIndicator, isAuthenticated } from "../states/core"; import { getQuoteStats } from "../states/quote-rate"; import * as GlarsesMode from "../legacy-states/glarses-mode"; import * as SlowTimer from "../legacy-states/slow-timer"; @@ -33,7 +33,6 @@ import * as TodayTracker from "./today-tracker"; import { configEvent } from "../events/config"; import * as Focus from "./focus"; import * as CustomText from "./custom-text"; -import * as CustomTextState from "./../legacy-states/custom-text-name"; import * as Funbox from "./funbox/funbox"; import Format from "../singletons/format"; import confetti from "canvas-confetti"; @@ -58,10 +57,9 @@ import { z } from "zod"; import * as TestState from "./test-state"; import { blurInputElement } from "../input/input-element"; import * as ConnectionState from "../legacy-states/connection"; -import { currentQuote } from "./test-words"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; -import { isTestInvalid } from "../states/test"; +import { getCurrentQuote, isTestInvalid } from "../states/test"; let result: CompletedEvent; let minChartVal: number; @@ -296,7 +294,7 @@ function applyFakeChartData(): void { export async function updateChartPBLine(): Promise { const themecolors = getTheme(); - const localPb = await DB.getLocalPB( + const localPb = DB.getLocalPB( result.mode, result.mode2, result.punctuation ?? false, @@ -511,7 +509,7 @@ export async function updateCrown(dontSave: boolean): Promise { console.debug("Result can get PB:", canGetPb.value, canGetPb.reason ?? ""); if (canGetPb.value) { - const localPb = await DB.getLocalPB( + const localPb = DB.getLocalPB( Config.mode, result.mode2, Config.punctuation, @@ -536,7 +534,7 @@ export async function updateCrown(dontSave: boolean): Promise { ); } } else { - const localPb = await DB.getLocalPB( + const localPb = DB.getLocalPB( Config.mode, result.mode2, Config.punctuation, @@ -1051,7 +1049,7 @@ export async function update( qs("main #result #rateQuoteButton")?.hide(); qs("main #result #reportQuoteButton")?.hide(); } else { - updateRateQuote(currentQuote); + updateRateQuote(getCurrentQuote()); qs("main #result #reportQuoteButton")?.show(); } qs("main #result .stats .dailyLeaderboard")?.hide(); @@ -1091,7 +1089,7 @@ export async function update( Config.words, Config.time, CustomText.getData(), - CustomTextState.isCustomTextLong() ?? false, + getCustomTextIndicator()?.isLong ?? false, ); if (Config.alwaysShowWordsHistory && canQuickRestart && !GlarsesMode.get()) { diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index c900b69756df..aded14af05bc 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -11,7 +11,6 @@ import { showSuccessNotification, } from "../states/notifications"; import * as CustomText from "./custom-text"; -import * as CustomTextState from "../legacy-states/custom-text-name"; import * as TestStats from "./test-stats"; import * as PractiseWords from "./practise-words"; import * as ShiftTracker from "./shift-tracker"; @@ -27,13 +26,22 @@ import * as TodayTracker from "./today-tracker"; import * as ChallengeContoller from "../controllers/challenge-controller"; import { clearQuoteStats } from "../states/quote-rate"; import * as Result from "./result"; -import { getActivePage, isAuthenticated } from "../states/core"; import { + getActivePage, + getCustomTextIndicator, + isAuthenticated, +} from "../states/core"; +import { + getCurrentQuote, getIncompleteSeconds, getIncompleteTests, getRestartCount, + isPaceRepeat, + isRepeated, pushIncompleteTest, resetIncompleteTests, + setIsPaceRepeat, + setIsRepeated, setIsTestInvalid, setLastResult, setResultVisible, @@ -186,10 +194,7 @@ export function startTest(now: number): boolean { } try { - if ( - Config.paceCaret !== "off" || - (Config.repeatedPace && TestState.isPaceRepeat) - ) { + if (Config.paceCaret !== "off" || (Config.repeatedPace && isPaceRepeat())) { PaceCaret.start(); } } catch (e) {} @@ -248,7 +253,7 @@ export function restart(options = {} as RestartOptions): void { Config.words, Config.time, CustomText.getData(), - CustomTextState.isCustomTextLong() ?? false, + getCustomTextIndicator()?.isLong ?? false, ) ) { let message = "Use your mouse to confirm."; @@ -270,7 +275,7 @@ export function restart(options = {} as RestartOptions): void { } } - if (TestState.isRepeated) { + if (isRepeated()) { options.withSameWordset = true; } @@ -287,10 +292,11 @@ export function restart(options = {} as RestartOptions): void { } } + const currentQuote = getCurrentQuote(); if ( Config.mode === "quote" && - TestWords.currentQuote !== null && - Config.language.startsWith(TestWords.currentQuote.language) && + currentQuote !== null && + Config.language.startsWith(currentQuote.language) && Config.repeatQuotes === "typing" && (TestState.isActive || failReason !== "") ) { @@ -381,8 +387,8 @@ export function restart(options = {} as RestartOptions): void { repeatWithPace = true; } - TestState.setRepeated(options.withSameWordset ?? false); - TestState.setPaceRepeat(repeatWithPace); + setIsRepeated(options.withSameWordset ?? false); + setIsPaceRepeat(repeatWithPace); TestInitFailed.hide(); TestState.setTestInitSuccess(true); const initResult = await init(); @@ -541,7 +547,7 @@ async function init(): Promise { mode: Config.mode, mode2: Misc.getMode2(Config, null), funbox: Config.funbox, - currentQuote: TestWords.currentQuote, + currentQuote: getCurrentQuote(), }); let wordsHaveTab = false; @@ -647,8 +653,7 @@ export function areAllTestWordsGenerated(): boolean { TestWords.words.length >= CustomText.getLimitValue() && CustomText.getLimitValue() !== 0) || (Config.mode === "quote" && - TestWords.words.length >= - (TestWords.currentQuote?.textSplit?.length ?? 0)) || + TestWords.words.length >= (getCurrentQuote()?.textSplit?.length ?? 0)) || (Config.mode === "custom" && CustomText.getLimitMode() === "section" && WordsGenerator.sectionIndex >= CustomText.getLimitValue() && @@ -854,7 +859,7 @@ function buildCompletedEvent( language = Strings.removeLanguageSize(Config.language); } - const quoteLength = TestWords.currentQuote?.group ?? -1; + const quoteLength = getCurrentQuote()?.group ?? -1; const completedEvent: Omit = { wpm: stats.wpm, @@ -868,7 +873,7 @@ function buildCompletedEvent( charTotal: stats.allChars, acc: stats.acc, mode: Config.mode, - mode2: Misc.getMode2(Config, TestWords.currentQuote), + mode2: Misc.getMode2(Config, getCurrentQuote()), quoteLength: quoteLength, punctuation: Config.punctuation, numbers: Config.numbers, @@ -1239,6 +1244,7 @@ function buildCompletedEvent2(): Omit { err: getErrorCountHistory(), }; + const currentQuote = getCurrentQuote(); const completedEvent: Omit = { wpm: Numbers.roundTo2(calculateWpm(chars.correctWord, duration)), rawWpm: Numbers.roundTo2( @@ -1252,7 +1258,7 @@ function buildCompletedEvent2(): Omit { lastKeyToEnd: getLastKeypressToEndMs(), startToFirstKey: getStartToFirstKeypressMs(), afkDuration: afkDuration, - quoteLength: TestWords.currentQuote?.group ?? -1, + quoteLength: currentQuote?.group ?? -1, customText: customText, tags: activeTagsIds, punctuation: Config.punctuation, @@ -1260,7 +1266,7 @@ function buildCompletedEvent2(): Omit { lazyMode: Config.lazyMode, timestamp: Date.now(), mode: Config.mode, - mode2: Misc.getMode2(Config, TestWords.currentQuote), + mode2: Misc.getMode2(Config, currentQuote), bailedOut: TestState.bailedOut, funbox: Config.funbox, difficulty: Config.difficulty, @@ -1307,8 +1313,8 @@ export async function finish(difficultyFailed = false): Promise { TestUI.onTestFinish(); - if (TestState.isRepeated && Config.mode === "quote") { - TestState.setRepeated(false); + if (isRepeated() && Config.mode === "quote") { + setIsRepeated(false); } // in case the tests ends with a keypress (not a word submission) @@ -1488,7 +1494,7 @@ export async function finish(difficultyFailed = false): Promise { showNoticeNotification("Test invalid - AFK detected"); setIsTestInvalid(true); dontSave = true; - } else if (TestState.isRepeated) { + } else if (isRepeated()) { showNoticeNotification("Test invalid - repeated"); setIsTestInvalid(true); dontSave = true; @@ -1543,7 +1549,7 @@ export async function finish(difficultyFailed = false): Promise { compareCompletedEvents(ce); } - if (TestState.isRepeated || difficultyFailed) { + if (isRepeated() || difficultyFailed) { if (Config.resultSaving) { const testSeconds = completedEvent.testDuration; const afkseconds = completedEvent.afkDuration; @@ -1554,8 +1560,8 @@ export async function finish(difficultyFailed = false): Promise { } } - const customTextName = CustomTextState.getCustomTextName(); - const isLong = CustomTextState.isCustomTextLong(); + const customTextName = getCustomTextIndicator()?.name ?? ""; + const isLong = getCustomTextIndicator()?.isLong === true; if (Config.mode === "custom" && customTextName !== "" && isLong) { // Let's update the custom text progress if ( @@ -1641,9 +1647,9 @@ export async function finish(difficultyFailed = false): Promise { difficultyFailed, failReason, afkDetected, - TestState.isRepeated, + isRepeated(), tooShort, - TestWords.currentQuote, + getCurrentQuote(), dontSave, ); @@ -1742,7 +1748,7 @@ async function saveResult( if (data.isPb !== undefined && data.isPb) { //new pb - const localPb = await DB.getLocalPB( + const localPb = DB.getLocalPB( result.mode, result.mode2, result.punctuation, @@ -1825,14 +1831,6 @@ const debouncedZipfCheck = debounce(250, async () => { } }); -qs(".pageTest")?.onChild( - "click", - "#testModesNotice .textButton.restart", - () => { - restart(); - }, -); - qs(".pageTest")?.onChild("click", "#testInitFailed button.restart", () => { restart(); }); diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts index 831c101d08ec..416d995b4d94 100644 --- a/frontend/src/ts/test/test-state.ts +++ b/frontend/src/ts/test/test-state.ts @@ -1,7 +1,5 @@ import { promiseWithResolvers } from "../utils/misc"; -export let isRepeated = false; -export let isPaceRepeat = false; export let isActive = false; export let bailedOut = false; export let selectedQuoteId = @@ -19,14 +17,6 @@ export function setKoreanStatus(val: boolean): void { koreanStatus = val; } -export function setRepeated(tf: boolean): void { - isRepeated = tf; -} - -export function setPaceRepeat(tf: boolean): void { - isPaceRepeat = tf; -} - export function setActive(tf: boolean): void { isActive = tf; } diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index cd1dec9b948d..e83cb0f51344 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -56,8 +56,6 @@ import * as Joining from "./break-joining"; import * as LayoutfluidFunboxTimer from "../test/funbox/layoutfluid-funbox-timer"; import * as Keymap from "../elements/keymap"; import * as ThemeController from "../controllers/theme-controller"; -import * as ModesNotice from "../elements/modes-notice"; -import * as Last10Average from "../elements/last-10-average"; import * as MemoryFunboxTimer from "./funbox/memory-funbox-timer"; import { ElementsWithUtils, @@ -68,7 +66,7 @@ import { } from "../utils/dom"; import { getTheme } from "../states/theme"; import { skipBreakdownEvent } from "../states/header"; -import { wordsHaveNewline } from "../states/test"; +import { getCurrentQuote, wordsHaveNewline } from "../states/test"; export const updateHintsPositionDebounced = Misc.debounceUntilResolved( updateHintsPosition, @@ -1134,7 +1132,7 @@ export async function scrollTape(noAnimation = false): Promise { } export function updatePremid(): void { - const mode2 = Misc.getMode2(Config, TestWords.currentQuote); + const mode2 = Misc.getMode2(Config, getCurrentQuote()); let fbtext = ""; if (Config.funbox.length > 0) { fbtext = ` ${Config.funbox.join(" ")}`; @@ -1895,14 +1893,6 @@ export function onTestRestart(source: "testPage" | "resultPage"): void { MonkeyPower.reset(); MemoryFunboxTimer.reset(); - if (Config.showAverage !== "off") { - void Last10Average.update().then(() => { - void ModesNotice.update(); - }); - } else { - void ModesNotice.update(); - } - if (source === "resultPage") { if (Config.randomTheme !== "off") { void ThemeController.randomizeTheme(); diff --git a/frontend/src/ts/test/test-words.ts b/frontend/src/ts/test/test-words.ts index 5ab6bf4337a1..b8ddae67d6ff 100644 --- a/frontend/src/ts/test/test-words.ts +++ b/frontend/src/ts/test/test-words.ts @@ -1,4 +1,3 @@ -import { QuoteWithTextSplit } from "../controllers/quotes-controller"; import * as TestState from "./test-state"; class Words { @@ -58,11 +57,6 @@ class Words { export const words = new Words(); export let hasNumbers = false; -export let currentQuote = null as QuoteWithTextSplit | null; - -export function setCurrentQuote(rq: QuoteWithTextSplit | null): void { - currentQuote = rq; -} export function setHasNumbers(tf: boolean): void { hasNumbers = tf; diff --git a/frontend/src/ts/test/timer-progress.ts b/frontend/src/ts/test/timer-progress.ts index 717748e9bbbc..ac19c3b6a198 100644 --- a/frontend/src/ts/test/timer-progress.ts +++ b/frontend/src/ts/test/timer-progress.ts @@ -8,6 +8,7 @@ import { configEvent } from "../events/config"; import { applyReducedMotion } from "../utils/misc"; import { requestDebouncedAnimationFrame } from "../utils/debounced-animation-frame"; import { animate } from "animejs"; +import { getCurrentQuote } from "../states/test"; const barEl = document.querySelector("#barTimerProgress .bar") as HTMLElement; const barOpacityEl = document.querySelector( @@ -205,7 +206,7 @@ export function update(): void { outof = CustomText.getLimitValue(); } if (Config.mode === "quote") { - outof = TestWords.currentQuote?.textSplit.length ?? 1; + outof = getCurrentQuote()?.textSplit.length ?? 1; } if (Config.timerStyle === "bar") { const percent = Math.floor( diff --git a/frontend/src/ts/test/words-generator.ts b/frontend/src/ts/test/words-generator.ts index d9c08d0744e7..95895c4ddbfd 100644 --- a/frontend/src/ts/test/words-generator.ts +++ b/frontend/src/ts/test/words-generator.ts @@ -6,7 +6,6 @@ import QuotesController, { Quote, QuoteWithTextSplit, } from "../controllers/quotes-controller"; -import * as TestWords from "./test-words"; import * as BritishEnglish from "./british-english"; import * as LazyMode from "./lazy-mode"; import * as EnglishPunctuation from "./english-punctuation"; @@ -28,6 +27,7 @@ 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"; //pin implementation const random = Math.random; @@ -375,10 +375,11 @@ async function applyBritishEnglishToWord( ): Promise { if (!Config.britishEnglish) return word; if (!Config.language.includes("english")) return word; + const currentQuote = getCurrentQuote(); if ( Config.mode === "quote" && - TestWords.currentQuote?.britishText !== undefined && - TestWords.currentQuote?.britishText !== "" + currentQuote?.britishText !== undefined && + currentQuote?.britishText !== "" ) { return word; } @@ -424,7 +425,7 @@ export function getLimit(): number { let limit = 100; - const currentQuote = TestWords.currentQuote; + const currentQuote = getCurrentQuote(); if (Config.mode === "quote" && currentQuote === null) { throw new WordGenError("Random quote is null"); @@ -498,12 +499,12 @@ async function getQuoteWordList( language: LanguageObject, wordOrder?: FunboxWordOrder, ): Promise { - if (TestState.isRepeated) { + if (isRepeated()) { if (currentWordset === null) { throw new WordGenError("Current wordset is null"); } - TestWords.setCurrentQuote(previousRandomQuote); + setCurrentQuote(previousRandomQuote); // need to re-reverse the words if the test is repeated // because it will be reversed again in the generateWords function @@ -579,17 +580,18 @@ async function getQuoteWordList( rq.textSplit = rq.text.split(" "); } - TestWords.setCurrentQuote(rq as QuoteWithTextSplit); + setCurrentQuote(rq as QuoteWithTextSplit); - if (TestWords.currentQuote === null) { + const currentQuote = getCurrentQuote(); + if (currentQuote === null) { throw new WordGenError("Random quote is null"); } - if (TestWords.currentQuote.textSplit === undefined) { + if (currentQuote.textSplit === undefined) { throw new WordGenError("Random quote textSplit is undefined"); } - return TestWords.currentQuote.textSplit; + return currentQuote.textSplit; } let currentWordset: Wordset | null = null; @@ -610,11 +612,11 @@ let previousRandomQuote: QuoteWithTextSplit | null = null; export async function generateWords( language: LanguageObject, ): Promise { - if (!TestState.isRepeated) { + if (!isRepeated()) { previousGetNextWordReturns = []; } - previousRandomQuote = TestWords.currentQuote; - TestWords.setCurrentQuote(null); + previousRandomQuote = getCurrentQuote(); + setCurrentQuote(null); currentSection = []; sectionIndex = 0; sectionHistory = []; @@ -705,7 +707,7 @@ export async function generateWords( i++; } - const quote = TestWords.currentQuote; + const quote = getCurrentQuote(); if (Config.mode === "quote" && quote === null) { throw new WordGenError("Random quote is null"); @@ -745,7 +747,7 @@ export async function getNextWord( previousWord2: string | undefined, ): Promise { console.debug("Getting next word", { - isRepeated: TestState.isRepeated, + isRepeated: isRepeated(), currentWordset, wordIndex, language: currentLanguage, @@ -765,7 +767,7 @@ export async function getNextWord( //because quote test can be repeated in the middle of a test //we cant rely on data inside previousGetNextWordReturns //because it might not include the full quote - if (TestState.isRepeated && Config.mode !== "quote") { + if (isRepeated() && Config.mode !== "quote") { const repeated = previousGetNextWordReturns[wordIndex]; if (repeated === undefined) { diff --git a/frontend/src/ts/types/quotes.ts b/frontend/src/ts/types/quotes.ts new file mode 100644 index 000000000000..a7d95b6a0b2e --- /dev/null +++ b/frontend/src/ts/types/quotes.ts @@ -0,0 +1,11 @@ +import { Language } from "@monkeytype/schemas/languages"; +import { QuoteDataQuote } from "@monkeytype/schemas/quotes"; +import { RequiredProperties } from "../utils/misc"; + +export type Quote = QuoteDataQuote & { + group: number; + language: Language; + textSplit?: string[]; +}; + +export type QuoteWithTextSplit = RequiredProperties; diff --git a/frontend/src/ts/ui.ts b/frontend/src/ts/ui.ts index 3ed96ce48f05..2283c1870186 100644 --- a/frontend/src/ts/ui.ts +++ b/frontend/src/ts/ui.ts @@ -5,9 +5,12 @@ import * as TestState from "./test/test-state"; import { configEvent } from "./events/config"; import { debounce, throttle } from "throttle-debounce"; import * as TestUI from "./test/test-ui"; -import { getActivePage, getGlobalOffsetTop } from "./states/core"; +import { + getActivePage, + getCustomTextIndicator, + getGlobalOffsetTop, +} from "./states/core"; import { isDevEnvironment } from "./utils/env"; -import { isCustomTextLong } from "./legacy-states/custom-text-name"; import { canQuickRestart } from "./utils/quick-restart"; import { FontName } from "@monkeytype/schemas/fonts"; import { qs, qsr } from "./utils/dom"; @@ -86,7 +89,7 @@ window.addEventListener("beforeunload", (event) => { Config.words, Config.time, CustomText.getData(), - isCustomTextLong() ?? false, + getCustomTextIndicator()?.isLong ?? false, ) ) { //ignore diff --git a/frontend/static/funbox/crt.css b/frontend/static/funbox/crt.css index 0ca8f62f387b..855f21cd6654 100644 --- a/frontend/static/funbox/crt.css +++ b/frontend/static/funbox/crt.css @@ -63,37 +63,6 @@ body.crtmode #bannerCenter .banner { 0 0 3px; } -body.crtmode #testModesNotice { - text-shadow: none; -} - -body.crtmode #testModesNotice .textButton { - text-shadow: - 3px 0 1px var(--crt-sub-color-glow), - -3px 0 var(--crt-sub-color-glow), - 0 0 3px; -} - -body.crtmode #testModesNotice .textButton:hover { - text-shadow: - 3px 0 1px var(--crt-text-color-glow), - -3px 0 var(--crt-text-color-glow), - 0 0 3px; -} - -body.crtmode #testModesNotice .textButton.active { - text-shadow: - 3px 0 1px var(--crt-main-color-glow), - -3px 0 var(--crt-main-color-glow), - 0 0 3px; -} - -body.crtmode #testModesNotice .row { - box-shadow: - 3px 0 1px var(--crt-sub-alt-color-glow), - -3px 0 var(--crt-sub-alt-color-glow); -} - body.crtmode #caret { box-shadow: 3px 0 1px var(--crt-caret-color-glow), From 7ccdea5679f5650b2920225c80f740acc075aa91 Mon Sep 17 00:00:00 2001 From: Leonabcd123 <156839416+Leonabcd123@users.noreply.github.com> Date: Tue, 2 Jun 2026 16:13:52 +0300 Subject: [PATCH 2/9] fix(nospace): don't move to next word when removing last character (@Leonabcd123) (#8038) Fixes #8037 Not sure whether `stop on error = word` should have any effect when `nospace` is enabled, so this pr currently only fixes the case where a mistake isn't blocked at the end of a word in `nospace`. --- frontend/src/ts/input/handlers/insert-text.ts | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/frontend/src/ts/input/handlers/insert-text.ts b/frontend/src/ts/input/handlers/insert-text.ts index af461149f4ca..fb9f453a7ea9 100644 --- a/frontend/src/ts/input/handlers/insert-text.ts +++ b/frontend/src/ts/input/handlers/insert-text.ts @@ -149,12 +149,39 @@ export async function onInsertText(options: OnInsertTextParams): Promise { correctShiftUsed, }); + // handing cases where last char needs to be removed + // this is here and not in beforeInsertText because we want to penalize for incorrect spaces + // like accuracy, keypress errors, and missed words + let removeLastChar = false; + let visualInputOverride: string | undefined; + if (Config.stopOnError === "letter" && !correct) { + if (!Config.blindMode) { + visualInputOverride = testInput + data; + } + removeLastChar = true; + } + + if (!isSpace(data) && correctShiftUsed === false) { + removeLastChar = true; + visualInputOverride = undefined; + incrementIncorrectShiftsInARow(); + if (getIncorrectShiftsInARow() >= 5) { + showNoticeNotification("Opposite shift mode is on.", { + important: true, + customTitle: "Reminder", + }); + } + } else { + resetIncorrectShiftsInARow(); + } + // word navigation check const noSpaceForce = isFunboxActiveWithProperty("nospace") && (testInput + data).length === TestWords.words.getCurrentText().length; const shouldGoToNextWord = - ((charIsSpace || charIsNewline) && !shouldInsertSpace) || noSpaceForce; + !removeLastChar && + (((charIsSpace || charIsNewline) && !shouldInsertSpace) || noSpaceForce); // update test input state if (!charIsSpace || shouldInsertSpace) { @@ -182,32 +209,6 @@ export async function onInsertText(options: OnInsertTextParams): Promise { TestInput.corrected.update(data, correct); } - // handing cases where last char needs to be removed - // this is here and not in beforeInsertText because we want to penalize for incorrect spaces - // like accuracy, keypress errors, and missed words - let removeLastChar = false; - let visualInputOverride: string | undefined; - if (Config.stopOnError === "letter" && !correct) { - if (!Config.blindMode) { - visualInputOverride = testInput + data; - } - removeLastChar = true; - } - - if (!isSpace(data) && correctShiftUsed === false) { - removeLastChar = true; - visualInputOverride = undefined; - incrementIncorrectShiftsInARow(); - if (getIncorrectShiftsInARow() >= 5) { - showNoticeNotification("Opposite shift mode is on.", { - important: true, - customTitle: "Reminder", - }); - } - } else { - resetIncorrectShiftsInARow(); - } - if (removeLastChar) { replaceInputElementLastValueChar(""); TestInput.input.syncWithInputElement(); From 6e0b2f06335b20a081eebb5d4621e84a6e60185a Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 2 Jun 2026 15:27:52 +0200 Subject: [PATCH 3/9] impr(custom text): add button to download custom texts (@fehmer) (#8039) --- .../ts/components/modals/SavedTextsModal.tsx | 31 +++++++++++++++++++ frontend/src/ts/test/test-screenshot.ts | 19 +++--------- frontend/src/ts/utils/misc.ts | 16 ++++++---- 3 files changed, 46 insertions(+), 20 deletions(-) diff --git a/frontend/src/ts/components/modals/SavedTextsModal.tsx b/frontend/src/ts/components/modals/SavedTextsModal.tsx index a63829b5d139..18a76e702f12 100644 --- a/frontend/src/ts/components/modals/SavedTextsModal.tsx +++ b/frontend/src/ts/components/modals/SavedTextsModal.tsx @@ -2,8 +2,13 @@ import { createSignal, For, Index, JSXElement, Setter, Show } from "solid-js"; import { setCustomTextIndicator } from "../../states/core"; import { hideModal } from "../../states/modals"; +import { + showErrorNotification, + showSuccessNotification, +} from "../../states/notifications"; import { showSimpleModal } from "../../states/simple-modal"; import * as CustomText from "../../test/custom-text"; +import { download } from "../../utils/misc"; import { AnimatedModal } from "../common/AnimatedModal"; import { Button } from "../common/Button"; import { Separator } from "../common/Separator"; @@ -81,6 +86,18 @@ export function SavedTextsModal(props: { }); }; + const handleDownload = (name: string, long: boolean) => { + const text = CustomText.getCustomText(name, long); + + try { + const data = new Blob([text.join(" ")], { type: "text/plain" }); + download({ filename: `${name}.txt`, data }); + showSuccessNotification("custom text downloaded"); + } catch (e) { + showErrorNotification(`failed to download custom text: ${e}`); + } + }; + return ( handleNameClick(name, false)} /> +
@@ -142,9 +166,16 @@ export function SavedTextsModal(props: { disabled={!hasProgress()} onClick={() => handleResetProgress(name())} /> +
diff --git a/frontend/src/ts/test/test-screenshot.ts b/frontend/src/ts/test/test-screenshot.ts index acdf7fdf4f0c..6e0def0152c3 100644 --- a/frontend/src/ts/test/test-screenshot.ts +++ b/frontend/src/ts/test/test-screenshot.ts @@ -18,6 +18,7 @@ import { convertRemToPixels } from "../utils/numbers"; import * as TestState from "./test-state"; import { qs, qsa } from "../utils/dom"; import { getTheme } from "../states/theme"; +import { download as downloadFile } from "../utils/misc"; let revealReplay = false; @@ -310,26 +311,16 @@ async function getBlob(): Promise { export async function download(): Promise { try { - const blob = await getBlob(); + const data = await getBlob(); - if (!blob) { + if (!data) { showErrorNotification("Failed to generate screenshot data"); return; } - - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); - link.download = `monkeytype-result-${timestamp}.png`; - - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); + const filename = `monkeytype-result-${timestamp}.png`; - URL.revokeObjectURL(url); + downloadFile({ data, filename }); showSuccessNotification("Screenshot download started"); } catch (error) { diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index ad12406dafa2..6480e38ae000 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -317,16 +317,20 @@ export async function downloadResultsCSV(array: Result[]): Promise { .join("\n"); const blob = new Blob([csvString], { type: "text/csv" }); + download({ filename: "results.csv", data: blob }); +} - const href = window.URL.createObjectURL(blob); - +export function download(options: { filename: string; data: Blob }): void { + const url = URL.createObjectURL(options.data); const link = document.createElement("a"); - link.setAttribute("href", href); - link.setAttribute("download", "results.csv"); - document.body.appendChild(link); // Required for FF + link.href = url; + link.download = options.filename; + document.body.appendChild(link); link.click(); - link.remove(); + document.body.removeChild(link); + + URL.revokeObjectURL(url); } export function isElementVisible(query: string): boolean { From c9e6289c5fda3f7e4abd67ceda9f1c881f3998a6 Mon Sep 17 00:00:00 2001 From: Miodec Date: Tue, 2 Jun 2026 15:46:51 +0200 Subject: [PATCH 4/9] chore: log funboxes to db --- backend/src/api/controllers/result.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index db5f605cedce..28fdf24b8aba 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -198,6 +198,7 @@ export async function reportCompletedEventMismatch( mode2, difficulty, duration, + funboxes, } = req.body; // Logger.warning( // `Completed event mismatch for uid ${uid}: ${notMatching.join(", ")}`, @@ -215,6 +216,7 @@ export async function reportCompletedEventMismatch( mode2, difficulty, duration, + funboxes, }, uid, ); From 5952c58cccc43f7789d2dedc06c55ca65741fcaf Mon Sep 17 00:00:00 2001 From: Christian Fehmer Date: Tue, 2 Jun 2026 18:22:05 +0200 Subject: [PATCH 5/9] fix(signup): signup with provider not working (@fehmer) (#8046) a4cfbb4f1eb0c2815afb7c7b40218da954d693f9 causes backend calls before the user was created on the backend --- frontend/src/ts/firebase.ts | 4 ++-- frontend/src/ts/modals/google-sign-up.ts | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/firebase.ts b/frontend/src/ts/firebase.ts index 7aff3b0624fd..55462c8bc268 100644 --- a/frontend/src/ts/firebase.ts +++ b/frontend/src/ts/firebase.ts @@ -134,7 +134,7 @@ export async function signInWithEmailAndPassword( return result; } -function setUserState( +export function setUserState( options: { uid: string; emailVerified: boolean; @@ -166,10 +166,10 @@ export async function signInWithPopup( throw translateFirebaseError(error, "Failed to sign in with popup"); } const additionalUserInfo = getAdditionalUserInfo(signedInUser); - setUserState(signedInUser.user); if (additionalUserInfo?.isNewUser) { googleSignUpEvent.dispatch({ signedInUser, isNewUser: true }); } else { + setUserState(signedInUser.user); ignoreAuthCallback = false; await readyCallback?.(true, signedInUser.user); } diff --git a/frontend/src/ts/modals/google-sign-up.ts b/frontend/src/ts/modals/google-sign-up.ts index 8f6305d0438f..63ab9b4686b4 100644 --- a/frontend/src/ts/modals/google-sign-up.ts +++ b/frontend/src/ts/modals/google-sign-up.ts @@ -17,7 +17,7 @@ import * as CaptchaController from "../controllers/captcha-controller"; import { showLoaderBar, hideLoaderBar } from "../states/loader-bar"; import { googleSignUpEvent } from "../events/google-sign-up"; import AnimatedModal from "../utils/animated-modal"; -import { resetIgnoreAuthCallback } from "../firebase"; +import { resetIgnoreAuthCallback, setUserState } from "../firebase"; import { ValidatedHtmlInputElement } from "../elements/input-validation"; import { UserNameSchema } from "@monkeytype/schemas/users"; import { remoteValidation } from "../utils/remote-validation"; @@ -105,6 +105,7 @@ async function apply(): Promise { } if (response.status === 200) { + setUserState(signedInUser.user); await updateProfile(signedInUser.user, { displayName: name }); await sendEmailVerification(signedInUser.user); showSuccessNotification("Account created"); From 8b6e863162168d84a9467559c2b66a6f242f88b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 18:31:38 +0200 Subject: [PATCH 6/9] chore(deps): bump lru-cache from 7.10.1 to 11.5.1 (#8028) Bumps [lru-cache](https://github.com/isaacs/node-lru-cache) from 7.10.1 to 11.5.1.
Changelog

Sourced from lru-cache's changelog.

cringe lorg

11.5

  • Add backgroundFetchSize option, defaulting to 1, to set an effective size for provisional background fetch objects while in flight, if they do not shadow an existing stale entry.

11.4

  • Add cache property to status objects, in order to differentiate which cache is emitting the metric or trace.
  • Several small bugs regarding fetch behavior edge cases.
    • onInsert does not fire for background fetch internal promises.
    • dispose() and disposeAfter() now fire for the stale value left behind when an in-process background fetch is pre-empted by eviction.
    • fetchMethod that returns a non-Promise value is handled correctly.
    • No Error is created, or abort() signaled, when a background fetch promise is resolved. (Presumably the implementation is done by that point.)

11.3

  • Add observability features, expand the coverage of LRUCache.Status objects.

11.2

  • Add the perf option to specify performance, Date, or any other object with a now() method that returns a number.

11.1

  • Add the onInsert method

11.0

  • Drop support for node less than v20

10.4

  • Accidental minor update, should've been patch.

10.3

  • add forceFetch() method
  • set disposeReason to 'expire' when it's the result of a TTL

... (truncated)

Commits
Install script changes

This version adds prepare script that runs during installation. Review the package contents before updating.


--------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Miodec --- backend/package.json | 2 +- backend/src/queues/later-queue.ts | 2 +- backend/src/utils/auth.ts | 2 +- pnpm-lock.yaml | 24 +++++++++--------------- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/backend/package.json b/backend/package.json index 62822237b713..8ed5d50dc86e 100644 --- a/backend/package.json +++ b/backend/package.json @@ -41,7 +41,7 @@ "firebase-admin": "12.0.0", "helmet": "4.6.0", "ioredis": "4.28.5", - "lru-cache": "7.10.1", + "lru-cache": "11.5.1", "mjml": "4.15.0", "mongodb": "6.3.0", "mustache": "4.2.0", diff --git a/backend/src/queues/later-queue.ts b/backend/src/queues/later-queue.ts index 8cd5194fdef5..c52283bab913 100644 --- a/backend/src/queues/later-queue.ts +++ b/backend/src/queues/later-queue.ts @@ -1,4 +1,4 @@ -import LRUCache from "lru-cache"; +import { LRUCache } from "lru-cache"; import Logger from "../utils/logger"; import { MonkeyQueue } from "./monkey-queue"; import { ValidModeRule } from "@monkeytype/schemas/configuration"; diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index a218010eb7c9..aed04d4c4f07 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,5 +1,5 @@ import FirebaseAdmin from "./../init/firebase-admin"; -import LRUCache from "lru-cache"; +import { LRUCache } from "lru-cache"; import { recordTokenCacheAccess, setTokenCacheLength, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d83eeaf6ece..54210ef9bff8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -141,8 +141,8 @@ importers: specifier: 4.28.5 version: 4.28.5 lru-cache: - specifier: 7.10.1 - version: 7.10.1 + specifier: 11.5.1 + version: 11.5.1 mjml: specifier: 4.15.0 version: 4.15.0(encoding@0.1.13) @@ -8502,8 +8502,8 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} - lru-cache@11.2.4: - resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==} + lru-cache@11.5.1: + resolution: {integrity: sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -8513,10 +8513,6 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} - lru-cache@7.10.1: - resolution: {integrity: sha512-BQuhQxPuRl79J5zSXRP+uNzPOyZw2oFI9JLRQ80XswSvg21KMKNtQza9eF42rfI/3Z40RvzBdXgziEkudzjo8A==} - engines: {node: '>=12'} - lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} engines: {node: '>=12'} @@ -11954,7 +11950,7 @@ snapshots: '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) '@csstools/css-tokenizer': 3.0.4 - lru-cache: 11.2.4 + lru-cache: 11.5.1 '@asamuzakjp/dom-selector@6.7.6': dependencies: @@ -11962,7 +11958,7 @@ snapshots: bidi-js: 1.0.3 css-tree: 3.1.0 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.4 + lru-cache: 11.5.1 '@asamuzakjp/nwsapi@2.3.9': {} @@ -17755,7 +17751,7 @@ snapshots: '@asamuzakjp/css-color': 4.1.1 '@csstools/css-syntax-patches-for-csstree': 1.0.23 css-tree: 3.1.0 - lru-cache: 11.2.4 + lru-cache: 11.5.1 csstype@3.2.3: {} @@ -20429,7 +20425,7 @@ snapshots: lru-cache@10.4.3: {} - lru-cache@11.2.4: {} + lru-cache@11.5.1: {} lru-cache@5.1.1: dependencies: @@ -20439,8 +20435,6 @@ snapshots: dependencies: yallist: 4.0.0 - lru-cache@7.10.1: {} - lru-cache@7.18.3: {} lru-memoizer@2.3.0: @@ -21571,7 +21565,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.4 + lru-cache: 11.5.1 minipass: 7.1.3 path-to-regexp@0.1.12: {} From 0a83d38cdbaaf82181eb1d2322e3dd238e589f55 Mon Sep 17 00:00:00 2001 From: Don <78839918+donmaruko@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:32:25 -0400 Subject: [PATCH 7/9] feat(quotes): add Witch Hat Atelier quote (@donmaruko) (#8030) Added a quote from Witch Hat Atelier (manga). The quote is in English and sourced from the official English translation. --- frontend/static/quotes/english.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index 0ac1c29a9451..590864655a65 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -39315,6 +39315,12 @@ "source": "The Stanley Parable", "id": 7765, "length": 242 + }, + { + "text": "Draw so that you may remember. Remember so that you may use. The power you imbue yourself thus… will prove an ally that never betrays.", + "source": "Witch Hat Atelier", + "id": 7766, + "length": 134 } ] } From 9ffa67cc51790d3190bc323ad2758d4b4f081dbb Mon Sep 17 00:00:00 2001 From: Leonabcd123 <156839416+Leonabcd123@users.noreply.github.com> Date: Tue, 2 Jun 2026 19:33:11 +0300 Subject: [PATCH 8/9] docs(rate-limit): update comments in rate limit file (@Leonabcd123) (#8026) - Capitalization - Add another comment for `5000` ms - Change every `x min` to `x minutes` (could be changed the other way, just important to be consistent) - Update `resultsMismatchReport` comment to be `5 minutes` instead of `15 min`. --- packages/contracts/src/rate-limit/index.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/contracts/src/rate-limit/index.ts b/packages/contracts/src/rate-limit/index.ts index a92e969dc4f1..cd4577fd22da 100644 --- a/packages/contracts/src/rate-limit/index.ts +++ b/packages/contracts/src/rate-limit/index.ts @@ -13,7 +13,7 @@ export const limits = { }, adminLimit: { - window: 5000, + window: 5000, // 5 seconds max: 1, }, @@ -73,23 +73,23 @@ export const limits = { // Quote reporting quoteReportSubmit: { - window: 30 * 60 * 1000, // 30 min + window: 30 * 60 * 1000, // 30 minutes max: 50, }, // Quote favorites quoteFavoriteGet: { - window: 30 * 60 * 1000, // 30 min + window: 30 * 60 * 1000, // 30 minutes max: 50, }, quoteFavoritePost: { - window: 30 * 60 * 1000, // 30 min + window: 30 * 60 * 1000, // 30 minutes max: 50, }, quoteFavoriteDelete: { - window: 30 * 60 * 1000, // 30 min + window: 30 * 60 * 1000, // 30 minutes max: 50, }, @@ -120,7 +120,7 @@ export const limits = { max: 60, }, - // get public speed stats + // Get public speed stats publicStatsGet: { window: "minute", max: 60, @@ -166,7 +166,7 @@ export const limits = { }, resultsMismatchReport: { - window: 5 * 60 * 1000, // 15 min + window: 5 * 60 * 1000, // 5 minutes max: 1, }, @@ -395,7 +395,7 @@ export const limits = { export type RateLimiterId = keyof typeof limits; export type RateLimitIds = { - /** rate limiter options for non-apeKey requests */ + /** Rate limiter options for non-apeKey requests */ normal: RateLimiterId; /** Rate limiter options for apeKey requests */ apeKey: RateLimiterId; From fd274a963a5252741530e1eab5884ac42cccdc81 Mon Sep 17 00:00:00 2001 From: Acas <67846394+MightyAcas@users.noreply.github.com> Date: Tue, 2 Jun 2026 12:42:30 -0400 Subject: [PATCH 9/9] feat(layout): add vylet_v4 layout (@MightyAcas) (#8024) ### Description Adding my personal layout: Vylet v4. This is the latest iteration of my original [Vylet](https://github.com/MightyAcas/vylet) layout. ### Checks - [ ] Adding quotes? - Make sure to follow the [quotes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/QUOTES.md) - [ ] Make sure to include translations for the quotes in the description (or another comment) so we can verify their content. - [ ] Adding a language? - Make sure to follow the [languages documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LANGUAGES.md) - [ ] Add language to `packages/schemas/src/languages.ts` - [ ] Add language to exactly one group in `frontend/src/ts/constants/languages.ts` - [ ] Add language json file to `frontend/static/languages` - [ ] Adding a theme? - Make sure to follow the [themes documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/THEMES.md) - [ ] Add theme to `packages/schemas/src/themes.ts` - [ ] Add theme to `frontend/src/ts/constants/themes.ts` - [ ] (optional) Add theme css file to `frontend/static/themes` - [ ] Add some screenshots of the theme, especially with different test settings (colorful, flip colors) to your pull request - [x] Adding a layout? - [x] Make sure to follow the [layouts documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/LAYOUTS.md) - [x] Add layout to `packages/schemas/src/layouts.ts` - [x] Add layout json file to `frontend/static/layouts` - [ ] Adding a font? - Make sure to follow the [fonts documentation](https://github.com/monkeytypegame/monkeytype/blob/master/docs/FONTS.md) - [ ] Add font file to `frontend/static/webfonts` - [ ] Add font to `packages/schemas/src/fonts.ts` - [ ] Add font to `frontend/src/ts/constants/fonts.ts` - [x] Check if any open issues are related to this PR; if so, be sure to tag them below. - [x] Make sure the PR title follows the Conventional Commits standard. (https://www.conventionalcommits.org for more info) - [x] Make sure to include your GitHub username prefixed with @ inside parentheses at the end of the PR title. Closes # --- frontend/static/layouts/vylet_v4.json | 62 +++++++++++++++++++++++++++ packages/schemas/src/layouts.ts | 1 + 2 files changed, 63 insertions(+) create mode 100644 frontend/static/layouts/vylet_v4.json diff --git a/frontend/static/layouts/vylet_v4.json b/frontend/static/layouts/vylet_v4.json new file mode 100644 index 000000000000..78780ec41558 --- /dev/null +++ b/frontend/static/layouts/vylet_v4.json @@ -0,0 +1,62 @@ +{ + "keymapShowTopRow": false, + "type": "ansi", + "keys": { + "row1": [ + ["`", "~"], + ["1", "!"], + ["2", "@"], + ["3", "#"], + ["4", "$"], + ["5", "%"], + ["6", "^"], + ["7", "&"], + ["8", "*"], + ["9", "("], + ["0", ")"], + ["[", "{"], + ["]", "}"] + ], + "row2": [ + ["w", "W"], + ["c", "C"], + ["m", "M"], + ["p", "P"], + ["k", "K"], + ["x", "X"], + ["l", "L"], + ["o", "O"], + ["u", "U"], + ["j", "J"], + ["-", "_"], + ["=", "+"], + ["\\", "|"] + ], + "row3": [ + ["r", "R"], + ["s", "S"], + ["t", "T"], + ["h", "H"], + ["f", "F"], + ["'", "\""], + ["n", "N"], + ["a", "A"], + ["e", "E"], + ["i", "I"], + ["/", "?"] + ], + "row4": [ + ["q", "Q"], + ["g", "G"], + ["v", "V"], + ["d", "D"], + ["b", "B"], + ["z", "Z"], + ["y", "Y"], + [".", "<"], + [";", ":"], + [",", ">"] + ], + "row5": [[" "]] + } +} diff --git a/packages/schemas/src/layouts.ts b/packages/schemas/src/layouts.ts index 0f9fe638508e..ce3cc2cd32d9 100644 --- a/packages/schemas/src/layouts.ts +++ b/packages/schemas/src/layouts.ts @@ -240,6 +240,7 @@ export const LayoutNameSchema = z.enum( "vitrimak", "miligram", "nokwts", + "vylet_v4", ], { errorMap: customEnumErrorHandler("Must be a supported layout"),