diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 49f01e1ba8a9..dd649439c014 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -22,6 +22,8 @@
Time left to memorise all words: 0s
Time left to memorise all words: 0s
+ +
diff --git a/frontend/src/ts/collections/results.ts b/frontend/src/ts/collections/results.ts index a9146207224c..9321cf2b2c3a 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"; @@ -30,12 +30,15 @@ import { baseKey } from "../queries/utils/keys"; import { isAuthenticated } from "../states/core"; import { getLastResult, setLastResult } from "../states/snapshot"; import { - getActiveTagsOnce, reconcileLocalTagPB, saveLocalTagPB, __nonReactive as tagsNonReactive, + 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"][]; @@ -215,6 +218,7 @@ const resultsCollection = createCollection( queryKey: queryKeys.root(), queryFn: async () => { if (!isAuthenticated()) return []; + const knownTagIds = new Set( tagsNonReactive.getTags().map((it) => it._id), ); @@ -581,39 +585,64 @@ 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 queryOptions = createMemo(() => ({ + ...getConfig, + mode2: getMode2(getConfig, getCurrentQuote()), + })); + + 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(queryOptions(), { + activeTagsOnly: true, + }) + .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) => q .from({ //we use sub-query to filter first and then aggregate - last10: buildSettingsResultsQuery(options, { tagIds }) + last10: buildSettingsResultsQuery(options, { + activeTagsOnly: true, + }) .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 if (!isAuthenticated()) return { wpm: 0, acc: 0 }; - const tagIds = (await getActiveTagsOnce()).map((it) => it._id); const result = await queryOnce(() => - buildSettingsResultsQuery(options, { tagIds }) + buildSettingsResultsQuery(options, { activeTagsOnly: true }) .where(({ r }) => gte(r.timestamp, Date.now() - 24 * 60 * 60 * 1000)) .orderBy(({ r }) => r.wpm, "desc") .limit(1) @@ -623,13 +652,13 @@ export async function getUserDailyBest( return result ?? { wpm: 0, acc: 0 }; } +const activeTagQuery = useActiveTagsLiveQuery(); + // oxlint-disable-next-line typescript/explicit-function-return-type function buildSettingsResultsQuery( filter: CurrentSettingsFilter, - options?: { tagIds?: string[] }, + options?: { activeTagsOnly?: boolean }, ) { - const tagIds = options?.tagIds; - let query = new Query() .from({ r: resultsCollection }) .where(({ r }) => eq(r.mode, filter.mode)) @@ -638,14 +667,15 @@ function buildSettingsResultsQuery( .where(({ r }) => eq(r.numbers, filter.numbers)) .where(({ r }) => eq(r.language, filter.language)) .where(({ r }) => eq(r.difficulty, filter.difficulty)) + .where(({ r }) => eq(r.lazyMode, filter.lazyMode)); - if (tagIds !== undefined) { + if (options?.activeTagsOnly) { query = query.where(({ r }) => or( false, - tagIds.length === 0, - ...tagIds.map((it) => inArray(it, r.tags)), + activeTagQuery().length === 0, + ...activeTagQuery().map((it) => inArray(it._id, r.tags)), ), ); } diff --git a/frontend/src/ts/collections/tags.ts b/frontend/src/ts/collections/tags.ts index a101c873bece..90f47383a06f 100644 --- a/frontend/src/ts/collections/tags.ts +++ b/frontend/src/ts/collections/tags.ts @@ -4,7 +4,6 @@ import { createCollection, createOptimisticAction, eq, - queryOnce, useLiveQuery, } from "@tanstack/solid-db"; import { z } from "zod"; @@ -64,8 +63,8 @@ export function useTagsLiveQuery() { } // oxlint-disable-next-line typescript/explicit-function-return-type -export async function getActiveTagsOnce() { - return queryOnce((q) => { +export function useActiveTagsLiveQuery() { + return useLiveQuery((q) => { return q .from({ tag: tagsCollection }) .where(({ tag }) => eq(tag.active, true)) 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/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/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 b0ce72d8e261..324b527778db 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 { Settings } from "./pages/settings/Settings"; import { TestConfig } from "./pages/test/TestConfig"; import { Popups } from "./popups/Popups"; +import { TestModesNotice } from "./test/modes-notice/TestModesNotice"; const components: Record JSXElement> = { footer: () =>