diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts index c564cd938..87303ef41 100644 --- a/backend/src/api/controllers/result.ts +++ b/backend/src/api/controllers/result.ts @@ -1,38 +1,16 @@ import * as ResultDAL from "../../dal/result"; -import * as PublicDAL from "../../dal/public"; import { isDevEnvironment, - omit, replaceObjectId, replaceObjectIds, } from "../../utils/misc"; -import objectHash from "object-hash"; import Logger from "../../utils/logger"; import "dotenv/config"; import { MonkeyResponse } from "../../utils/monkey-response"; import MonkeyError from "../../utils/error"; -import { isTestTooShort } from "../../utils/validation"; -import { - implemented as anticheatImplemented, - validateResult, - validateKeys, -} from "../../anticheat/index"; -import MonkeyStatusCodes from "../../constants/monkey-status-codes"; -import { - incrementResult, - incrementDailyLeaderboard, -} from "../../utils/prometheus"; -import GeorgeQueue from "../../queues/george-queue"; -import { getDailyLeaderboard } from "../../utils/daily-leaderboards"; -import AutoRoleList from "../../constants/auto-roles"; +import { implemented as anticheatImplemented } from "../../anticheat/index"; import * as UserDAL from "../../dal/user"; -import { buildMonkeyMail } from "../../utils/monkey-mail"; -import * as WeeklyXpLeaderboard from "../../services/weekly-xp-leaderboard"; -import { UAParser } from "ua-parser-js"; -import { canFunboxGetPb } from "../../utils/pb"; -import { buildDbResult } from "../../utils/result"; -import { Configuration } from "@monkeytype/schemas/configuration"; -import { addImportantLog, addLog } from "../../dal/logs"; +import { addLog } from "../../dal/logs"; import { AddResultRequest, AddResultResponse, @@ -44,26 +22,7 @@ import { UpdateResultTagsRequest, UpdateResultTagsResponse, } from "@monkeytype/contracts/results"; -import { - CompletedEvent, - KeyStats, - PostResultResponse, - XpBreakdown, -} from "@monkeytype/schemas/results"; -import { - isSafeNumber, - mapRange, - roundTo2, - stdDev, -} from "@monkeytype/util/numbers"; -import { - getCurrentDayTimestamp, - getStartOfDayTimestamp, -} from "@monkeytype/util/date-and-time"; import { MonkeyRequest } from "../types"; -import { getFunbox, checkCompatibility } from "@monkeytype/funbox"; -import { tryCatch } from "@monkeytype/util/trycatch"; -import { getCachedConfiguration } from "../../init/configuration"; try { if (!anticheatImplemented()) throw new Error("undefined"); @@ -197,649 +156,16 @@ export async function updateTags( } export async function addResult( - req: MonkeyRequest + _req: MonkeyRequest ): Promise { // todo remove - return new MonkeyResponse("Result added"); - const { uid } = req.ctx.decodedToken; - - const user = await UserDAL.getUser(uid, "add result"); - - if (user.needsToChangeName) { - throw new MonkeyError( - 403, - "Please change your name before submitting a result" - ); - } - - const completedEvent = req.body.result; - completedEvent.uid = uid; - - if (isTestTooShort(completedEvent)) { - const status = MonkeyStatusCodes.TEST_TOO_SHORT; - throw new MonkeyError(status.code, status.message); - } - - if (user.lbOptOut !== true && completedEvent.acc < 75) { - throw new MonkeyError(400, "Accuracy too low"); - } - - const resulthash = completedEvent.hash; - if (req.ctx.configuration.results.objectHashCheckEnabled) { - const objectToHash = omit(completedEvent, ["hash"]); - const serverhash = objectHash(objectToHash); - if (serverhash !== resulthash) { - void addLog( - "incorrect_result_hash", - { - serverhash, - resulthash, - result: completedEvent, - }, - uid - ); - const status = MonkeyStatusCodes.RESULT_HASH_INVALID; - throw new MonkeyError(status.code, "Incorrect result hash"); - } - } else { - Logger.warning("Object hash check is disabled, skipping hash check"); - } - - if (completedEvent.funbox.length !== new Set(completedEvent.funbox).size) { - throw new MonkeyError(400, "Duplicate funboxes"); - } - - if (!checkCompatibility(completedEvent.funbox)) { - throw new MonkeyError(400, "Impossible funbox combination"); - } - - let keySpacingStats: KeyStats | undefined = undefined; - if ( - completedEvent.keySpacing !== "toolong" && - completedEvent.keySpacing.length > 0 - ) { - keySpacingStats = { - average: - completedEvent.keySpacing.reduce( - (previous, current) => (current += previous) - ) / completedEvent.keySpacing.length, - sd: stdDev(completedEvent.keySpacing), - }; - } - - let keyDurationStats: KeyStats | undefined = undefined; - if ( - completedEvent.keyDuration !== "toolong" && - completedEvent.keyDuration.length > 0 - ) { - keyDurationStats = { - average: - completedEvent.keyDuration.reduce( - (previous, current) => (current += previous) - ) / completedEvent.keyDuration.length, - sd: stdDev(completedEvent.keyDuration), - }; - } - - if (user.suspicious && completedEvent.testDuration <= 120) { - await addImportantLog("suspicious_user_result", completedEvent, uid); - } - - if ( - completedEvent.mode === "time" && - (completedEvent.mode2 === "60" || completedEvent.mode2 === "15") && - completedEvent.wpm > 250 && - user.lbOptOut !== true - ) { - await addImportantLog("highwpm_user_result", completedEvent, uid); - } - - if (anticheatImplemented()) { - if ( - !validateResult( - completedEvent, - ((req.raw.headers["x-client-version"] as string) || - req.raw.headers["client-version"]) as string, - JSON.stringify(new UAParser(req.raw.headers["user-agent"]).getResult()), - user.lbOptOut === true - ) - ) { - const status = MonkeyStatusCodes.RESULT_DATA_INVALID; - throw new MonkeyError(status.code, "Result data doesn't make sense"); - } else if (isDevEnvironment()) { - Logger.success("Result data validated"); - } - } else { - if (!isDevEnvironment()) { - throw new Error("No anticheat module found"); - } - Logger.warning( - "No anticheat module found. Continuing in dev mode, results will not be validated." - ); - } - - //dont use - result timestamp is unreliable, can be changed by system time and stuff - // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) { - // log( - // "time_traveler", - // { - // resultTimestamp: result.timestamp, - // serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10, - // }, - // uid - // ); - // return res.status(400).json({ message: "Time traveler detected" }); - - const { data: lastResultTimestamp } = await tryCatch( - ResultDAL.getLastResultTimestamp(uid) - ); - - //convert result test duration to miliseconds - completedEvent.timestamp = Math.floor(Date.now() / 1000) * 1000; - - //check if now is earlier than last result plus duration (-1 second as a buffer) - const testDurationMilis = completedEvent.testDuration * 1000; - const incompleteTestsMilis = completedEvent.incompleteTestSeconds * 1000; - const earliestPossible = - (lastResultTimestamp ?? 0) + testDurationMilis + incompleteTestsMilis; - const nowNoMilis = Math.floor(Date.now() / 1000) * 1000; - if ( - isSafeNumber(lastResultTimestamp) && - nowNoMilis < earliestPossible - 1000 - ) { - void addLog( - "invalid_result_spacing", - { - lastTimestamp: lastResultTimestamp, - earliestPossible, - now: nowNoMilis, - testDuration: testDurationMilis, - difference: nowNoMilis - earliestPossible, - }, - uid - ); - const status = MonkeyStatusCodes.RESULT_SPACING_INVALID; - throw new MonkeyError(status.code, "Invalid result spacing"); - } - - //check keyspacing and duration here for bots - if ( - completedEvent.mode === "time" && - completedEvent.wpm > 130 && - completedEvent.testDuration < 122 && - (user.verified === false || user.verified === undefined) && - user.lbOptOut !== true - ) { - if (!keySpacingStats || !keyDurationStats) { - const status = MonkeyStatusCodes.MISSING_KEY_DATA; - throw new MonkeyError(status.code, "Missing key data"); - } - if (completedEvent.keyOverlap === undefined) { - throw new MonkeyError(400, "Old key data format"); - } - if (anticheatImplemented()) { - if ( - !validateKeys(completedEvent, keySpacingStats, keyDurationStats, uid) - ) { - //autoban - const autoBanConfig = req.ctx.configuration.users.autoBan; - if (autoBanConfig.enabled) { - const didUserGetBanned = await UserDAL.recordAutoBanEvent( - uid, - autoBanConfig.maxCount, - autoBanConfig.maxHours - ); - if (didUserGetBanned) { - const mail = buildMonkeyMail({ - subject: "Banned", - body: "Your account has been automatically banned for triggering the anticheat system. If you believe this is a mistake, please contact support.", - }); - await UserDAL.addToInbox( - uid, - [mail], - req.ctx.configuration.users.inbox - ); - user.banned = true; - } - } - const status = MonkeyStatusCodes.BOT_DETECTED; - throw new MonkeyError(status.code, "Possible bot detected"); - } - } else { - if (!isDevEnvironment()) { - throw new Error("No anticheat module found"); - } - Logger.warning( - "No anticheat module found. Continuing in dev mode, results will not be validated." - ); - } - } - - if (req.ctx.configuration.users.lastHashesCheck.enabled) { - let lastHashes = user.lastReultHashes ?? []; - if (lastHashes.includes(resulthash)) { - void addLog( - "duplicate_result", - { - lastHashes, - resulthash, - result: completedEvent, - }, - uid - ); - const status = MonkeyStatusCodes.DUPLICATE_RESULT; - throw new MonkeyError(status.code, "Duplicate result"); - } else { - lastHashes.unshift(resulthash); - const maxHashes = req.ctx.configuration.users.lastHashesCheck.maxHashes; - if (lastHashes.length > maxHashes) { - lastHashes = lastHashes.slice(0, maxHashes); - } - await UserDAL.updateLastHashes(uid, lastHashes); - } - } - - if (keyDurationStats) { - keyDurationStats.average = roundTo2(keyDurationStats.average); - keyDurationStats.sd = roundTo2(keyDurationStats.sd); - } - if (keySpacingStats) { - keySpacingStats.average = roundTo2(keySpacingStats.average); - keySpacingStats.sd = roundTo2(keySpacingStats.sd); - } - - let isPb = false; - let tagPbs: string[] = []; - - if (!completedEvent.bailedOut) { - [isPb, tagPbs] = await Promise.all([ - UserDAL.checkIfPb(uid, user, completedEvent), - UserDAL.checkIfTagPb(uid, user, completedEvent), - ]); - } - - if (completedEvent.mode === "time" && completedEvent.mode2 === "60") { - void UserDAL.incrementBananas(uid, completedEvent.wpm); - if ( - isPb && - user.discordId !== undefined && - user.discordId !== "" && - user.lbOptOut !== true - ) { - void GeorgeQueue.updateDiscordRole(user.discordId, completedEvent.wpm); - } - } - - if ( - completedEvent.challenge !== null && - completedEvent.challenge !== undefined && - AutoRoleList.includes(completedEvent.challenge) && - user.discordId !== undefined && - user.discordId !== "" - ) { - void GeorgeQueue.awardChallenge(user.discordId, completedEvent.challenge); - } else { - delete completedEvent.challenge; - } - - const afk = completedEvent.afkDuration ?? 0; - const totalDurationTypedSeconds = - completedEvent.testDuration + completedEvent.incompleteTestSeconds - afk; - void UserDAL.updateTypingStats( - uid, - completedEvent.restartCount, - totalDurationTypedSeconds - ); - void PublicDAL.updateStats( - completedEvent.restartCount, - totalDurationTypedSeconds - ); - - const dailyLeaderboardsConfig = req.ctx.configuration.dailyLeaderboards; - const dailyLeaderboard = getDailyLeaderboard( - completedEvent.language, - completedEvent.mode, - completedEvent.mode2, - dailyLeaderboardsConfig - ); - - let dailyLeaderboardRank = -1; - - const stopOnLetterTriggered = - completedEvent.stopOnLetter && completedEvent.acc < 100; - - const minTimeTyping = (await getCachedConfiguration(true)).leaderboards - .minTimeTyping; - - const userEligibleForLeaderboard = - user.banned !== true && - user.lbOptOut !== true && - (isDevEnvironment() || (user.timeTyping ?? 0) > minTimeTyping); - - const validResultCriteria = - canFunboxGetPb(completedEvent) && - !completedEvent.bailedOut && - userEligibleForLeaderboard && - !stopOnLetterTriggered; - - const selectedBadgeId = user.inventory?.badges?.find((b) => b.selected)?.id; - const isPremium = - (await UserDAL.checkIfUserIsPremium(user.uid, user)) || undefined; - - if (dailyLeaderboard && validResultCriteria) { - incrementDailyLeaderboard( - completedEvent.mode, - completedEvent.mode2, - completedEvent.language - ); - dailyLeaderboardRank = await dailyLeaderboard.addResult( - { - name: user.name, - wpm: completedEvent.wpm, - raw: completedEvent.rawWpm, - acc: completedEvent.acc, - consistency: completedEvent.consistency, - timestamp: completedEvent.timestamp, - uid, - discordAvatar: user.discordAvatar, - discordId: user.discordId, - badgeId: selectedBadgeId, - isPremium, - }, - dailyLeaderboardsConfig - ); - if ( - dailyLeaderboardRank >= 1 && - dailyLeaderboardRank <= 10 && - completedEvent.testDuration <= 120 - ) { - const now = Date.now(); - const reset = getCurrentDayTimestamp(); - const limit = 6 * 60 * 60 * 1000; - if (now - reset >= limit) { - await addLog("daily_leaderboard_top_10_result", completedEvent, uid); - } - } - } - - const streak = await UserDAL.updateStreak(uid, completedEvent.timestamp); - const badgeWaitingInInbox = ( - user.inbox?.flatMap((i) => - (i.rewards ?? []).map((r) => (r.type === "badge" ? r.item.id : null)) - ) ?? [] - ).includes(14); - - const shouldGetBadge = - streak >= 365 && - user.inventory?.badges?.find((b) => b.id === 14) === undefined && - !badgeWaitingInInbox; - - if (shouldGetBadge) { - const mail = buildMonkeyMail({ - subject: "Badge", - body: "Congratulations for reaching a 365 day streak! You have been awarded a special badge. Now, go touch some grass.", - rewards: [ - { - type: "badge", - item: { - id: 14, - }, - }, - ], - }); - await UserDAL.addToInbox(uid, [mail], req.ctx.configuration.users.inbox); - } - - const xpGained = await calculateXp( - completedEvent, - req.ctx.configuration.users.xp, - lastResultTimestamp, - user.xp ?? 0, - streak - ); - - if (xpGained.xp < 0) { - throw new MonkeyError( - 500, - "Calculated XP is negative", - JSON.stringify({ - xpGained, - result: completedEvent, - }), - uid - ); - } - - const weeklyXpLeaderboardConfig = req.ctx.configuration.leaderboards.weeklyXp; - let weeklyXpLeaderboardRank = -1; - - const weeklyXpLeaderboard = WeeklyXpLeaderboard.get( - weeklyXpLeaderboardConfig - ); - if (userEligibleForLeaderboard && xpGained.xp > 0 && weeklyXpLeaderboard) { - weeklyXpLeaderboardRank = await weeklyXpLeaderboard.addResult( - weeklyXpLeaderboardConfig, - { - entry: { - uid, - name: user.name, - discordAvatar: user.discordAvatar, - discordId: user.discordId, - badgeId: selectedBadgeId, - lastActivityTimestamp: Date.now(), - isPremium, - timeTypedSeconds: totalDurationTypedSeconds, - }, - xpGained: xpGained.xp, - } - ); - } - - const dbresult = buildDbResult(completedEvent, user.name, isPb); - if (keySpacingStats !== undefined) { - dbresult.keySpacingStats = keySpacingStats; - } - if (keyDurationStats !== undefined) { - dbresult.keyDurationStats = keyDurationStats; - } - - const addedResult = await ResultDAL.addResult(uid, dbresult); - - await UserDAL.incrementXp(uid, xpGained.xp); - await UserDAL.incrementTestActivity(user, completedEvent.timestamp); - - if (isPb) { - void addLog( - "user_new_pb", - `${completedEvent.mode + " " + completedEvent.mode2} ${ - completedEvent.wpm - } ${completedEvent.acc}% ${completedEvent.rawWpm} ${ - completedEvent.consistency - }% (${addedResult.insertedId})`, - uid - ); - } - - const data: PostResultResponse = { - isPb, - tagPbs, - insertedId: addedResult.insertedId.toHexString(), - xp: xpGained.xp, - dailyXpBonus: xpGained.dailyBonus ?? false, - xpBreakdown: xpGained.breakdown ?? {}, - streak, - }; - - if (dailyLeaderboardRank !== -1) { - data.dailyLeaderboardRank = dailyLeaderboardRank; - } - - if (weeklyXpLeaderboardRank !== -1) { - data.weeklyXpLeaderboardRank = weeklyXpLeaderboardRank; - } - - incrementResult(completedEvent, dbresult.isPb); - - return new MonkeyResponse("Result saved", data); -} - -type XpResult = { - xp: number; - dailyBonus?: boolean; - breakdown?: XpBreakdown; -}; - -async function calculateXp( - result: CompletedEvent, - xpConfiguration: Configuration["users"]["xp"], - lastResultTimestamp: number | null, - currentTotalXp: number, - streak: number -): Promise { - const { - mode, - acc, - testDuration, - incompleteTestSeconds, - incompleteTests, - afkDuration, - charStats, - punctuation, - numbers, - funbox: resultFunboxes, - } = result; - - const { - enabled, - gainMultiplier, - maxDailyBonus, - minDailyBonus, - funboxBonus: funboxBonusConfiguration, - } = xpConfiguration; - - if (mode === "zen" || !enabled) { - return { - xp: 0, - }; - } - - const breakdown: XpBreakdown = {}; - - const baseXp = Math.round((testDuration - afkDuration) * 2); - breakdown.base = baseXp; - - let modifier = 1; - - const correctedEverything = charStats - .slice(1) - .every((charStat: number) => charStat === 0); - - if (acc === 100) { - modifier += 0.5; - breakdown.fullAccuracy = Math.round(baseXp * 0.5); - } else if (correctedEverything) { - // corrected everything bonus - modifier += 0.25; - breakdown["corrected"] = Math.round(baseXp * 0.25); - } - - if (mode === "quote") { - // real sentences bonus - modifier += 0.5; - breakdown.quote = Math.round(baseXp * 0.5); - } else { - // punctuation bonus - if (punctuation) { - modifier += 0.4; - breakdown.punctuation = Math.round(baseXp * 0.4); - } - if (numbers) { - modifier += 0.1; - breakdown.numbers = Math.round(baseXp * 0.1); - } - } - - if (funboxBonusConfiguration > 0 && resultFunboxes.length !== 0) { - const funboxModifier = resultFunboxes.reduce((sum, funboxName) => { - const funbox = getFunbox(funboxName); - const difficultyLevel = funbox?.difficultyLevel ?? 0; - return sum + Math.max(difficultyLevel * funboxBonusConfiguration, 0); - }, 0); - - if (funboxModifier > 0) { - modifier += funboxModifier; - breakdown.funbox = Math.round(baseXp * funboxModifier); - } - } - - if (xpConfiguration.streak.enabled) { - const streakModifier = parseFloat( - mapRange( - streak, - 0, - xpConfiguration.streak.maxStreakDays, - 0, - xpConfiguration.streak.maxStreakMultiplier, - true - ).toFixed(1) - ); - - if (streakModifier > 0) { - modifier += streakModifier; - breakdown.streak = Math.round(baseXp * streakModifier); - } - } - - let incompleteXp = 0; - if (incompleteTests !== undefined && incompleteTests.length > 0) { - incompleteTests.forEach((it: { acc: number; seconds: number }) => { - let modifier = (it.acc - 50) / 50; - if (modifier < 0) modifier = 0; - incompleteXp += Math.round(it.seconds * modifier); - }); - breakdown.incomplete = incompleteXp; - } else if (incompleteTestSeconds && incompleteTestSeconds > 0) { - incompleteXp = Math.round(incompleteTestSeconds); - breakdown.incomplete = incompleteXp; - } - - const accuracyModifier = (acc - 50) / 50; - - let dailyBonus = 0; - if (isSafeNumber(lastResultTimestamp)) { - const lastResultDay = getStartOfDayTimestamp(lastResultTimestamp); - const today = getCurrentDayTimestamp(); - if (lastResultDay !== today) { - const proportionalXp = Math.round(currentTotalXp * 0.05); - dailyBonus = Math.max( - Math.min(maxDailyBonus, proportionalXp), - minDailyBonus - ); - breakdown.daily = dailyBonus; - } - } - - const xpWithModifiers = Math.round(baseXp * modifier); - - const xpAfterAccuracy = Math.round(xpWithModifiers * accuracyModifier); - breakdown.accPenalty = xpWithModifiers - xpAfterAccuracy; - - const totalXp = - Math.round((xpAfterAccuracy + incompleteXp) * gainMultiplier) + dailyBonus; - - if (gainMultiplier > 1) { - // breakdown.push([ - // "configMultiplier", - // Math.round((xpAfterAccuracy + incompleteXp) * (gainMultiplier - 1)), - // ]); - breakdown.configMultiplier = gainMultiplier; - } - - const isAwardingDailyBonus = dailyBonus > 0; - - return { - xp: totalXp, - dailyBonus: isAwardingDailyBonus, - breakdown, - }; + return new MonkeyResponse("Result added", { + isPb: false, + tagPbs: [], + insertedId: "", + xp: 0, + dailyXpBonus: false, + xpBreakdown: {}, + streak: 0, + }); } diff --git a/backend/src/utils/misc.ts b/backend/src/utils/misc.ts index caedef1cd..6c87f4a00 100644 --- a/backend/src/utils/misc.ts +++ b/backend/src/utils/misc.ts @@ -189,7 +189,7 @@ export function formatSeconds( } export function intersect(a: T[], b: T[], removeDuplicates = false): T[] { - let t; + let t: T[]; // eslint-disable-next-line @typescript-eslint/no-unused-expressions if (b.length > a.length) (t = b), (b = a), (a = t); // indexOf to loop over shorter const filtered = a.filter(function (e) { @@ -198,9 +198,8 @@ export function intersect(a: T[], b: T[], removeDuplicates = false): T[] { return removeDuplicates ? [...new Set(filtered)] : filtered; } - export function escapeHTML(str: string): string { - return String(str).replace(/[^\w. ]/gi, function (c) { + return str.replace(/[^\w. ]/gi, function (c) { return "&#" + c.charCodeAt(0) + ";"; }); } diff --git a/frontend/src/html/pages/test.html b/frontend/src/html/pages/test.html index 891a2d2ce..b9f2c403c 100644 --- a/frontend/src/html/pages/test.html +++ b/frontend/src/html/pages/test.html @@ -557,10 +557,10 @@ --> - -
- Sign in - to save your result +
+ Sign in + to save your result +
diff --git a/frontend/src/styles/test.scss b/frontend/src/styles/test.scss index bbec30a3e..3b016a138 100644 --- a/frontend/src/styles/test.scss +++ b/frontend/src/styles/test.scss @@ -645,6 +645,7 @@ "tribeResults tribeResults" "stats chart" "morestats morestats" + "bottom bottom" "tribeBottom tribeBottom"; // "wordsHistory wordsHistory" // "buttons buttons" @@ -780,6 +781,10 @@ -webkit-user-select: none; } + .bottom { + grid-area: bottom; + } + .chart { grid-area: chart; width: 100%; diff --git a/frontend/src/styles/tribe.scss b/frontend/src/styles/tribe.scss index a76b7a58a..6cca69e6a 100644 --- a/frontend/src/styles/tribe.scss +++ b/frontend/src/styles/tribe.scss @@ -711,9 +711,17 @@ } } -.pageTest #typingTest .tribeBars, .pageTribe .tribeBars { grid-area: players; +} + +.pageTest #typingTest .tribeBars { + margin-bottom: 1em; + font-size: 1rem; +} + +.pageTest #typingTest .tribeBars, +.pageTribe .tribeBars { td { padding: 0 0.5rem; } diff --git a/frontend/src/ts/commandline/commandline-metadata.ts b/frontend/src/ts/commandline/commandline-metadata.ts index fb105f009..8969c1585 100644 --- a/frontend/src/ts/commandline/commandline-metadata.ts +++ b/frontend/src/ts/commandline/commandline-metadata.ts @@ -732,4 +732,16 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = { options: "fromSchema", }, }, + + //tribe + tribeDelta: { + subgroup: { + options: "fromSchema", + }, + }, + tribeCarets: { + subgroup: { + options: "fromSchema", + }, + }, }; diff --git a/frontend/src/ts/commandline/lists.ts b/frontend/src/ts/commandline/lists.ts index 5b1d2ead5..8c8d5f937 100644 --- a/frontend/src/ts/commandline/lists.ts +++ b/frontend/src/ts/commandline/lists.ts @@ -60,6 +60,12 @@ const adsCommand = buildCommandForConfigKey("ads"); const minSpeedCommand = buildCommandForConfigKey("minWpm"); const minAccCommand = buildCommandForConfigKey("minAcc"); const paceCaretCommand = buildCommandForConfigKey("paceCaret"); +const modeCommand = buildCommandForConfigKey("mode"); +const timeCommand = buildCommandForConfigKey("time"); +const wordsCommand = buildCommandForConfigKey("words"); +const quoteLengthCommand = buildCommandForConfigKey("quoteLength"); +const punctuationCommand = buildCommandForConfigKey("punctuation"); +const numbersCommand = buildCommandForConfigKey("numbers"); export const commands: CommandsSubgroup = { title: "", @@ -68,12 +74,12 @@ export const commands: CommandsSubgroup = { ...ResultScreenCommands, //test screen - buildCommandForConfigKey("punctuation"), - buildCommandForConfigKey("numbers"), - buildCommandForConfigKey("mode"), - buildCommandForConfigKey("time"), - buildCommandForConfigKey("words"), - buildCommandForConfigKey("quoteLength"), + punctuationCommand, + numbersCommand, + modeCommand, + timeCommand, + wordsCommand, + quoteLengthCommand, languageCommand, { id: "changeCustomModeText", @@ -371,12 +377,12 @@ const lists = { tags: TagsCommands[0]?.subgroup, resultSaving: ResultSavingCommands[0]?.subgroup, blindMode: blindModeCommand.subgroup, - mode: ModeCommands[0]?.subgroup, - time: TimeCommands[0]?.subgroup, - words: WordsCommands[0]?.subgroup, - quoteLength: QuoteLengthCommands[0]?.subgroup, - punctuation: PunctuationCommands[0]?.subgroup, - numbers: NumbersCommands[0]?.subgroup, + mode: modeCommand.subgroup, + time: timeCommand.subgroup, + words: wordsCommand.subgroup, + quoteLength: quoteLengthCommand.subgroup, + punctuation: punctuationCommand.subgroup, + numbers: numbersCommand.subgroup, }; export function doesListExist(listName: string): boolean { diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 96e9d259d..f4c307da1 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -210,17 +210,29 @@ export function genericSet( } //numbers -export function setNumbers(numb: boolean, nosave?: boolean): boolean { - return genericSet("numbers", numb, nosave); +export function setNumbers( + numb: boolean, + nosave?: boolean, + tribeOverride = false +): boolean { + return genericSet("numbers", numb, nosave, tribeOverride); } //punctuation -export function setPunctuation(punc: boolean, nosave?: boolean): boolean { - return genericSet("punctuation", punc, nosave); +export function setPunctuation( + punc: boolean, + nosave?: boolean, + tribeOverride = false +): boolean { + return genericSet("punctuation", punc, nosave, tribeOverride); } -export function setMode(mode: Mode, nosave?: boolean): boolean { - return genericSet("mode", mode, nosave); +export function setMode( + mode: Mode, + nosave?: boolean, + tribeOverride = false +): boolean { + return genericSet("mode", mode, nosave, tribeOverride); } export function setPlaySoundOnError( diff --git a/frontend/src/ts/controllers/input-controller.ts b/frontend/src/ts/controllers/input-controller.ts index 98b9c2eba..18174444a 100644 --- a/frontend/src/ts/controllers/input-controller.ts +++ b/frontend/src/ts/controllers/input-controller.ts @@ -1008,12 +1008,12 @@ $(document).on("keydown", async (event) => { if (TribeState.getState() >= 5) { if (TribeState.getState() > 5 && TribeState.getState() < 21) return; if (TribeState.getState() === 5 && ActivePage.get() !== "tribe") { - navigate("/tribe"); + void navigate("/tribe"); return; } } else { if (ActivePage.get() !== "test") { - navigate("/"); + void navigate("/"); return; } } @@ -1061,10 +1061,10 @@ $(document).on("keydown", async (event) => { event, }); } else { - handleChar("\n", TestInput.input.current.length); + void handleChar("\n", TestInput.input.current.length); setWordsInput(" " + TestInput.input.current); if (Config.tapeMode !== "off") { - TestUI.scrollTape(); + void TestUI.scrollTape(); } } } @@ -1093,12 +1093,12 @@ $(document).on("keydown", async (event) => { if (TribeState.getState() >= 5) { if (TribeState.getState() > 5 && TribeState.getState() < 21) return; if (TribeState.getState() === 5 && ActivePage.get() !== "tribe") { - navigate("/tribe"); + void navigate("/tribe"); return; } } else { if (ActivePage.get() !== "test") { - navigate("/"); + void navigate("/"); return; } } @@ -1146,10 +1146,10 @@ $(document).on("keydown", async (event) => { event, }); } else { - handleChar("\n", TestInput.input.current.length); + void handleChar("\n", TestInput.input.current.length); setWordsInput(" " + TestInput.input.current); if (Config.tapeMode !== "off") { - TestUI.scrollTape(); + void TestUI.scrollTape(); } } } diff --git a/frontend/src/ts/controllers/route-controller.ts b/frontend/src/ts/controllers/route-controller.ts index 4d142a54d..4e3f5c265 100644 --- a/frontend/src/ts/controllers/route-controller.ts +++ b/frontend/src/ts/controllers/route-controller.ts @@ -7,8 +7,9 @@ import { isFunboxActive } from "../test/funbox/list"; import * as TestState from "../test/test-state"; import * as Notifications from "../elements/notifications"; import tribeSocket from "../tribe/tribe-socket"; -import { setAutoJoin } from "../tribe/tribe"; +import { setAutoJoin } from "../tribe/tribe-auto-join"; import { LoadingOptions } from "../pages/page"; +import { setNavigationService } from "../observables/navigate-event"; //source: https://www.youtube.com/watch?v=OstALBk-jTc // https://www.youtube.com/watch?v=OstALBk-jTc @@ -310,3 +311,11 @@ document.addEventListener("DOMContentLoaded", () => { } }); }); + +// Register navigation service for modules that can't directly import navigate +// due to circular dependency constraints +setNavigationService({ + navigate(url, options) { + void navigate(url, options); + }, +}); diff --git a/frontend/src/ts/elements/input-suggestions.ts b/frontend/src/ts/elements/input-suggestions.ts index 7d4575ced..8a64867e9 100644 --- a/frontend/src/ts/elements/input-suggestions.ts +++ b/frontend/src/ts/elements/input-suggestions.ts @@ -1,8 +1,8 @@ import * as TribeTypes from "../tribe/types"; export class InputSuggestions { - private inputElement: JQuery; - private suggestionsElement: JQuery | undefined; + private inputElement: JQuery; + private suggestionsElement: JQuery | undefined; private maxSuggestions: number; private selectedIndex: number | undefined; private prefix: string; @@ -14,7 +14,7 @@ export class InputSuggestions { private applyWith: string[]; constructor( - inputElement: JQuery, + inputElement: JQuery, prefix: string, suffix: string, maxSuggestions: number, diff --git a/frontend/src/ts/observables/navigate-event.ts b/frontend/src/ts/observables/navigate-event.ts new file mode 100644 index 000000000..f3f4120ac --- /dev/null +++ b/frontend/src/ts/observables/navigate-event.ts @@ -0,0 +1,25 @@ +type NavigateOptions = { + force?: boolean; + tribeOverride?: boolean; +}; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export interface NavigationService { + navigate(url: string, options?: NavigateOptions): void; +} + +let service: NavigationService | undefined; + +export function setNavigationService(s: NavigationService): void { + if (service !== undefined) { + throw new Error("NavigationService already initialized"); + } + service = s; +} + +export function navigate(url: string, options?: NavigateOptions): void { + if (service === undefined) { + throw new Error("NavigationService not initialized"); + } + service.navigate(url, options); +} diff --git a/frontend/src/ts/pages/test.ts b/frontend/src/ts/pages/test.ts index ce4263797..eb7ed96cd 100644 --- a/frontend/src/ts/pages/test.ts +++ b/frontend/src/ts/pages/test.ts @@ -17,11 +17,10 @@ export const page = new Page({ beforeHide: async (): Promise => { $("#wordsInput").trigger("focusout"); }, - afterHide: async (options): Promise => { + afterHide: async (): Promise => { ManualRestart.set(); TestLogic.restart({ noAnim: true, - tribeOverride: options.tribeOverride ?? false, }); void Funbox.clear(); void ModesNotice.update(); diff --git a/frontend/src/ts/pages/tribe.ts b/frontend/src/ts/pages/tribe.ts index ff98f796e..97979a6e8 100644 --- a/frontend/src/ts/pages/tribe.ts +++ b/frontend/src/ts/pages/tribe.ts @@ -4,7 +4,7 @@ import * as TribeState from "../tribe/tribe-state"; import * as TribeChat from "../tribe/tribe-chat"; export const page = new Page({ - name: "tribe", + id: "tribe", element: $(".page.pageTribe"), path: "/tribe", beforeHide: async () => { diff --git a/frontend/src/ts/popups/tribe-browse-public-rooms-popup.ts b/frontend/src/ts/popups/tribe-browse-public-rooms-popup.ts index 76fc89247..8e78314f6 100644 --- a/frontend/src/ts/popups/tribe-browse-public-rooms-popup.ts +++ b/frontend/src/ts/popups/tribe-browse-public-rooms-popup.ts @@ -56,7 +56,7 @@ export function show(): void { .css("opacity", 0) .removeClass("hidden") .animate({ opacity: 1 }, 125, () => { - $("#tribeBrowsePublicRoomsPopup .search").focus(); + $("#tribeBrowsePublicRoomsPopup .search").trigger("focus"); $("#tribeBrowsePublicRoomsPopup .search").val(""); }); } diff --git a/frontend/src/ts/popups/tribe-room-code-popup.ts b/frontend/src/ts/popups/tribe-room-code-popup.ts index efd83db2c..693ac0a6d 100644 --- a/frontend/src/ts/popups/tribe-room-code-popup.ts +++ b/frontend/src/ts/popups/tribe-room-code-popup.ts @@ -8,7 +8,7 @@ export function show(): void { .css("opacity", 0) .removeClass("hidden") .animate({ opacity: 1 }, 125, () => { - $("#tribeRoomCodePopup input").focus(); + $("#tribeRoomCodePopup input").trigger("focus"); $("#tribeRoomCodePopup input").val(""); }); } diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 06bdd4349..26582e7c4 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -807,7 +807,7 @@ export async function retrySavingResult(): Promise { const tribeChartData = { wpm: [...(completedEvent.chartData as ChartData).wpm], - raw: [...(completedEvent.chartData as ChartData).raw], + burst: [...(completedEvent.chartData as ChartData).burst], err: [...(completedEvent.chartData as ChartData).err], }; @@ -1302,7 +1302,7 @@ export async function finish(difficultyFailed = false): Promise { const tribeChartData = { wpm: [...(completedEvent.chartData as ChartData).wpm], - raw: [...(completedEvent.chartData as ChartData).raw], + burst: [...(completedEvent.chartData as ChartData).burst], err: [...(completedEvent.chartData as ChartData).err], }; diff --git a/frontend/src/ts/test/test-state.ts b/frontend/src/ts/test/test-state.ts index 2cebf3531..e95915678 100644 --- a/frontend/src/ts/test/test-state.ts +++ b/frontend/src/ts/test/test-state.ts @@ -14,6 +14,7 @@ export let isLanguageRightToLeft = false; export let isDirectionReversed = false; export let testRestarting = false; export let resultVisible = false; +export let removedUIWordCount = 0; export function setRepeated(tf: boolean): void { isRepeated = tf; @@ -85,3 +86,11 @@ export function setTestRestarting(val: boolean): void { export function setResultVisible(val: boolean): void { resultVisible = val; } + +export function setRemovedUIWordCount(val: number): void { + removedUIWordCount = val; +} + +export function incrementRemovedUIWordCount(): void { + removedUIWordCount++; +} diff --git a/frontend/src/ts/tribe/pages/tribe-page-lobby.ts b/frontend/src/ts/tribe/pages/tribe-page-lobby.ts index d21bf0d1a..90bf53631 100644 --- a/frontend/src/ts/tribe/pages/tribe-page-lobby.ts +++ b/frontend/src/ts/tribe/pages/tribe-page-lobby.ts @@ -194,7 +194,9 @@ export function updateRoomConfig(): void { $(".pageTribe .tribePage.lobby .currentConfig .groups").append(`
- ${room.config.funbox.replace(/_/g, " ")} + ${ + room.config.funbox.join(", ").replace(/_/g, " ") || "none" + }
`); @@ -264,29 +266,27 @@ export function init(): void { TribeConfig.apply(room.config); } -$(".pageTribe .tribePage.lobby .inviteLink .text").hover( - function () { +$(".pageTribe .tribePage.lobby .inviteLink .text") + .on("mouseenter", function () { $(this).css( "color", "#" + $(".pageTribe .tribePage.lobby .inviteLink .text").text() ); - }, - function () { + }) + .on("mouseleave", function () { $(this).css("color", ""); - } -); + }); -$(".pageTest #result #tribeResultBottom .inviteLink .text").hover( - function () { +$(".pageTest #result #tribeResultBottom .inviteLink .text") + .on("mouseenter", function () { $(this).css( "color", "#" + $(".pageTest #result #tribeResultBottom .inviteLink .text").text() ); - }, - function () { + }) + .on("mouseleave", function () { $(this).css("color", ""); - } -); + }); $( ".pageTribe .tribePage.lobby .inviteLink .text, .pageTest #result #tribeResultBottom .inviteLink .text" @@ -297,7 +297,7 @@ $( ); Notifications.add("Code copied", 1); } catch (e) { - Notifications.add("Could not copy to clipboard: " + e, -1); + Notifications.add("Could not copy to clipboard: " + String(e), -1); } }); @@ -310,7 +310,7 @@ $( ); Notifications.add("Link copied", 1); } catch (e) { - Notifications.add("Could not copy to clipboard: " + e, -1); + Notifications.add("Could not copy to clipboard: " + String(e), -1); } }); diff --git a/frontend/src/ts/tribe/tribe-auto-join.ts b/frontend/src/ts/tribe/tribe-auto-join.ts new file mode 100644 index 000000000..9bf37b057 --- /dev/null +++ b/frontend/src/ts/tribe/tribe-auto-join.ts @@ -0,0 +1,13 @@ +let autoJoin: string | undefined = undefined; + +export function setAutoJoin(code: string): void { + autoJoin = code; +} + +export function getAutoJoin(): string | undefined { + return autoJoin; +} + +export function clearAutoJoin(): void { + autoJoin = undefined; +} diff --git a/frontend/src/ts/tribe/tribe-bars.ts b/frontend/src/ts/tribe/tribe-bars.ts index 076a87dc0..658400dad 100644 --- a/frontend/src/ts/tribe/tribe-bars.ts +++ b/frontend/src/ts/tribe/tribe-bars.ts @@ -5,7 +5,7 @@ import tribeSocket from "./tribe-socket"; import * as ThemeColors from "../elements/theme-colors"; export function init(page: string): void { - let el: JQuery | undefined; + let el: JQuery | undefined; if (page === "test") { el = $(".pageTest #typingTest .tribeBars"); @@ -100,7 +100,7 @@ export function update(page: string, userId: string): void { update("tribe", userId); return; } - let el: JQuery | undefined; + let el: JQuery | undefined; if (page === "test") { el = $(".pageTest #typingTest .tribeBars"); } else if (page === "tribe") { @@ -138,7 +138,7 @@ export function completeBar(page: string, userId: string): void { completeBar("tribe", userId); return; } - let el: JQuery | undefined; + let el: JQuery | undefined; if (page === "test") { el = $(".pageTest #typingTest .tribeBars"); } else if (page === "tribe") { @@ -169,7 +169,7 @@ export function fadeUser( fadeUser("tribe", userId, changeColor); return; } - let el: JQuery | undefined; + let el: JQuery | undefined; if (page === "test") { el = $(".pageTest #typingTest .tribeBars"); } else if (page === "tribe") { diff --git a/frontend/src/ts/tribe/tribe-carets.ts b/frontend/src/ts/tribe/tribe-carets.ts index cdc0fe749..14b84c301 100644 --- a/frontend/src/ts/tribe/tribe-carets.ts +++ b/frontend/src/ts/tribe/tribe-carets.ts @@ -13,7 +13,7 @@ import * as TribeTypes from "./types"; const carets: { [key: string]: TribeCaret } = {}; export class TribeCaret { - private element: JQuery | undefined; + private element: JQuery | undefined; constructor( private socketId: string, @@ -138,8 +138,7 @@ export class TribeCaret { caretWidth === undefined ) { //todo fix - // oxlint-disable-next-line @typescript-eslint/only-throw-error - // eslint-disable-next-line no-throw-literal + // eslint-disable-next-line @typescript-eslint/only-throw-error, no-throw-literal throw ``; } diff --git a/frontend/src/ts/tribe/tribe-chart-controller.ts b/frontend/src/ts/tribe/tribe-chart-controller.ts index 636262a1d..b5c3c1f1f 100644 --- a/frontend/src/ts/tribe/tribe-chart-controller.ts +++ b/frontend/src/ts/tribe/tribe-chart-controller.ts @@ -270,7 +270,7 @@ async function fillData(chart: Chart, userId: string): Promise { // Math.max(...result.chartData.raw) // ); - const smoothedRawData = smooth(result.chartData.raw, 1); + const smoothedRawData = smooth(result.chartData.burst, 1); chart.data.labels = labels; //@ts-expect-error tribe @@ -351,7 +351,7 @@ export async function updateChartMaxValues(): Promise { const result = room.users[userId]?.result; if (!result) continue; const maxUserWpm = Math.max(maxWpm, Math.max(...result.chartData.wpm)); - const maxUserRaw = Math.max(maxRaw, Math.max(...result.chartData.raw)); + const maxUserRaw = Math.max(maxRaw, Math.max(...result.chartData.burst)); if (maxUserWpm > maxWpm) { maxWpm = maxUserWpm; } diff --git a/frontend/src/ts/tribe/tribe-chat.ts b/frontend/src/ts/tribe/tribe-chat.ts index f4d6bc30c..ecafbd189 100644 --- a/frontend/src/ts/tribe/tribe-chat.ts +++ b/frontend/src/ts/tribe/tribe-chat.ts @@ -1,7 +1,7 @@ import * as Notifications from "../elements/notifications"; import * as TribeState from "../tribe/tribe-state"; import * as Misc from "../utils/misc"; -import * as TestUI from "../test/test-ui"; +import * as TestState from "../test/test-state"; import tribeSocket from "./tribe-socket"; import { InputSuggestions } from "../elements/input-suggestions"; import { getEmojiList } from "../utils/json-data"; @@ -331,21 +331,23 @@ $(".pageTest #result #tribeResultBottom .chat .input input").on( } ); -$(document).keydown((e) => { +$(document).on("keydown", (e) => { if (TribeState.getState() === 5) { if ( e.key === "/" && !$(".pageTribe .lobby .chat .input input").is(":focus") ) { - $(".pageTribe .lobby .chat .input input").focus(); + $(".pageTribe .lobby .chat .input input").trigger("focus"); e.preventDefault(); } - } else if (TestUI.resultVisible && TribeState.getState() >= 20) { + } else if (TestState.resultVisible && TribeState.getState() >= 20) { if ( e.key === "/" && !$(".pageTest #result #tribeResultBottom .chat .input input").is(":focus") ) { - $(".pageTest #result #tribeResultBottom .chat .input input").focus(); + $(".pageTest #result #tribeResultBottom .chat .input input").trigger( + "focus" + ); e.preventDefault(); } } diff --git a/frontend/src/ts/tribe/tribe-config.ts b/frontend/src/ts/tribe/tribe-config.ts index 831580619..44ab5c9e2 100644 --- a/frontend/src/ts/tribe/tribe-config.ts +++ b/frontend/src/ts/tribe/tribe-config.ts @@ -7,12 +7,13 @@ import * as TribeButtons from "./tribe-buttons"; import * as TribeState from "../tribe/tribe-state"; import tribeSocket from "./tribe-socket"; import * as TribeTypes from "./types"; -import { Difficulty, Mode } from "@monkeytype/contracts/schemas/shared"; +import { Difficulty, Mode } from "@monkeytype/schemas/shared"; import { FunboxName, - QuoteLength, + QuoteLengthConfig, StopOnError, -} from "@monkeytype/contracts/schemas/configs"; +} from "@monkeytype/schemas/configs"; +import { Language } from "@monkeytype/schemas/languages"; export function getArray(config: TribeTypes.RoomConfig): string[] { const ret: string[] = []; @@ -72,7 +73,7 @@ export function apply(config: TribeTypes.RoomConfig): void { } else if (config.mode === "words") { UpdateConfig.setWordCount(config.mode2 as number, true, true); } else if (config.mode === "quote") { - UpdateConfig.setQuoteLength(config.mode2 as QuoteLength, true, true, true); + UpdateConfig.setQuoteLength(config.mode2 as QuoteLengthConfig, true, true); } else if (config.mode === "custom") { //todo fix // CustomText.setText(config.customText.text, true); @@ -82,7 +83,7 @@ export function apply(config: TribeTypes.RoomConfig): void { // CustomText.setWord(config.customText.word, true); } UpdateConfig.setDifficulty(config.difficulty as Difficulty, true, true); - UpdateConfig.setLanguage(config.language, true, true); + UpdateConfig.setLanguage(config.language as Language, true, true); UpdateConfig.setPunctuation(config.punctuation, true, true); UpdateConfig.setNumbers(config.numbers, true, true); Funbox.setFunbox(config.funbox as FunboxName[], true); diff --git a/frontend/src/ts/tribe/tribe-pages.ts b/frontend/src/ts/tribe/tribe-pages.ts index d8e9813d6..3027511c4 100644 --- a/frontend/src/ts/tribe/tribe-pages.ts +++ b/frontend/src/ts/tribe/tribe-pages.ts @@ -15,9 +15,13 @@ export async function change( if (transition) return; transition = true; const activePage = $(".page.pageTribe .tribePage.active"); + const activePageEl = activePage[0] as HTMLElement; + const newPageEl = document.querySelector( + `.page.pageTribe .tribePage.${page}` + ) as HTMLElement; void swapElements( - activePage, - $(`.page.pageTribe .tribePage.${page}`), + activePageEl, + newPageEl, 250, async () => { active = page; diff --git a/frontend/src/ts/tribe/tribe-results.ts b/frontend/src/ts/tribe/tribe-results.ts index 5d31a76d6..d907ece74 100644 --- a/frontend/src/ts/tribe/tribe-results.ts +++ b/frontend/src/ts/tribe/tribe-results.ts @@ -109,7 +109,7 @@ export function updateBar( Config.mode === "time" ? user.progress?.wpmProgress + "%" : user.progress?.progress + "%"; - if (percentOverride) { + if (percentOverride !== undefined && percentOverride !== 0) { percent = percentOverride + "%"; } el.stop(true, false).animate( @@ -161,7 +161,7 @@ export function updatePositions( //todo once i use state and redraw elements as needed instead of always keeping elements in the dom //reorder table rows based on the ordered list if (reorder) { - const elements: Record> = {}; + const elements: Record = {}; const el = $(".pageTest #result #tribeResults table tbody"); el.find("tr.user").each((_, userEl) => { const id = $(userEl).attr("id"); @@ -175,13 +175,13 @@ export function updatePositions( for (const [_pos, users] of Object.entries(positions)) { for (const user of users) { - el.append(elements[user.id] as JQuery); + el.append(elements[user.id] as JQuery); // eslint-disable-next-line @typescript-eslint/no-dynamic-delete delete elements[user.id]; } } for (const id of Object.keys(elements)) { - el.append(elements[id] as JQuery); + el.append(elements[id] as JQuery); } } } diff --git a/frontend/src/ts/tribe/tribe-socket/routes/room.ts b/frontend/src/ts/tribe/tribe-socket/routes/room.ts index e09a90253..ce6b68e70 100644 --- a/frontend/src/ts/tribe/tribe-socket/routes/room.ts +++ b/frontend/src/ts/tribe/tribe-socket/routes/room.ts @@ -1,6 +1,6 @@ -import { Mode } from "@monkeytype/contracts/schemas/shared"; +import { Mode } from "@monkeytype/schemas/shared"; import Socket from "../socket"; -import { QuoteLength } from "@monkeytype/contracts/schemas/configs"; +import { QuoteLength } from "@monkeytype/schemas/configs"; import * as TribeTypes from "../../types"; type GetPublicRoomsResponse = { diff --git a/frontend/src/ts/tribe/tribe.ts b/frontend/src/ts/tribe/tribe.ts index 3ce18b01a..d69931aba 100644 --- a/frontend/src/ts/tribe/tribe.ts +++ b/frontend/src/ts/tribe/tribe.ts @@ -29,20 +29,15 @@ import * as TestStats from "../test/test-stats"; import * as TestInput from "../test/test-input"; import * as TribeCarets from "./tribe-carets"; import * as TribeTypes from "./types"; -import { navigate } from "../controllers/route-controller"; +import { navigate } from "../observables/navigate-event"; import { ColorName } from "../elements/theme-colors"; +import * as TribeAutoJoin from "./tribe-auto-join"; const defaultName = "Guest"; let name = "Guest"; export const expectedVersion = "0.13.5"; -let autoJoin: string | undefined = undefined; - -export function setAutoJoin(code: string): void { - autoJoin = code; -} - export function getStateString(state: number): string { if (state === -1) return "error"; if (state === 1) return "connected"; @@ -220,11 +215,12 @@ async function connect(): Promise { UpdateConfig.setTimerStyle("mini", true); TribePageMenu.enableButtons(); updateState(1); - if (autoJoin !== undefined) { - TribePagePreloader.updateText(`Joining room ${autoJoin}`); + const autoJoinCode = TribeAutoJoin.getAutoJoin(); + if (autoJoinCode !== undefined) { + TribePagePreloader.updateText(`Joining room ${autoJoinCode}`); TribePagePreloader.updateSubtext("Please wait..."); setTimeout(() => { - joinRoom(autoJoin as string); + joinRoom(autoJoinCode); }, 500); } else { void TribePages.change("menu"); @@ -296,7 +292,7 @@ TribeSocket.in.system.disconnect((reason, details) => { void reset(); if (roomId !== undefined) { - autoJoin = roomId; + TribeAutoJoin.setAutoJoin(roomId); } }); diff --git a/frontend/src/ts/tribe/types.ts b/frontend/src/ts/tribe/types.ts index 252d14f81..2dda2c72b 100644 --- a/frontend/src/ts/tribe/types.ts +++ b/frontend/src/ts/tribe/types.ts @@ -1,4 +1,4 @@ -import { ChartData } from "@monkeytype/contracts/schemas/results"; +import { ChartData } from "@monkeytype/schemas/results"; export type SystemStats = { pingStart: number; diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 4b4cb3052..8f5d5fce7 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -173,9 +173,9 @@ export function escapeHTML(str: T): T { if (str === null || str === undefined) { return str; } - return String(str).replace(/[^\w. ]/gi, function (c) { + return str.replace(/[^\w. ]/gi, function (c) { return "&#" + c.charCodeAt(0) + ";"; - }); + }) as T; } export function isUsernameValid(name: string): boolean { diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 538d0b433..9b44e28cf 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -11,7 +11,7 @@ import * as Loader from "../elements/loader"; import * as AccountButton from "../elements/account-button"; import { restart as restartTest } from "../test/test-logic"; import * as ChallengeController from "../controllers/challenge-controller"; -import * as Tribe from "../tribe/tribe"; +import { setAutoJoin } from "../tribe/tribe-auto-join"; import { DifficultySchema, Mode2Schema, @@ -315,7 +315,7 @@ export function loadTribeAutoJoinFromUrl(override?: string): void { if (window.location.pathname !== "/tribe") return; const getValue = Misc.findGetParameter("code", override); if (getValue === null) return; - Tribe.setAutoJoin(getValue); + setAutoJoin(getValue); } AuthEvent.subscribe((event) => {