diff --git a/frontend/__tests__/utils/strings.spec.ts b/frontend/__tests__/utils/strings.spec.ts
index cae1aeef927d..8fa02f4c5e81 100644
--- a/frontend/__tests__/utils/strings.spec.ts
+++ b/frontend/__tests__/utils/strings.spec.ts
@@ -265,7 +265,7 @@ describe("string utils", () => {
] as const)(
"should return %s for word '%s' (%s)",
(expected: boolean, word: string, _description: string) => {
- expect(Strings.__testing.hasRTLCharacters(word)).toBe(expected);
+ expect(Strings.__testing.hasRTLCharacters(word)[0]).toBe(expected);
},
);
});
@@ -321,27 +321,27 @@ describe("string utils", () => {
languageRTL: boolean,
_description: string,
) => {
- expect(Strings.isWordRightToLeft(word, languageRTL)).toBe(expected);
+ expect(Strings.isWordRightToLeft(word, languageRTL)[0]).toBe(expected);
},
);
it("should return languageRTL for undefined word", () => {
- expect(Strings.isWordRightToLeft(undefined, false)).toBe(false);
- expect(Strings.isWordRightToLeft(undefined, true)).toBe(true);
+ expect(Strings.isWordRightToLeft(undefined, false)[0]).toBe(false);
+ expect(Strings.isWordRightToLeft(undefined, true)[0]).toBe(true);
});
// testing reverseDirection
it("should return true for LTR word with reversed direction", () => {
- expect(Strings.isWordRightToLeft("hello", false, true)).toBe(true);
- expect(Strings.isWordRightToLeft("hello", true, true)).toBe(true);
+ expect(Strings.isWordRightToLeft("hello", false, true)[0]).toBe(true);
+ expect(Strings.isWordRightToLeft("hello", true, true)[0]).toBe(true);
});
it("should return false for RTL word with reversed direction", () => {
- expect(Strings.isWordRightToLeft("مرحبا", true, true)).toBe(false);
- expect(Strings.isWordRightToLeft("مرحبا", false, true)).toBe(false);
+ expect(Strings.isWordRightToLeft("مرحبا", true, true)[0]).toBe(false);
+ expect(Strings.isWordRightToLeft("مرحبا", false, true)[0]).toBe(false);
});
it("should return reverse of languageRTL for undefined word with reversed direction", () => {
- expect(Strings.isWordRightToLeft(undefined, false, true)).toBe(true);
- expect(Strings.isWordRightToLeft(undefined, true, true)).toBe(false);
+ expect(Strings.isWordRightToLeft(undefined, false, true)[0]).toBe(true);
+ expect(Strings.isWordRightToLeft(undefined, true, true)[0]).toBe(false);
});
describe("caching", () => {
@@ -364,8 +364,8 @@ describe("string utils", () => {
it("should use cache for repeated calls", () => {
// First call should cache the result (cache miss)
const result1 = Strings.isWordRightToLeft("hello", false);
- expect(result1).toBe(false);
- expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
+ expect(result1[0]).toBe(false);
+ expect(mapSetSpy).toHaveBeenCalledWith("hello", [false, 0]);
// Reset spies to check second call
mapGetSpy.mockClear();
@@ -373,7 +373,7 @@ describe("string utils", () => {
// Second call should use cache (cache hit)
const result2 = Strings.isWordRightToLeft("hello", false);
- expect(result2).toBe(false);
+ expect(result2[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
@@ -382,7 +382,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
const result3 = Strings.isWordRightToLeft("hello", true);
- expect(result3).toBe(false); // Still false because "hello" is LTR regardless of language
+ expect(result3[0]).toBe(false); // Still false because "hello" is LTR regardless of language
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled(); // Should not set again
});
@@ -390,15 +390,15 @@ describe("string utils", () => {
it("should cache based on core word without punctuation", () => {
// First call should cache the result for core "hello"
const result1 = Strings.isWordRightToLeft("hello", false);
- expect(result1).toBe(false);
- expect(mapSetSpy).toHaveBeenCalledWith("hello", false);
+ expect(result1[0]).toBe(false);
+ expect(mapSetSpy).toHaveBeenCalledWith("hello", [false, 0]);
mapGetSpy.mockClear();
mapSetSpy.mockClear();
// These should all use the same cache entry since they have the same core
const result2 = Strings.isWordRightToLeft("hello!", false);
- expect(result2).toBe(false);
+ expect(result2[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
@@ -406,7 +406,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
const result3 = Strings.isWordRightToLeft("!hello", false);
- expect(result3).toBe(false);
+ expect(result3[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
@@ -414,7 +414,7 @@ describe("string utils", () => {
mapSetSpy.mockClear();
const result4 = Strings.isWordRightToLeft("!hello!", false);
- expect(result4).toBe(false);
+ expect(result4[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("hello");
expect(mapSetSpy).not.toHaveBeenCalled();
});
@@ -422,7 +422,7 @@ describe("string utils", () => {
it("should handle cache clearing", () => {
// Cache a result
Strings.isWordRightToLeft("test", false);
- expect(mapSetSpy).toHaveBeenCalledWith("test", false);
+ expect(mapSetSpy).toHaveBeenCalledWith("test", [false, 0]);
// Clear cache
Strings.clearWordDirectionCache();
@@ -434,23 +434,23 @@ describe("string utils", () => {
// Should work normally after cache clear (cache miss again)
const result = Strings.isWordRightToLeft("test", false);
- expect(result).toBe(false);
- expect(mapSetSpy).toHaveBeenCalledWith("test", false);
+ expect(result[0]).toBe(false);
+ expect(mapSetSpy).toHaveBeenCalledWith("test", [false, 0]);
});
it("should demonstrate cache miss vs cache hit behavior", () => {
// Test cache miss - first time seeing this word
const result1 = Strings.isWordRightToLeft("unique", false);
- expect(result1).toBe(false);
+ expect(result1[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
- expect(mapSetSpy).toHaveBeenCalledWith("unique", false);
+ expect(mapSetSpy).toHaveBeenCalledWith("unique", [false, 0]);
mapGetSpy.mockClear();
mapSetSpy.mockClear();
// Test cache hit - same word again
const result2 = Strings.isWordRightToLeft("unique", false);
- expect(result2).toBe(false);
+ expect(result2[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("unique");
expect(mapSetSpy).not.toHaveBeenCalled(); // No cache set on hit
@@ -459,9 +459,9 @@ describe("string utils", () => {
// Test cache miss - different word
const result3 = Strings.isWordRightToLeft("different", false);
- expect(result3).toBe(false);
+ expect(result3[0]).toBe(false);
expect(mapGetSpy).toHaveBeenCalledWith("different");
- expect(mapSetSpy).toHaveBeenCalledWith("different", false);
+ expect(mapSetSpy).toHaveBeenCalledWith("different", [false, 0]);
});
});
});
diff --git a/frontend/src/html/pages/settings.html b/frontend/src/html/pages/settings.html
index 50d329dcb723..af7618edf5b6 100644
--- a/frontend/src/html/pages/settings.html
+++ b/frontend/src/html/pages/settings.html
@@ -697,6 +697,7 @@
wholetone
fist fight
rubber keys
+ fart
diff --git a/frontend/src/html/popups.html b/frontend/src/html/popups.html
index 686dd1306e99..7dff29cceec5 100644
--- a/frontend/src/html/popups.html
+++ b/frontend/src/html/popups.html
@@ -1060,9 +1060,17 @@
You can only do this once!
-
+
+
+
+
+
+
+
+
+
- set
+ set
diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss
index 644d865acad6..c29d727145e1 100644
--- a/frontend/src/styles/popups.scss
+++ b/frontend/src/styles/popups.scss
@@ -1636,6 +1636,20 @@ body.darkMode {
.red {
color: var(--error-color);
}
+ .group {
+ display: grid;
+ grid-template-columns: 1fr 1fr 1fr;
+ gap: 0.5rem;
+ justify-items: center;
+ align-items: center;
+ font-size: 2em;
+ button {
+ width: 100%;
+ }
+ input {
+ text-align: center;
+ }
+ }
.preview {
& > div:first-child {
margin-bottom: 1rem;
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss
index 82bfdb89c81a..d0064f2c7c24 100644
--- a/frontend/src/styles/test.scss
+++ b/frontend/src/styles/test.scss
@@ -319,6 +319,10 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
+
+ .wordRtl {
+ unicode-bidi: bidi-override;
+ }
}
&.withLigatures {
.word {
@@ -789,6 +793,10 @@
&.rightToLeftTest {
//flex-direction: row-reverse; // no need for hacking 😉, CSS fully support right-to-left languages
direction: rtl;
+
+ .wordRtl {
+ unicode-bidi: bidi-override;
+ }
}
&.withLigatures {
.word {
diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts
index 68ed146e49a4..3b0da94b42e1 100644
--- a/frontend/src/ts/commandline/commandline-metadata.ts
+++ b/frontend/src/ts/commandline/commandline-metadata.ts
@@ -391,6 +391,7 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = {
"13": "wholetone",
"14": "fist fight",
"15": "rubber keys",
+ "16": "fart",
};
return map[value];
},
diff --git a/frontend/src/ts/commandline/commandline.ts b/frontend/src/ts/commandline/commandline.ts
index 0a5acca873bb..d5c640d3dd67 100644
--- a/frontend/src/ts/commandline/commandline.ts
+++ b/frontend/src/ts/commandline/commandline.ts
@@ -20,6 +20,7 @@ import {
} from "../elements/input-validation";
import { isInputElementFocused } from "../input/input-element";
import { qs } from "../utils/dom";
+import { ConfigKey } from "@monkeytype/schemas/configs";
type CommandlineMode = "search" | "input";
type InputModeParams = {
@@ -70,7 +71,10 @@ function addCommandlineBackground(): void {
}
type ShowSettings = {
- subgroupOverride?: CommandsSubgroup | string;
+ subgroupOverride?:
+ | CommandsSubgroup
+ | CommandlineLists.ListsObjectKeys
+ | ConfigKey;
commandOverride?: string;
singleListOverride?: boolean;
};
@@ -102,7 +106,7 @@ export function show(
if (exists) {
Loader.show();
subgroupOverride = await CommandlineLists.getList(
- settings.subgroupOverride as CommandlineLists.ListsObjectKeys,
+ settings.subgroupOverride,
);
Loader.hide();
} else {
diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts
index 09894ad90fe4..cefee0c7bf60 100644
--- a/frontend/src/ts/commandline/lists.ts
+++ b/frontend/src/ts/commandline/lists.ts
@@ -32,6 +32,7 @@ import { Command, CommandsSubgroup } from "./types";
import { buildCommandForConfigKey } from "./util";
import { CommandlineConfigMetadataObject } from "./commandline-metadata";
import { isAuthAvailable, isAuthenticated, signOut } from "../firebase";
+import { ConfigKey } from "@monkeytype/schemas/configs";
const challengesPromise = JSONData.getChallengeList();
challengesPromise
@@ -44,23 +45,6 @@ challengesPromise
);
});
-const languageCommand = buildCommandForConfigKey("language");
-const difficultyCommand = buildCommandForConfigKey("difficulty");
-const blindModeCommand = buildCommandForConfigKey("blindMode");
-const oppositeShiftModeCommand = buildCommandForConfigKey("oppositeShiftMode");
-const stopOnErrorCommand = buildCommandForConfigKey("stopOnError");
-const confidenceModeCommand = buildCommandForConfigKey("confidenceMode");
-const lazyModeCommand = buildCommandForConfigKey("lazyMode");
-const layoutCommand = buildCommandForConfigKey("layout");
-const showAverageCommand = buildCommandForConfigKey("showAverage");
-const showPbCommand = buildCommandForConfigKey("showPb");
-const keymapLayoutCommand = buildCommandForConfigKey("keymapLayout");
-const customThemeCommand = buildCommandForConfigKey("customTheme");
-const adsCommand = buildCommandForConfigKey("ads");
-const minSpeedCommand = buildCommandForConfigKey("minWpm");
-const minAccCommand = buildCommandForConfigKey("minAcc");
-const paceCaretCommand = buildCommandForConfigKey("paceCaret");
-
export const commands: CommandsSubgroup = {
title: "",
list: [
@@ -68,13 +52,15 @@ export const commands: CommandsSubgroup = {
...ResultScreenCommands,
//test screen
- buildCommandForConfigKey("punctuation"),
- buildCommandForConfigKey("numbers"),
- buildCommandForConfigKey("mode"),
- buildCommandForConfigKey("time"),
- buildCommandForConfigKey("words"),
- buildCommandForConfigKey("quoteLength"),
- languageCommand,
+ ...buildCommands(
+ "punctuation",
+ "numbers",
+ "mode",
+ "time",
+ "words",
+ "quoteLength",
+ "language",
+ ),
{
id: "changeCustomModeText",
display: "Change custom text",
@@ -111,14 +97,14 @@ export const commands: CommandsSubgroup = {
//behavior
...buildCommands(
- difficultyCommand,
+ "difficulty",
"quickRestart",
"repeatQuotes",
- blindModeCommand,
+ "blindMode",
"alwaysShowWordsHistory",
"singleListCommandLine",
- minSpeedCommand,
- minAccCommand,
+ "minWpm",
+ "minAcc",
...MinBurstCommands,
"britishEnglish",
...FunboxCommands,
@@ -130,15 +116,15 @@ export const commands: CommandsSubgroup = {
...buildCommands(
"freedomMode",
"strictSpace",
- oppositeShiftModeCommand,
- stopOnErrorCommand,
- confidenceModeCommand,
+ "oppositeShiftMode",
+ "stopOnError",
+ "confidenceMode",
"quickEnd",
"indicateTypos",
"compositionDisplay",
"hideExtraLetters",
- lazyModeCommand,
- layoutCommand,
+ "lazyMode",
+ "layout",
"codeUnindentOnBackspace",
),
@@ -154,7 +140,7 @@ export const commands: CommandsSubgroup = {
...buildCommands(
"smoothCaret",
"caretStyle",
- paceCaretCommand,
+ "paceCaret",
"repeatedPace",
"paceCaretStyle",
),
@@ -184,14 +170,14 @@ export const commands: CommandsSubgroup = {
"keymapStyle",
"keymapLegendStyle",
"keymapSize",
- keymapLayoutCommand,
+ "keymapLayout",
"keymapShowTopRow",
),
//theme
...buildCommands(
...ThemesCommands,
- customThemeCommand,
+ "customTheme",
...CustomThemesListCommands,
"flipTestColors",
@@ -218,14 +204,14 @@ export const commands: CommandsSubgroup = {
"showKeyTips",
"showOutOfFocusWarning",
"capsLockWarning",
- showAverageCommand,
- showPbCommand,
+ "showAverage",
+ "showPb",
"monkeyPowerLevel",
"monkey",
),
//danger zone
- adsCommand,
+ ...buildCommands("ads"),
//other
...LoadChallengeCommands,
@@ -382,40 +368,39 @@ export const commands: CommandsSubgroup = {
};
const lists = {
- keymapLayouts: keymapLayoutCommand.subgroup,
- enableAds: adsCommand.subgroup,
- customThemesList: customThemeCommand.subgroup,
themes: ThemesCommands[0]?.subgroup,
loadChallenge: LoadChallengeCommands[0]?.subgroup,
- languages: languageCommand.subgroup,
- difficulty: difficultyCommand.subgroup,
- lazyMode: lazyModeCommand.subgroup,
- paceCaretMode: paceCaretCommand.subgroup,
- showAverage: showAverageCommand.subgroup,
- showPb: showPbCommand.subgroup,
- minWpm: minSpeedCommand.subgroup,
- minAcc: minAccCommand.subgroup,
minBurst: MinBurstCommands[0]?.subgroup,
funbox: FunboxCommands[0]?.subgroup,
- confidenceMode: confidenceModeCommand.subgroup,
- stopOnError: stopOnErrorCommand.subgroup,
- layouts: layoutCommand.subgroup,
- oppositeShiftMode: oppositeShiftModeCommand.subgroup,
tags: TagsCommands[0]?.subgroup,
resultSaving: ResultSavingCommands[0]?.subgroup,
- blindMode: blindModeCommand.subgroup,
};
+const subgroupByConfigKey = Object.fromEntries(
+ commands.list
+ .filter((it) => it.subgroup?.configKey !== undefined)
+ .map((it) => [it.subgroup?.configKey, it.subgroup]),
+) as Record;
+
export function doesListExist(listName: string): boolean {
+ if (subgroupByConfigKey[listName] !== undefined) {
+ return true;
+ }
+
return lists[listName as ListsObjectKeys] !== undefined;
}
export async function getList(
- listName: ListsObjectKeys,
+ listName: ListsObjectKeys | ConfigKey,
): Promise {
await Promise.allSettled([challengesPromise]);
- const list = lists[listName];
+ const subGroup = subgroupByConfigKey[listName];
+ if (subGroup !== undefined) {
+ return subGroup;
+ }
+
+ const list = lists[listName as ListsObjectKeys];
if (!list) {
Notifications.add(`List not found: ${listName}`, -1);
throw new Error(`List ${listName} not found`);
diff --git a/frontend/src/ts/controllers/sound-controller.ts b/frontend/src/ts/controllers/sound-controller.ts
index 07b9befa6e30..e2fee06bf71d 100644
--- a/frontend/src/ts/controllers/sound-controller.ts
+++ b/frontend/src/ts/controllers/sound-controller.ts
@@ -35,6 +35,8 @@ let clickSounds: ClickSounds | null = null;
let timeWarning: Howl | null = null;
+let fartReverb: Howl | null = null;
+
async function initTimeWarning(): Promise {
const Howl = (await gethowler()).Howl;
if (timeWarning !== null) return;
@@ -43,6 +45,14 @@ async function initTimeWarning(): Promise {
});
}
+async function initFartReverb(): Promise {
+ const Howl = (await gethowler()).Howl;
+ if (fartReverb !== null) return;
+ fartReverb = new Howl({
+ src: "../sound/fart-reverb.wav",
+ });
+}
+
async function initErrorSound(): Promise {
const Howl = (await gethowler()).Howl;
if (errorSounds !== null) return;
@@ -396,6 +406,85 @@ async function init(): Promise {
counter: 0,
},
],
+ 16: [
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_1.wav" }),
+ new Howl({ src: "../sound/click16/click16_1.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_2.wav" }),
+ new Howl({ src: "../sound/click16/click16_2.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_3.wav" }),
+ new Howl({ src: "../sound/click16/click16_3.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_4.wav" }),
+ new Howl({ src: "../sound/click16/click16_4.wav" }),
+ ],
+ counter: 0,
+ },
+ // {
+ // sounds: [
+ // new Howl({ src: "../sound/click16/click16_5.wav" }),
+ // new Howl({ src: "../sound/click16/click16_5.wav" }),
+ // ],
+ // counter: 0,
+ // },
+ // {
+ // sounds: [
+ // new Howl({ src: "../sound/click16/click16_6.wav" }),
+ // new Howl({ src: "../sound/click16/click16_6.wav" }),
+ // ],
+ // counter: 0,
+ // },
+ // {
+ // sounds: [
+ // new Howl({ src: "../sound/click16/click16_7.wav" }),
+ // new Howl({ src: "../sound/click16/click16_7.wav" }),
+ // ],
+ // counter: 0,
+ // },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_8.wav" }),
+ new Howl({ src: "../sound/click16/click16_8.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_9.wav" }),
+ new Howl({ src: "../sound/click16/click16_9.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_10.wav" }),
+ new Howl({ src: "../sound/click16/click16_10.wav" }),
+ ],
+ counter: 0,
+ },
+ {
+ sounds: [
+ new Howl({ src: "../sound/click16/click16_11.wav" }),
+ new Howl({ src: "../sound/click16/click16_11.wav" }),
+ ],
+ counter: 0,
+ },
+ ],
};
Howler.volume(Config.soundVolume);
}
@@ -638,6 +727,14 @@ export async function playTimeWarning(): Promise {
soundToPlay.play();
}
+export async function playFartReverb(): Promise {
+ if (fartReverb === null) await initFartReverb();
+ const soundToPlay = fartReverb as Howl;
+ soundToPlay.stop();
+ soundToPlay.seek(0);
+ soundToPlay.play();
+}
+
export async function clearAllSounds(): Promise {
const Howl = (await gethowler()).Howler;
Howl.stop();
diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts
index e2091b9c47b3..cac27c4e3696 100644
--- a/frontend/src/ts/elements/modes-notice.ts
+++ b/frontend/src/ts/elements/modes-notice.ts
@@ -106,7 +106,7 @@ export async function update(): Promise {
if (Config.mode !== "zen" && !usingPolyglot) {
testModesNotice.appendHtml(
- ` ${getLanguageDisplayString(
+ ` ${getLanguageDisplayString(
Config.language,
Config.mode === "quote",
)} `,
@@ -158,7 +158,7 @@ export async function update(): Promise {
});
testModesNotice.appendHtml(
- ` ${
+ ` ${
Config.paceCaret === "average"
? "average"
: Config.paceCaret === "pb"
@@ -278,7 +278,7 @@ export async function update(): Promise {
if (Config.layout !== "default") {
testModesNotice.appendHtml(
- ` emulating ${Config.layout.replace(
+ ` emulating ${Config.layout.replace(
/_/g,
" ",
)} `,
diff --git a/frontend/src/ts/event-handlers/footer.ts b/frontend/src/ts/event-handlers/footer.ts
index 8e288c970848..949cca104459 100644
--- a/frontend/src/ts/event-handlers/footer.ts
+++ b/frontend/src/ts/event-handlers/footer.ts
@@ -65,7 +65,7 @@ document
}
setConfig("customTheme", true);
} else {
- const subgroup = Config.customTheme ? "customThemesList" : "themes";
+ const subgroup = Config.customTheme ? "customTheme" : "themes";
Commandline.show({
subgroupOverride: subgroup,
});
diff --git a/frontend/src/ts/event-handlers/keymap.ts b/frontend/src/ts/event-handlers/keymap.ts
index 807ea0552a45..90eb7e8f46ff 100644
--- a/frontend/src/ts/event-handlers/keymap.ts
+++ b/frontend/src/ts/event-handlers/keymap.ts
@@ -2,6 +2,6 @@ import * as Commandline from "../commandline/commandline";
$("#keymap").on("click", ".r5 .layoutIndicator", async () => {
Commandline.show({
- subgroupOverride: "keymapLayouts",
+ subgroupOverride: "keymapLayout",
});
});
diff --git a/frontend/src/ts/event-handlers/test.ts b/frontend/src/ts/event-handlers/test.ts
index fa839c1b6c4f..072c212cd3dd 100644
--- a/frontend/src/ts/event-handlers/test.ts
+++ b/frontend/src/ts/event-handlers/test.ts
@@ -15,11 +15,13 @@ import * as PractiseWordsModal from "../modals/practise-words";
import { navigate } from "../controllers/route-controller";
import { getMode2 } from "../utils/misc";
import * as ShareTestSettingsPopup from "../modals/share-test-settings";
+import { ConfigKey } from "@monkeytype/schemas/configs";
+import { ListsObjectKeys } from "../commandline/lists";
$(".pageTest").on("click", "#testModesNotice .textButton", async (event) => {
const attr = $(event.currentTarget).attr("commands");
if (attr === undefined) return;
- Commandline.show({ subgroupOverride: attr });
+ Commandline.show({ subgroupOverride: attr as ConfigKey | ListsObjectKeys });
});
$(".pageTest").on("click", "#testModesNotice .textButton", async (event) => {
diff --git a/frontend/src/ts/modals/streak-hour-offset.ts b/frontend/src/ts/modals/streak-hour-offset.ts
index 5b1dbc36a654..68b0f3e3257b 100644
--- a/frontend/src/ts/modals/streak-hour-offset.ts
+++ b/frontend/src/ts/modals/streak-hour-offset.ts
@@ -8,6 +8,10 @@ import { getSnapshot, setSnapshot } from "../db";
import AnimatedModal from "../utils/animated-modal";
import { Snapshot } from "../constants/default-snapshot";
+let state = {
+ offset: 0,
+};
+
export function show(): void {
if (!ConnectionState.get()) {
Notifications.add("You are offline", 0, {
@@ -17,17 +21,22 @@ export function show(): void {
}
void modal.show({
- focusFirstInput: true,
+ focusFirstInput: "focusAndSelect",
beforeAnimation: async (modalEl) => {
if (getSnapshot()?.streakHourOffset !== undefined) {
modalEl.qs("input")?.remove();
modalEl.qs(".preview")?.remove();
- modalEl.qs("button")?.remove();
+ modalEl.qsa("button")?.remove();
modalEl
.qs(".text")
- ?.setText("You have already set your streak hour offset.");
+ ?.setText(
+ `You have already set your streak hour offset to ${
+ getSnapshot()?.streakHourOffset ?? "?"
+ }. You can only set your streak hour offset once.`,
+ );
} else {
- modalEl.qs("input")?.setValue("0");
+ state.offset = 0;
+ updateDisplay();
updatePreview();
}
},
@@ -35,18 +44,12 @@ export function show(): void {
}
function updatePreview(): void {
- const inputValue = parseInt(
- modal.getModal().qs("input")?.getValue() ?? "",
- 10,
- );
+ const inputValue = state.offset;
const preview = modal.getModal().qs(".preview");
const date = new Date();
- date.setUTCHours(0);
- date.setUTCMinutes(0);
- date.setUTCSeconds(0);
- date.setUTCMilliseconds(0);
+ date.setUTCHours(0, 0, 0, 0);
const newDate = new Date();
newDate.setUTCHours(0);
@@ -62,23 +65,30 @@ function updatePreview(): void {
`);
}
+function updateDisplay(): void {
+ modal
+ .getModal()
+ .qs("input")
+ ?.setValue(state.offset.toFixed(1));
+}
+
function hide(): void {
void modal.hide();
}
async function apply(): Promise {
- const value = parseInt(
- modal.getModal().qs("input")?.getValue() ?? "",
- 10,
- );
+ const value = state.offset;
if (isNaN(value)) {
Notifications.add("Streak hour offset must be a number", 0);
return;
}
- if (value < -11 || value > 12) {
- Notifications.add("Streak hour offset must be between -11 and 12", 0);
+ if (value < -11 || value > 12 || (value % 1 !== 0 && value % 1 !== 0.5)) {
+ Notifications.add(
+ "Streak offset must be between -11 and 12. Times ending in .5 can be used for 30-minute increments.",
+ 0,
+ );
return;
}
@@ -88,25 +98,61 @@ async function apply(): Promise {
body: { hourOffset: value },
});
Loader.hide();
+
if (response.status !== 200) {
Notifications.add("Failed to set streak hour offset", -1, { response });
} else {
Notifications.add("Streak hour offset set", 1);
const snap = getSnapshot() as Snapshot;
+
snap.streakHourOffset = value;
setSnapshot(snap);
hide();
}
}
+function setStateToInput(): void {
+ const inputValue = parseFloat(
+ modal.getModal().qs("input")?.getValue() ?? "0",
+ );
+ if (!isNaN(inputValue)) {
+ state.offset = inputValue;
+ if (state.offset < -11) state.offset = -11;
+ if (state.offset > 12) state.offset = 12;
+ } else {
+ state.offset = 0;
+ }
+}
+
const modal = new AnimatedModal({
dialogId: "streakHourOffsetModal",
setup: async (modalEl): Promise => {
- modalEl.qs("input")?.on("input", () => {
+ modalEl.qs("input")?.on("focusout", () => {
+ setStateToInput();
+ updateDisplay();
updatePreview();
});
- modalEl.qs("button")?.on("click", () => {
+ modalEl.qs("input")?.on("keyup", (e) => {
+ if (e.key === "Enter") {
+ setStateToInput();
+ updateDisplay();
+ updatePreview();
+ }
+ });
+ modalEl.qs(".submit")?.on("click", () => {
void apply();
});
+ modalEl.qs(".decreaseOffset")?.on("click", () => {
+ state.offset -= 0.5;
+ if (state.offset < -11) state.offset = -11;
+ updateDisplay();
+ updatePreview();
+ });
+ modalEl.qs(".increaseOffset")?.on("click", () => {
+ state.offset += 0.5;
+ if (state.offset > 12) state.offset = 12;
+ updateDisplay();
+ updatePreview();
+ });
},
});
diff --git a/frontend/src/ts/modals/support.ts b/frontend/src/ts/modals/support.ts
index bc25ca8593c9..19cc05285508 100644
--- a/frontend/src/ts/modals/support.ts
+++ b/frontend/src/ts/modals/support.ts
@@ -10,7 +10,7 @@ const modal = new AnimatedModal({
setup: async (modalEl): Promise => {
modalEl.qs("button.ads")?.on("click", async () => {
Commandline.show(
- { subgroupOverride: "enableAds" },
+ { subgroupOverride: "ads" },
{
modalChain: modal,
},
diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts
index 86dba54a5d7f..6dfa81b14e32 100644
--- a/frontend/src/ts/test/test-ui.ts
+++ b/frontend/src/ts/test/test-ui.ts
@@ -217,7 +217,7 @@ async function joinOverlappingHints(
activeWordLetters: NodeListOf,
hintElements: HTMLCollection,
): Promise {
- const isWordRightToLeft = Strings.isWordRightToLeft(
+ const [isWordRightToLeft, _isFullMatch] = Strings.isWordRightToLeft(
TestWords.words.getCurrent(),
TestState.isLanguageRightToLeft,
TestState.isDirectionReversed,
@@ -1904,6 +1904,9 @@ export function onTestFinish(): void {
TimerProgress.hide();
OutOfFocus.hide();
Monkey.hide();
+ if (Config.playSoundOnClick === "16") {
+ void SoundController.playFartReverb();
+ }
}
$(".pageTest #copyWordsListButton").on("click", async () => {
diff --git a/frontend/src/ts/utils/caret.ts b/frontend/src/ts/utils/caret.ts
index 47408db78917..11935474a789 100644
--- a/frontend/src/ts/utils/caret.ts
+++ b/frontend/src/ts/utils/caret.ts
@@ -405,7 +405,7 @@ export class Caret {
isLanguageRightToLeft: boolean;
isDirectionReversed: boolean;
}): { left: number; top: number; width: number } {
- const isWordRTL = isWordRightToLeft(
+ const [isWordRTL, isFullMatch] = isWordRightToLeft(
options.wordText,
options.isLanguageRightToLeft,
options.isDirectionReversed,
@@ -455,6 +455,7 @@ export class Caret {
// yes, this is all super verbose, but its easier to maintain and understand
if (isWordRTL) {
+ if (isFullMatch) options.word.addClass("wordRtl");
let afterLetterCorrection = 0;
if (options.side === "afterLetter") {
if (this.isFullWidth()) {
diff --git a/frontend/src/ts/utils/strings.ts b/frontend/src/ts/utils/strings.ts
index 993f49682be5..4ec735b670ef 100644
--- a/frontend/src/ts/utils/strings.ts
+++ b/frontend/src/ts/utils/strings.ts
@@ -214,23 +214,24 @@ export function replaceControlCharacters(textToClear: string): string {
* @param word the word to check for RTL characters
* @returns true if the word contains RTL characters, false otherwise
*/
-function hasRTLCharacters(word: string): boolean {
+function hasRTLCharacters(word: string): [boolean, number] {
if (!word || word.length === 0) {
- return false;
+ return [false, 0];
}
// This covers Arabic, Farsi, Urdu, and other RTL scripts
const rtlPattern =
- /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/;
+ /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]+/;
- return rtlPattern.test(word);
+ const result = rtlPattern.exec(word);
+ return [result !== null, result?.[0].length ?? 0];
}
/**
* Cache for word direction to avoid repeated calculations per word
* Keyed by the stripped core of the word; can be manually cleared when needed
*/
-let wordDirectionCache: Map = new Map();
+let wordDirectionCache: Map = new Map();
export function clearWordDirectionCache(): void {
wordDirectionCache.clear();
@@ -240,25 +241,33 @@ export function isWordRightToLeft(
word: string | undefined,
languageRTL: boolean,
reverseDirection?: boolean,
-): boolean {
+): [boolean, boolean] {
if (word === undefined || word.length === 0) {
- return reverseDirection ? !languageRTL : languageRTL;
+ return reverseDirection ? [!languageRTL, false] : [languageRTL, false];
}
// Strip leading/trailing punctuation and whitespace so attached opposite-direction
// punctuation like "word؟" or "،word" doesn't flip the direction detection
// and if only punctuation/symbols/whitespace, use main language direction
const core = word.replace(/^[\p{P}\p{S}\s]+|[\p{P}\p{S}\s]+$/gu, "");
- if (core.length === 0) return reverseDirection ? !languageRTL : languageRTL;
+ if (core.length === 0) {
+ return reverseDirection ? [!languageRTL, false] : [languageRTL, false];
+ }
// cache by core to handle variants like "word" vs "word؟"
const cached = wordDirectionCache.get(core);
- if (cached !== undefined) return reverseDirection ? !cached : cached;
+ if (cached !== undefined) {
+ return reverseDirection
+ ? [!cached[0], false]
+ : [cached[0], cached[1] === word.length];
+ }
const result = hasRTLCharacters(core);
wordDirectionCache.set(core, result);
- return reverseDirection ? !result : result;
+ return reverseDirection
+ ? [!result[0], false]
+ : [result[0], result[1] === word.length];
}
export const CHAR_EQUIVALENCE_SETS = [
diff --git a/frontend/static/sound/click16/click16_1.wav b/frontend/static/sound/click16/click16_1.wav
new file mode 100644
index 000000000000..7acf355c3ce6
Binary files /dev/null and b/frontend/static/sound/click16/click16_1.wav differ
diff --git a/frontend/static/sound/click16/click16_10.wav b/frontend/static/sound/click16/click16_10.wav
new file mode 100644
index 000000000000..3d3c1e496afc
Binary files /dev/null and b/frontend/static/sound/click16/click16_10.wav differ
diff --git a/frontend/static/sound/click16/click16_11.wav b/frontend/static/sound/click16/click16_11.wav
new file mode 100644
index 000000000000..95ce4f84c6dd
Binary files /dev/null and b/frontend/static/sound/click16/click16_11.wav differ
diff --git a/frontend/static/sound/click16/click16_2.wav b/frontend/static/sound/click16/click16_2.wav
new file mode 100644
index 000000000000..e93765a20e7f
Binary files /dev/null and b/frontend/static/sound/click16/click16_2.wav differ
diff --git a/frontend/static/sound/click16/click16_3.wav b/frontend/static/sound/click16/click16_3.wav
new file mode 100644
index 000000000000..a39720fbd1d8
Binary files /dev/null and b/frontend/static/sound/click16/click16_3.wav differ
diff --git a/frontend/static/sound/click16/click16_4.wav b/frontend/static/sound/click16/click16_4.wav
new file mode 100644
index 000000000000..b5cfed02fc0d
Binary files /dev/null and b/frontend/static/sound/click16/click16_4.wav differ
diff --git a/frontend/static/sound/click16/click16_5.wav b/frontend/static/sound/click16/click16_5.wav
new file mode 100644
index 000000000000..171b1d7993b4
Binary files /dev/null and b/frontend/static/sound/click16/click16_5.wav differ
diff --git a/frontend/static/sound/click16/click16_6.wav b/frontend/static/sound/click16/click16_6.wav
new file mode 100644
index 000000000000..62eeb100b807
Binary files /dev/null and b/frontend/static/sound/click16/click16_6.wav differ
diff --git a/frontend/static/sound/click16/click16_7.wav b/frontend/static/sound/click16/click16_7.wav
new file mode 100644
index 000000000000..9ee853933e34
Binary files /dev/null and b/frontend/static/sound/click16/click16_7.wav differ
diff --git a/frontend/static/sound/click16/click16_8.wav b/frontend/static/sound/click16/click16_8.wav
new file mode 100644
index 000000000000..e3cbc08a9623
Binary files /dev/null and b/frontend/static/sound/click16/click16_8.wav differ
diff --git a/frontend/static/sound/click16/click16_9.wav b/frontend/static/sound/click16/click16_9.wav
new file mode 100644
index 000000000000..db7637ee5062
Binary files /dev/null and b/frontend/static/sound/click16/click16_9.wav differ
diff --git a/frontend/static/sound/fart-reverb.wav b/frontend/static/sound/fart-reverb.wav
new file mode 100644
index 000000000000..c3288f449242
Binary files /dev/null and b/frontend/static/sound/fart-reverb.wav differ
diff --git a/packages/schemas/src/configs.ts b/packages/schemas/src/configs.ts
index edfd6af29355..21e374ebbe72 100644
--- a/packages/schemas/src/configs.ts
+++ b/packages/schemas/src/configs.ts
@@ -143,6 +143,7 @@ export const PlaySoundOnClickSchema = z.enum([
"13",
"14",
"15",
+ "16",
]);
export type PlaySoundOnClick = z.infer;
diff --git a/packages/schemas/src/users.ts b/packages/schemas/src/users.ts
index ad6041bf898c..9247f7090f50 100644
--- a/packages/schemas/src/users.ts
+++ b/packages/schemas/src/users.ts
@@ -60,7 +60,7 @@ export const ResultFiltersSchema = z.object({
});
export type ResultFilters = z.infer;
-export const StreakHourOffsetSchema = z.number().int().min(-11).max(12);
+export const StreakHourOffsetSchema = z.number().min(-11).max(12).step(0.5);
export type StreakHourOffset = z.infer;
export const UserStreakSchema = z
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index a48758b1f11e..ad23c2f832de 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -6252,6 +6252,7 @@ packages:
keygrip@1.1.0:
resolution: {integrity: sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==}
engines: {node: '>= 0.6'}
+ deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
keyv@4.5.4:
resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}