mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-12-27 10:31:22 +08:00
fix: Resolve build errors, circular dependencies, and config sync loop (@eikomaniac) (#7173)
### Description - Break circular dependencies in tribes module by extracting `navigate-event.ts` and `tribe-auto-join.ts` - Fix TypeScript build and runtime errors across tribes-related files - Prevent infinite config sync loop in tribes lobby - + code quality fixes --------- Co-authored-by: Miodec <jack@monkeytype.com>
This commit is contained in:
parent
0318ee8c1e
commit
42041954b3
32 changed files with 214 additions and 789 deletions
|
|
@ -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<undefined, AddResultRequest>
|
||||
_req: MonkeyRequest<undefined, AddResultRequest>
|
||||
): Promise<AddResultResponse> {
|
||||
// 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<XpResult> {
|
||||
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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ export function formatSeconds(
|
|||
}
|
||||
|
||||
export function intersect<T>(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<T>(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) + ";";
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -557,10 +557,10 @@
|
|||
<i class="fas fa-fw fa-ad"></i>
|
||||
</button> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="loginTip">
|
||||
<a href="/login" router-link>Sign in</a>
|
||||
to save your result
|
||||
<div class="loginTip">
|
||||
<a href="/login" router-link>Sign in</a>
|
||||
to save your result
|
||||
</div>
|
||||
</div>
|
||||
<div class="ssWatermark hidden">monkeytype.com</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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%;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -732,4 +732,16 @@ export const commandlineConfigMetadata: CommandlineConfigMetadataObject = {
|
|||
options: "fromSchema",
|
||||
},
|
||||
},
|
||||
|
||||
//tribe
|
||||
tribeDelta: {
|
||||
subgroup: {
|
||||
options: "fromSchema",
|
||||
},
|
||||
},
|
||||
tribeCarets: {
|
||||
subgroup: {
|
||||
options: "fromSchema",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -210,17 +210,29 @@ export function genericSet<T extends keyof ConfigSchemas.Config>(
|
|||
}
|
||||
|
||||
//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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
import * as TribeTypes from "../tribe/types";
|
||||
|
||||
export class InputSuggestions {
|
||||
private inputElement: JQuery<HTMLElement>;
|
||||
private suggestionsElement: JQuery<HTMLElement> | 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<HTMLElement>,
|
||||
inputElement: JQuery,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
maxSuggestions: number,
|
||||
|
|
|
|||
25
frontend/src/ts/observables/navigate-event.ts
Normal file
25
frontend/src/ts/observables/navigate-event.ts
Normal file
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -17,11 +17,10 @@ export const page = new Page({
|
|||
beforeHide: async (): Promise<void> => {
|
||||
$("#wordsInput").trigger("focusout");
|
||||
},
|
||||
afterHide: async (options): Promise<void> => {
|
||||
afterHide: async (): Promise<void> => {
|
||||
ManualRestart.set();
|
||||
TestLogic.restart({
|
||||
noAnim: true,
|
||||
tribeOverride: options.tribeOverride ?? false,
|
||||
});
|
||||
void Funbox.clear();
|
||||
void ModesNotice.update();
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("");
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -807,7 +807,7 @@ export async function retrySavingResult(): Promise<void> {
|
|||
|
||||
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<void> {
|
|||
|
||||
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],
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -194,7 +194,9 @@ export function updateRoomConfig(): void {
|
|||
|
||||
$(".pageTribe .tribePage.lobby .currentConfig .groups").append(`
|
||||
<div class='group' aria-label="Funbox" data-balloon-pos="up" commands="funbox">
|
||||
<i class="fas fa-gamepad"></i>${room.config.funbox.replace(/_/g, " ")}
|
||||
<i class="fas fa-gamepad"></i>${
|
||||
room.config.funbox.join(", ").replace(/_/g, " ") || "none"
|
||||
}
|
||||
</div>
|
||||
`);
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
13
frontend/src/ts/tribe/tribe-auto-join.ts
Normal file
13
frontend/src/ts/tribe/tribe-auto-join.ts
Normal file
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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<HTMLElement> | 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<HTMLElement> | 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<HTMLElement> | 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<HTMLElement> | undefined;
|
||||
let el: JQuery | undefined;
|
||||
if (page === "test") {
|
||||
el = $(".pageTest #typingTest .tribeBars");
|
||||
} else if (page === "tribe") {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import * as TribeTypes from "./types";
|
|||
const carets: { [key: string]: TribeCaret } = {};
|
||||
|
||||
export class TribeCaret {
|
||||
private element: JQuery<HTMLElement> | 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 ``;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -270,7 +270,7 @@ async function fillData(chart: Chart, userId: string): Promise<void> {
|
|||
// 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<void> {
|
|||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<string, JQuery<HTMLElement>> = {};
|
||||
const elements: Record<string, JQuery> = {};
|
||||
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<HTMLElement>);
|
||||
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<HTMLElement>);
|
||||
el.append(elements[id] as JQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
|||
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);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { ChartData } from "@monkeytype/contracts/schemas/results";
|
||||
import { ChartData } from "@monkeytype/schemas/results";
|
||||
|
||||
export type SystemStats = {
|
||||
pingStart: number;
|
||||
|
|
|
|||
|
|
@ -173,9 +173,9 @@ export function escapeHTML<T extends string | null | undefined>(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 {
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue