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/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, ); 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/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 @@
; - +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( - `${getLanguageDisplayString( - Config.language, - Config.mode === "quote", - )} `, - ); - } - - if (usingPolyglot) { - const languages = Config.customPolyglot - .map((lang) => { - const langDisplay = getLanguageDisplayString(lang, true); - return langDisplay; - }) - .join(", "); - - testModesNotice.appendHtml( - `${languages} `, - ); - } - - if (Config.difficulty === "expert") { - testModesNotice.appendHtml( - `expert `, - ); - } else if (Config.difficulty === "master") { - testModesNotice.appendHtml( - `master `, - ); - } - - if (Config.blindMode) { - testModesNotice.appendHtml( - `blind `, - ); - } - - if (Config.lazyMode) { - testModesNotice.appendHtml( - `lazy `, - ); - } - - if ( - Config.paceCaret !== "off" || - (Config.repeatedPace && TestState.isPaceRepeat) - ) { - const speed = Format.typingSpeed(PaceCaret.settings?.wpm ?? 0, { - showDecimalPlaces: false, - suffix: ` ${Config.typingSpeedUnit}`, - }); - - testModesNotice.appendHtml( - `${ - Config.paceCaret === "average" - ? "average" - : Config.paceCaret === "pb" - ? "pb" - : Config.paceCaret === "tagPb" - ? "tag pb" - : Config.paceCaret === "last" - ? "last" - : Config.paceCaret === "daily" - ? "daily" - : "custom" - } pace ${speed} `, - ); - } - - 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( - `avg: ${text} `, - ); - } - } - - 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( - `${str} `, - ); - } - - if (Config.minWpm !== "off") { - testModesNotice.appendHtml( - `min ${Format.typingSpeed( - Config.minWpmCustomSpeed, - { showDecimalPlaces: false, suffix: ` ${Config.typingSpeedUnit}` }, - )} `, - ); - } - - if (Config.minAcc !== "off") { - testModesNotice.appendHtml( - `min ${Config.minAccCustom}% acc `, - ); - } - - if (Config.minBurst !== "off") { - testModesNotice.appendHtml( - `min ${Format.typingSpeed( - Config.minBurstCustomSpeed, - { showDecimalPlaces: false }, - )} ${Config.typingSpeedUnit} burst ${ - Config.minBurst === "flex" ? "(flex)" : "" - } `, - ); - } - - if (Config.funbox.length > 0) { - testModesNotice.appendHtml( - `${Config.funbox - .map((it) => it.replace(/_/g, " ")) - .join(", ")} `, - ); - } - - if (Config.confidenceMode === "on") { - testModesNotice.appendHtml( - `confidence `, - ); - } - if (Config.confidenceMode === "max") { - testModesNotice.appendHtml( - `max confidence `, - ); - } - - if (Config.stopOnError !== "off") { - testModesNotice.appendHtml( - `stop on ${Config.stopOnError} `, - ); - } - - if (Config.layout !== "default") { - testModesNotice.appendHtml( - `emulating ${Config.layout.replace( - /_/g, - " ", - )} `, - ); - } - - if (Config.oppositeShiftMode !== "off") { - testModesNotice.appendHtml( - `opposite shift${ - Config.oppositeShiftMode === "keymap" ? " (keymap)" : "" - } `, - ); - } - - let tagsString = ""; - try { - __nonReactive.getActiveTags().forEach((tag) => { - tagsString += `${tag.name}, `; - }); - - if (tagsString !== "") { - testModesNotice.appendHtml( - `${tagsString.substring( - 0, - tagsString.length - 2, - )} `, - ); - } - } 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/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/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(); 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/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"); 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-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/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/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 { 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), 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/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 } ] } 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; 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"), 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: {}