mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-11-10 14:10:59 +08:00
Merge branch 'master' into newnav
This commit is contained in:
commit
24f1c7c609
28 changed files with 672 additions and 95 deletions
|
|
@ -489,4 +489,32 @@ describe("UserDal", () => {
|
|||
expect(resetUser.bananas).toStrictEqual(0);
|
||||
expect(resetUser.xp).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("getInbox should return the user's inbox", async () => {
|
||||
await UserDAL.addUser("test name", "test email", "TestID");
|
||||
|
||||
const emptyInbox = await UserDAL.getInbox("TestID");
|
||||
|
||||
expect(emptyInbox).toStrictEqual([]);
|
||||
|
||||
await UserDAL.addToInbox(
|
||||
"TestID",
|
||||
[
|
||||
{
|
||||
getTemplate: (user) => ({
|
||||
subject: `Hello ${user.name}!`,
|
||||
}),
|
||||
} as any,
|
||||
],
|
||||
0
|
||||
);
|
||||
|
||||
const inbox = await UserDAL.getInbox("TestID");
|
||||
|
||||
expect(inbox).toStrictEqual([
|
||||
{
|
||||
subject: "Hello test name!",
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ const dailyLeaderboardsConfig = {
|
|||
],
|
||||
dailyLeaderboardCacheSize: 3,
|
||||
topResultsToAnnounce: 3,
|
||||
xpReward: 0,
|
||||
};
|
||||
|
||||
describe("Daily Leaderboards", () => {
|
||||
|
|
|
|||
22
backend/__tests__/utils/monkey-mail.spec.ts
Normal file
22
backend/__tests__/utils/monkey-mail.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { buildMonkeyMail } from "../../src/utils/monkey-mail";
|
||||
|
||||
describe("Monkey Mail", () => {
|
||||
it("should properly create a mail object", () => {
|
||||
const mailConfig = {
|
||||
subject: "",
|
||||
body: "",
|
||||
timestamp: Date.now(),
|
||||
getTemplate: (): any => ({}),
|
||||
};
|
||||
|
||||
const mail = buildMonkeyMail(mailConfig) as any;
|
||||
|
||||
expect(mail.id).toBeDefined();
|
||||
expect(mail.subject).toBe("");
|
||||
expect(mail.body).toBe("");
|
||||
expect(mail.timestamp).toBeDefined();
|
||||
expect(mail.read).toBe(false);
|
||||
expect(mail.rewards).toEqual([]);
|
||||
expect(mail.getTemplate).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -117,6 +117,19 @@ export async function updateEmail(
|
|||
return new MonkeyResponse("Email updated");
|
||||
}
|
||||
|
||||
function getRelevantUserInfo(
|
||||
user: MonkeyTypes.User
|
||||
): Partial<MonkeyTypes.User> {
|
||||
return _.omit(user, [
|
||||
"bananas",
|
||||
"lbPersonalBests",
|
||||
"quoteMod",
|
||||
"inbox",
|
||||
"nameHistory",
|
||||
"lastNameChange",
|
||||
]);
|
||||
}
|
||||
|
||||
export async function getUser(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
|
|
@ -142,7 +155,12 @@ export async function getUser(
|
|||
const agentLog = buildAgentLog(req);
|
||||
Logger.logToDb("user_data_requested", agentLog, uid);
|
||||
|
||||
return new MonkeyResponse("User data retrieved", userInfo);
|
||||
const userData = {
|
||||
...getRelevantUserInfo(userInfo),
|
||||
inboxUnreadSize: _.filter(userInfo.inbox, { read: false }).length,
|
||||
};
|
||||
|
||||
return new MonkeyResponse("User data retrieved", userData);
|
||||
}
|
||||
|
||||
export async function linkDiscord(
|
||||
|
|
@ -487,3 +505,24 @@ export async function updateProfile(
|
|||
|
||||
return new MonkeyResponse("Profile updated");
|
||||
}
|
||||
|
||||
export async function getInbox(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
const inbox = await UserDAL.getInbox(uid);
|
||||
|
||||
return new MonkeyResponse("Inbox retrieved", inbox);
|
||||
}
|
||||
|
||||
export async function updateInbox(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const { mailIdsToMarkRead, mailIdsToDelete } = req.body;
|
||||
|
||||
await UserDAL.updateInbox(uid, mailIdsToMarkRead, mailIdsToDelete);
|
||||
|
||||
return new MonkeyResponse("Inbox updated");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -470,4 +470,35 @@ router.patch(
|
|||
asyncHandler(UserController.updateProfile)
|
||||
);
|
||||
|
||||
const mailIdSchema = joi.array().items(joi.string().guid()).min(1).default([]);
|
||||
|
||||
const requireInboxEnabled = validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.users.inbox.enabled;
|
||||
},
|
||||
invalidMessage: "Your inbox is not available at this time.",
|
||||
});
|
||||
|
||||
router.get(
|
||||
"/inbox",
|
||||
requireInboxEnabled,
|
||||
authenticateRequest(),
|
||||
RateLimit.userMailGet,
|
||||
asyncHandler(UserController.getInbox)
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/inbox",
|
||||
requireInboxEnabled,
|
||||
authenticateRequest(),
|
||||
RateLimit.userMailUpdate,
|
||||
validateRequest({
|
||||
body: {
|
||||
mailIdsToDelete: mailIdSchema,
|
||||
mailIdsToMarkRead: mailIdSchema,
|
||||
},
|
||||
}),
|
||||
asyncHandler(UserController.updateInbox)
|
||||
);
|
||||
|
||||
export default router;
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
maxDailyBonus: 0,
|
||||
minDailyBonus: 0,
|
||||
},
|
||||
inbox: {
|
||||
enabled: false,
|
||||
maxMail: 0,
|
||||
},
|
||||
},
|
||||
rateLimiting: {
|
||||
badAuthentication: {
|
||||
|
|
@ -63,6 +67,7 @@ export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
// GOTCHA! MUST ATLEAST BE 1, LRUCache module will make process crash and die
|
||||
dailyLeaderboardCacheSize: 1,
|
||||
topResultsToAnnounce: 1, // This should never be 0. Setting to zero will announce all results.
|
||||
xpReward: 0,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -255,6 +260,21 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
|
|||
},
|
||||
},
|
||||
},
|
||||
inbox: {
|
||||
type: "object",
|
||||
label: "Inbox",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
maxMail: {
|
||||
type: "number",
|
||||
label: "Max Messages",
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
profiles: {
|
||||
type: "object",
|
||||
label: "User Profiles",
|
||||
|
|
@ -347,6 +367,11 @@ export const CONFIGURATION_FORM_SCHEMA: ObjectSchema = {
|
|||
label: "Top Results To Announce",
|
||||
min: 1,
|
||||
},
|
||||
xpReward: {
|
||||
type: "number",
|
||||
label: "XP Reward",
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -4,9 +4,10 @@ import { updateUserEmail } from "../utils/auth";
|
|||
import { checkAndUpdatePb } from "../utils/pb";
|
||||
import * as db from "../init/db";
|
||||
import MonkeyError from "../utils/error";
|
||||
import { Collection, ObjectId, WithId, Long } from "mongodb";
|
||||
import { Collection, ObjectId, WithId, Long, UpdateFilter } from "mongodb";
|
||||
import Logger from "../utils/logger";
|
||||
import { flattenObjectDeep } from "../utils/misc";
|
||||
import { MonkeyMailWithTemplate } from "../utils/monkey-mail";
|
||||
|
||||
const SECONDS_PER_HOUR = 3600;
|
||||
|
||||
|
|
@ -732,3 +733,110 @@ export async function updateProfile(
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getInbox(
|
||||
uid: string
|
||||
): Promise<MonkeyTypes.User["inbox"]> {
|
||||
const user = await getUser(uid, "get inventory");
|
||||
return user.inbox ?? [];
|
||||
}
|
||||
|
||||
export async function addToInbox(
|
||||
uid: string,
|
||||
mail: MonkeyMailWithTemplate[],
|
||||
maxInboxSize: number
|
||||
): Promise<void> {
|
||||
const user = await getUser(uid, "add to inbox");
|
||||
|
||||
const inbox = user.inbox ?? [];
|
||||
|
||||
for (let i = 0; i < inbox.length + mail.length - maxInboxSize; i++) {
|
||||
inbox.pop();
|
||||
}
|
||||
|
||||
const evaluatedMail: MonkeyTypes.MonkeyMail[] = mail.map((mail) => {
|
||||
return _.omit(
|
||||
{
|
||||
...mail,
|
||||
...(mail.getTemplate && mail.getTemplate(user)),
|
||||
},
|
||||
"getTemplate"
|
||||
);
|
||||
});
|
||||
|
||||
inbox.unshift(...evaluatedMail);
|
||||
const newInbox = inbox.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
await getUsersCollection().updateOne(
|
||||
{ uid },
|
||||
{
|
||||
$set: {
|
||||
inbox: newInbox,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function buildRewardUpdates(
|
||||
rewards: MonkeyTypes.AllRewards[]
|
||||
): UpdateFilter<WithId<MonkeyTypes.User>> {
|
||||
let totalXp = 0;
|
||||
const newBadges: MonkeyTypes.Badge[] = [];
|
||||
|
||||
rewards.forEach((reward) => {
|
||||
if (reward.type === "xp") {
|
||||
totalXp += reward.item;
|
||||
} else if (reward.type === "badge") {
|
||||
newBadges.push(reward.item);
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
$inc: {
|
||||
xp: totalXp,
|
||||
},
|
||||
$push: {
|
||||
"inventory.badges": { $each: newBadges },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateInbox(
|
||||
uid: string,
|
||||
mailToRead: string[],
|
||||
mailToDelete: string[]
|
||||
): Promise<void> {
|
||||
const user = await getUser(uid, "update inbox");
|
||||
|
||||
const inbox = user.inbox ?? [];
|
||||
|
||||
const mailToReadSet = new Set(mailToRead);
|
||||
const mailToDeleteSet = new Set(mailToDelete);
|
||||
|
||||
const allRewards: MonkeyTypes.AllRewards[] = [];
|
||||
|
||||
const newInbox = inbox
|
||||
.filter((mail) => {
|
||||
const { id, rewards } = mail;
|
||||
|
||||
if (mailToReadSet.has(id) && !mail.read) {
|
||||
mail.read = true;
|
||||
if (rewards.length > 0) {
|
||||
allRewards.push(...rewards);
|
||||
}
|
||||
}
|
||||
|
||||
return !mailToDeleteSet.has(id);
|
||||
})
|
||||
.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
const baseUpdate = {
|
||||
$set: {
|
||||
inbox: newInbox,
|
||||
},
|
||||
};
|
||||
const rewardUpdates = buildRewardUpdates(allRewards);
|
||||
const mergedUpdates = _.merge(baseUpdate, rewardUpdates);
|
||||
|
||||
await getUsersCollection().updateOne({ uid }, mergedUpdates);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { getCurrentDayTimestamp } from "../utils/misc";
|
|||
import { getCachedConfiguration } from "../init/configuration";
|
||||
import { DailyLeaderboard } from "../utils/daily-leaderboards";
|
||||
import { announceDailyLeaderboardTopResults } from "../tasks/george";
|
||||
import { addToInbox } from "../dal/user";
|
||||
import { buildMonkeyMail } from "../utils/monkey-mail";
|
||||
|
||||
const CRON_SCHEDULE = "1 0 * * *"; // At 00:01.
|
||||
const ONE_DAY_IN_MILLISECONDS = 24 * 60 * 60 * 1000;
|
||||
|
|
@ -24,7 +26,8 @@ async function announceDailyLeaderboard(
|
|||
language: string,
|
||||
mode: string,
|
||||
mode2: string,
|
||||
dailyLeaderboardsConfig: MonkeyTypes.Configuration["dailyLeaderboards"]
|
||||
dailyLeaderboardsConfig: MonkeyTypes.Configuration["dailyLeaderboards"],
|
||||
inboxConfig: MonkeyTypes.Configuration["users"]["inbox"]
|
||||
): Promise<void> {
|
||||
const yesterday = getCurrentDayTimestamp() - ONE_DAY_IN_MILLISECONDS;
|
||||
const dailyLeaderboard = new DailyLeaderboard(
|
||||
|
|
@ -43,6 +46,29 @@ async function announceDailyLeaderboard(
|
|||
return;
|
||||
}
|
||||
|
||||
if (inboxConfig.enabled) {
|
||||
const { xpReward } = dailyLeaderboardsConfig;
|
||||
|
||||
const inboxPromises = topResults.map(async (entry) => {
|
||||
const mail = buildMonkeyMail({
|
||||
rewards: [
|
||||
{
|
||||
type: "xp",
|
||||
item: xpReward,
|
||||
},
|
||||
],
|
||||
getTemplate: (user) => ({
|
||||
subject: `${xpReward} XP for top placement in the daily leaderboard!`,
|
||||
body: `Congratulations ${user.name} on placing top ${entry.rank} in the ${language} ${mode} ${mode2} daily leaderboard! Claim your ${xpReward} xp!`,
|
||||
}),
|
||||
});
|
||||
|
||||
return await addToInbox(entry.uid, [mail], inboxConfig.maxMail);
|
||||
});
|
||||
|
||||
await Promise.allSettled(inboxPromises);
|
||||
}
|
||||
|
||||
const leaderboardId = `${mode} ${mode2} ${language}`;
|
||||
await announceDailyLeaderboardTopResults(
|
||||
leaderboardId,
|
||||
|
|
@ -52,14 +78,24 @@ async function announceDailyLeaderboard(
|
|||
}
|
||||
|
||||
async function announceDailyLeaderboards(): Promise<void> {
|
||||
const { dailyLeaderboards, maintenance } = await getCachedConfiguration();
|
||||
const {
|
||||
dailyLeaderboards,
|
||||
users: { inbox },
|
||||
maintenance,
|
||||
} = await getCachedConfiguration();
|
||||
if (!dailyLeaderboards.enabled || maintenance) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.allSettled(
|
||||
leaderboardsToAnnounce.map(({ language, mode, mode2 }) => {
|
||||
return announceDailyLeaderboard(language, mode, mode2, dailyLeaderboards);
|
||||
return announceDailyLeaderboard(
|
||||
language,
|
||||
mode,
|
||||
mode2,
|
||||
dailyLeaderboards,
|
||||
inbox
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -439,6 +439,20 @@ export const userProfileUpdate = rateLimit({
|
|||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userMailGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
export const userMailUpdate = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
max: 60 * REQUEST_MULTIPLIER,
|
||||
keyGenerator: getKeyWithUid,
|
||||
handler: customHandler,
|
||||
});
|
||||
|
||||
// ApeKeys Routing
|
||||
export const apeKeysGet = rateLimit({
|
||||
windowMs: ONE_HOUR_MS,
|
||||
|
|
|
|||
32
backend/src/types/types.d.ts
vendored
32
backend/src/types/types.d.ts
vendored
|
|
@ -46,6 +46,10 @@ declare namespace MonkeyTypes {
|
|||
maxDailyBonus: number;
|
||||
minDailyBonus: number;
|
||||
};
|
||||
inbox: {
|
||||
enabled: boolean;
|
||||
maxMail: number;
|
||||
};
|
||||
};
|
||||
apeKeys: {
|
||||
endpointsEnabled: boolean;
|
||||
|
|
@ -68,6 +72,7 @@ declare namespace MonkeyTypes {
|
|||
validModeRules: ValidModeRule[];
|
||||
dailyLeaderboardCacheSize: number;
|
||||
topResultsToAnnounce: number;
|
||||
xpReward: number;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -98,6 +103,32 @@ declare namespace MonkeyTypes {
|
|||
};
|
||||
}
|
||||
|
||||
interface Reward<T> {
|
||||
type: string;
|
||||
item: T;
|
||||
}
|
||||
|
||||
interface XpReward extends Reward<number> {
|
||||
type: "xp";
|
||||
item: number;
|
||||
}
|
||||
|
||||
interface BadgeReward extends Reward<Badge> {
|
||||
type: "badge";
|
||||
item: Badge;
|
||||
}
|
||||
|
||||
type AllRewards = XpReward | BadgeReward;
|
||||
|
||||
interface MonkeyMail {
|
||||
id: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
timestamp: number;
|
||||
read: boolean;
|
||||
rewards: AllRewards[];
|
||||
}
|
||||
|
||||
interface User {
|
||||
autoBanTimestamps?: number[];
|
||||
addedAt: number;
|
||||
|
|
@ -128,6 +159,7 @@ declare namespace MonkeyTypes {
|
|||
profileDetails?: UserProfileDetails;
|
||||
inventory?: UserInventory;
|
||||
xp?: number;
|
||||
inbox?: MonkeyMail[];
|
||||
}
|
||||
|
||||
interface UserInventory {
|
||||
|
|
|
|||
20
backend/src/utils/monkey-mail.ts
Normal file
20
backend/src/utils/monkey-mail.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { v4 } from "uuid";
|
||||
|
||||
type MonkeyMailOptions = Partial<Omit<MonkeyMailWithTemplate, "id" | "read">>;
|
||||
export interface MonkeyMailWithTemplate extends MonkeyTypes.MonkeyMail {
|
||||
getTemplate?: (user: MonkeyTypes.User) => MonkeyMailOptions;
|
||||
}
|
||||
|
||||
export function buildMonkeyMail(
|
||||
options: MonkeyMailOptions
|
||||
): MonkeyMailWithTemplate {
|
||||
return {
|
||||
id: v4(),
|
||||
subject: options.subject || "",
|
||||
body: options.body || "",
|
||||
timestamp: options.timestamp || Date.now(),
|
||||
read: false,
|
||||
rewards: options.rewards || [],
|
||||
getTemplate: options.getTemplate,
|
||||
};
|
||||
}
|
||||
10
frontend/package-lock.json
generated
10
frontend/package-lock.json
generated
|
|
@ -7188,6 +7188,11 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
|
|
@ -20123,6 +20128,11 @@
|
|||
"integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==",
|
||||
"dev": true
|
||||
},
|
||||
"immediate": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
|
||||
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.0.0.tgz",
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@ a {
|
|||
}
|
||||
}
|
||||
|
||||
a[data-link] * {
|
||||
a[router-link] * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
|
@ -471,13 +471,15 @@ key {
|
|||
|
||||
.configureAPI.button {
|
||||
position: fixed;
|
||||
left: 2rem;
|
||||
bottom: 2rem;
|
||||
left: 0;
|
||||
top: 10rem;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
z-index: 999999999;
|
||||
border-radius: 0 1rem 1rem 0;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
.pbsWords,
|
||||
.pbsTime,
|
||||
.details {
|
||||
user-select: none;
|
||||
// user-select: none;
|
||||
background: var(--sub-alt-color);
|
||||
padding: 1rem;
|
||||
border-radius: var(--roundness);
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ window.addEventListener("popstate", () => {
|
|||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.body.addEventListener("click", (e) => {
|
||||
const target = e?.target as HTMLLinkElement;
|
||||
if (target.matches("[data-link]") && target?.href) {
|
||||
if (target.matches("[router-link]") && target?.href) {
|
||||
e.preventDefault();
|
||||
navigate(target.href);
|
||||
}
|
||||
|
|
@ -167,10 +167,6 @@ $("#top .logo").on("click", () => {
|
|||
navigate("/");
|
||||
});
|
||||
|
||||
$(document).on("click", "#leaderboards .entryName", (e) => {
|
||||
const uid = $(e.target).attr("uid");
|
||||
if (uid) {
|
||||
navigate(`/profile/${uid}`);
|
||||
Leaderboards.hide();
|
||||
}
|
||||
$(document).on("click", "#leaderboards a.entryName", () => {
|
||||
Leaderboards.hide();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -392,6 +392,9 @@ $(document).ready(() => {
|
|||
// opens command line if escape or ctrl/cmd + shift + p
|
||||
if (
|
||||
((event.key === "Escape" && Config.quickRestart !== "esc") ||
|
||||
(event.key?.toLowerCase() === "p" &&
|
||||
(event.metaKey || event.ctrlKey) &&
|
||||
event.shiftKey) ||
|
||||
(event.key === "Tab" && Config.quickRestart === "esc")) &&
|
||||
!$("#commandLineWrapper").hasClass("hidden")
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -322,7 +322,9 @@ async function fillTable(lb: LbKey, prepend?: number): Promise<void> {
|
|||
}</td>
|
||||
<td>
|
||||
<div class="avatarNameBadge">${avatar}
|
||||
<span class="entryName" uid=${entry.uid}>${entry.name}</span>
|
||||
<a href="${location.origin}/profile/${entry.uid}" class="entryName" uid=${
|
||||
entry.uid
|
||||
} router-link>${entry.name}</a>
|
||||
${entry.badgeId ? getBadgeHTMLbyId(entry.badgeId) : ""}
|
||||
</div>
|
||||
</td>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ if (window.location.hostname === "localhost") {
|
|||
$("#bottom .version .text").text("localhost");
|
||||
$("#bottom .version").css("opacity", 1);
|
||||
$("body").prepend(
|
||||
`<a class='button configureAPI' href='http://localhost:5005/configure/' target='_blank'><i class="fas fa-fw fa-server"></i>Configure Server</a>`
|
||||
`<a class='button configureAPI' href='http://localhost:5005/configure/' target='_blank' aria-label="Configure API" data-balloon-pos="right"><i class="fas fa-fw fa-server"></i></a>`
|
||||
);
|
||||
} else {
|
||||
Misc.getReleasesFromGitHub().then((v) => {
|
||||
|
|
|
|||
|
|
@ -45,12 +45,11 @@ interface PoemObject {
|
|||
author: string;
|
||||
}
|
||||
|
||||
export async function getPoem(): Promise<Poem | undefined> {
|
||||
export async function getPoem(): Promise<Poem | false> {
|
||||
console.log("Getting poem");
|
||||
|
||||
const response = await axios.get(apiURL);
|
||||
|
||||
try {
|
||||
const response = await axios.get(apiURL);
|
||||
const poemObj: PoemObject = response.data[0];
|
||||
|
||||
const words: string[] = [];
|
||||
|
|
@ -64,7 +63,6 @@ export async function getPoem(): Promise<Poem | undefined> {
|
|||
return new Poem(poemObj.title, poemObj.author, words);
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -763,7 +763,9 @@ async function getNextWord(
|
|||
let regenarationCount = 0; //infinite loop emergency stop button
|
||||
while (
|
||||
regenarationCount < 100 &&
|
||||
((/[A-Z]/.test(randomWord) && !Config.punctuation) ||
|
||||
((Config.mode !== "custom" &&
|
||||
/[A-Z]/.test(randomWord) &&
|
||||
!Config.punctuation) ||
|
||||
previousWord == randomWord ||
|
||||
previousWord2 == randomWord ||
|
||||
(Config.mode !== "custom" &&
|
||||
|
|
@ -947,7 +949,18 @@ export async function init(): Promise<void> {
|
|||
? await Wikipedia.getSection(Config.language)
|
||||
: await Poetry.getPoem();
|
||||
|
||||
if (Config.funbox == "poetry" && section === false) {
|
||||
Notifications.add(
|
||||
"Error while getting poetry. Please try again later",
|
||||
-1
|
||||
);
|
||||
UpdateConfig.setFunbox("none");
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (section === undefined) continue;
|
||||
if (section === false) continue;
|
||||
|
||||
for (const word of section.words) {
|
||||
if (wordCount >= Config.words && Config.mode == "words") {
|
||||
|
|
@ -1156,7 +1169,19 @@ export async function addWord(): Promise<void> {
|
|||
? await Wikipedia.getSection(Config.language)
|
||||
: await Poetry.getPoem();
|
||||
|
||||
if (Config.funbox == "poetry" && section === false) {
|
||||
Notifications.add(
|
||||
"Error while getting poetry. Please try again later",
|
||||
-1
|
||||
);
|
||||
UpdateConfig.setFunbox("none");
|
||||
restart();
|
||||
return;
|
||||
}
|
||||
|
||||
if (section === undefined) return;
|
||||
if (section === false) return;
|
||||
|
||||
let wordCount = 0;
|
||||
for (const word of section.words) {
|
||||
if (wordCount >= Config.words && Config.mode == "words") {
|
||||
|
|
@ -1243,63 +1268,7 @@ export async function retrySavingResult(): Promise<void> {
|
|||
|
||||
Notifications.add("Retrying to save...");
|
||||
|
||||
const response = await Ape.results.save(completedEvent);
|
||||
|
||||
AccountButton.loading(false);
|
||||
Result.hideCrown();
|
||||
|
||||
if (response.status !== 200) {
|
||||
retrySaving.canRetry = true;
|
||||
$("#retrySavingResultButton").removeClass("hidden");
|
||||
return Notifications.add("Result not saved. " + response.message, -1);
|
||||
}
|
||||
|
||||
if (response.data.xp) {
|
||||
const snapxp = DB.getSnapshot().xp;
|
||||
AccountButton.updateXpBar(
|
||||
snapxp,
|
||||
response.data.xp,
|
||||
response.data.dailyXpBonus,
|
||||
response.data.xpBreakdown
|
||||
);
|
||||
DB.addXp(response.data.xp);
|
||||
}
|
||||
|
||||
completedEvent._id = response.data.insertedId;
|
||||
if (response.data.isPb) {
|
||||
completedEvent.isPb = true;
|
||||
}
|
||||
|
||||
DB.saveLocalResult(completedEvent);
|
||||
DB.updateLocalStats(
|
||||
TestStats.restartCount + 1,
|
||||
completedEvent.testDuration +
|
||||
completedEvent.incompleteTestSeconds -
|
||||
completedEvent.afkDuration
|
||||
);
|
||||
|
||||
AnalyticsController.log("testCompleted");
|
||||
|
||||
if (response.data.isPb) {
|
||||
//new pb
|
||||
Result.showCrown();
|
||||
Result.updateCrown();
|
||||
DB.saveLocalPB(
|
||||
Config.mode,
|
||||
completedEvent.mode2,
|
||||
Config.punctuation,
|
||||
Config.language,
|
||||
Config.difficulty,
|
||||
Config.lazyMode,
|
||||
completedEvent.wpm,
|
||||
completedEvent.acc,
|
||||
completedEvent.rawWpm,
|
||||
completedEvent.consistency
|
||||
);
|
||||
}
|
||||
|
||||
$("#retrySavingResultButton").addClass("hidden");
|
||||
Notifications.add("Result saved", 1);
|
||||
saveResult(completedEvent, true);
|
||||
}
|
||||
|
||||
function buildCompletedEvent(difficultyFailed: boolean): CompletedEvent {
|
||||
|
|
@ -1650,18 +1619,25 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
|
||||
completedEvent.hash = objectHash(completedEvent);
|
||||
|
||||
saveResult(completedEvent, false);
|
||||
}
|
||||
|
||||
async function saveResult(
|
||||
completedEvent: CompletedEvent,
|
||||
isRetrying: boolean
|
||||
): Promise<void> {
|
||||
const response = await Ape.results.save(completedEvent);
|
||||
|
||||
AccountButton.loading(false);
|
||||
Result.hideCrown();
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Error saving result", completedEvent);
|
||||
$("#retrySavingResultButton").removeClass("hidden");
|
||||
if (response.message === "Incorrect result hash") {
|
||||
console.log(completedEvent);
|
||||
}
|
||||
retrySaving.completedEvent = completedEvent;
|
||||
retrySaving.canRetry = true;
|
||||
$("#retrySavingResultButton").removeClass("hidden");
|
||||
if (!isRetrying) {
|
||||
retrySaving.completedEvent = completedEvent;
|
||||
}
|
||||
return Notifications.add("Failed to save result: " + response.message, -1);
|
||||
}
|
||||
|
||||
|
|
@ -1676,8 +1652,6 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
DB.addXp(response.data.xp);
|
||||
}
|
||||
|
||||
Result.hideCrown();
|
||||
|
||||
completedEvent._id = response.data.insertedId;
|
||||
if (response.data.isPb) {
|
||||
completedEvent.isPb = true;
|
||||
|
|
@ -1723,6 +1697,9 @@ export async function finish(difficultyFailed = false): Promise<void> {
|
|||
}
|
||||
|
||||
$("#retrySavingResultButton").addClass("hidden");
|
||||
if (isRetrying) {
|
||||
Notifications.add("Result saved", 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function fail(reason: string): void {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="content">
|
||||
<div class="image"></div>
|
||||
<div>Ooops! Looks like you found a page that doesn't exist.</div>
|
||||
<a href="/" class="button" data-link>
|
||||
<a href="/" class="button" router-link>
|
||||
<i class="fas fa-home"></i>
|
||||
Go Home
|
||||
</a>
|
||||
|
|
|
|||
|
|
@ -288,7 +288,7 @@
|
|||
<canvas id="wpmChart"></canvas>
|
||||
</div>
|
||||
<div class="loginTip">
|
||||
<a href="/login" tabindex="2" data-link>Sign in</a>
|
||||
<a href="/login" tabindex="2" router-link>Sign in</a>
|
||||
to save your result
|
||||
</div>
|
||||
<div class="bottom" style="grid-column: 1/3">
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
tabindex="2"
|
||||
href="/"
|
||||
onclick="this.blur();"
|
||||
data-link
|
||||
router-link
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="fas fa-fw fa-keyboard"></i>
|
||||
|
|
@ -70,7 +70,7 @@
|
|||
tabindex="2"
|
||||
href="/about"
|
||||
onclick="this.blur();"
|
||||
data-link
|
||||
router-link
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="fas fa-fw fa-info"></i>
|
||||
|
|
@ -93,7 +93,7 @@
|
|||
tabindex="2"
|
||||
href="/settings"
|
||||
onclick="this.blur();"
|
||||
data-link
|
||||
router-link
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="fas fa-fw fa-cog"></i>
|
||||
|
|
@ -105,7 +105,7 @@
|
|||
tabindex="2"
|
||||
href="/account"
|
||||
onclick="this.blur();"
|
||||
data-link
|
||||
router-link
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="fas fa-fw fa-user"></i>
|
||||
|
|
@ -126,7 +126,7 @@
|
|||
tabindex="2"
|
||||
href="/login"
|
||||
onclick="this.blur();"
|
||||
data-link
|
||||
router-link
|
||||
>
|
||||
<div class="icon">
|
||||
<i class="far fa-fw fa-user"></i>
|
||||
|
|
|
|||
|
|
@ -292,6 +292,10 @@
|
|||
"name": "armenian",
|
||||
"languages": ["armenian_western", "armenian_western_1k"]
|
||||
},
|
||||
{
|
||||
"name": "myanmar",
|
||||
"languages": ["myanmar_burmese"]
|
||||
},
|
||||
{
|
||||
"name": "japanese",
|
||||
"languages": ["japanese_hiragana", "japanese_katakana"]
|
||||
|
|
|
|||
|
|
@ -170,6 +170,7 @@
|
|||
,"shona_1k"
|
||||
,"armenian_western"
|
||||
,"armenian_western_1k"
|
||||
,"myanmar_burmese"
|
||||
,"japanese_hiragana"
|
||||
,"japanese_katakana"
|
||||
,"sinhala"
|
||||
|
|
|
|||
|
|
@ -82,6 +82,7 @@
|
|||
"demikian",
|
||||
"dengan",
|
||||
"depan",
|
||||
"desa",
|
||||
"detik",
|
||||
"di",
|
||||
"dia",
|
||||
|
|
@ -89,6 +90,7 @@
|
|||
"dua",
|
||||
"dulu",
|
||||
"ganti",
|
||||
"gila",
|
||||
"habis",
|
||||
"hanya",
|
||||
"hari",
|
||||
|
|
@ -107,6 +109,7 @@
|
|||
"jarak",
|
||||
"jika",
|
||||
"jumlah",
|
||||
"jari",
|
||||
"kalah",
|
||||
"kalau",
|
||||
"kalimat",
|
||||
|
|
@ -123,6 +126,7 @@
|
|||
"kemarin",
|
||||
"kembali",
|
||||
"kemudian",
|
||||
"kemana",
|
||||
"keras",
|
||||
"kerja",
|
||||
"kertas",
|
||||
|
|
@ -154,6 +158,7 @@
|
|||
"membuat",
|
||||
"memerlukan",
|
||||
"memiliki",
|
||||
"mencuri",
|
||||
"menang",
|
||||
"mengapa",
|
||||
"mengatakan",
|
||||
|
|
@ -191,6 +196,7 @@
|
|||
"radius",
|
||||
"ragam",
|
||||
"ras",
|
||||
"rasa",
|
||||
"rasio",
|
||||
"rasional",
|
||||
"rayu",
|
||||
|
|
@ -261,6 +267,7 @@
|
|||
"tinggi",
|
||||
"tua",
|
||||
"tumbuh",
|
||||
"tunas",
|
||||
"tunggu",
|
||||
"turun",
|
||||
"tutup",
|
||||
|
|
@ -268,6 +275,7 @@
|
|||
"untuk",
|
||||
"wahai",
|
||||
"wajah",
|
||||
"wajar",
|
||||
"waktu",
|
||||
"wanita",
|
||||
"yaitu",
|
||||
|
|
|
|||
208
frontend/static/languages/myanmar_burmese.json
Normal file
208
frontend/static/languages/myanmar_burmese.json
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
{
|
||||
"name": "myanmar_burmese",
|
||||
"leftToRight": true,
|
||||
"noLazyMode": true,
|
||||
"bcp47": "my-MY",
|
||||
"words": [
|
||||
"အဆိုပါ",
|
||||
"ဖြစ်သည်",
|
||||
"ဝေဖန်",
|
||||
"နှင့်",
|
||||
"တစ်ခု",
|
||||
"အပြင်သို့",
|
||||
"အထဲသို့",
|
||||
"သူ",
|
||||
"တည်ရှိ",
|
||||
"ထိုအရာ",
|
||||
"ဝတ်ဆင်",
|
||||
"အတွက်",
|
||||
"သူတို့",
|
||||
"ကျွန်တော်",
|
||||
"နှင့်အတူ",
|
||||
"အဖက်လုပ်",
|
||||
"မဟုတ်ဘူး",
|
||||
"အရွဲ့တိုက်",
|
||||
"သတင်းစကား",
|
||||
"ထာဝရ",
|
||||
"ငလျင်လှုပ်",
|
||||
"အသေးစိတ်",
|
||||
"ကျွန်မ",
|
||||
"ခင်ဗျား",
|
||||
"ပုရွက်ဆိတ်",
|
||||
"ဒါပေမယ့်",
|
||||
"ပြည်သူ",
|
||||
"သံသရာ",
|
||||
"ချစ်ကျွမ်းဝင်",
|
||||
"တကယ်တမ်း",
|
||||
"ဖက်လဲတကင်း",
|
||||
"အားလုံး",
|
||||
"သေတမ်းစာ",
|
||||
"ဟိုနေရာမှာ",
|
||||
"ပြောဆို",
|
||||
"မုန့်ဟင်းခါး",
|
||||
"မုန်းတီး",
|
||||
"သောအခါ",
|
||||
"ဒယိမ်းဒယိုင်",
|
||||
"ဒလန်",
|
||||
"ဓားစာခံ",
|
||||
"ညီအစ်မ",
|
||||
"အမျိုးသား",
|
||||
"ပယောဂ",
|
||||
"အခြား",
|
||||
"ထို့နောက်",
|
||||
"ပစ်ခတ်",
|
||||
"အချိန်",
|
||||
"မှတစ်ပါး",
|
||||
"သွားလာ",
|
||||
"အကြောင်း",
|
||||
"နှောင်ကြိုး",
|
||||
"ဖက်ဆစ်",
|
||||
"တော်လှန်ရေး",
|
||||
"ပြည်နယ်",
|
||||
"ငြိမ်းချမ်းရေး",
|
||||
"အသစ်",
|
||||
"ခုနှစ်",
|
||||
"ဖောက်ပြန်",
|
||||
"ဘိလပ်မြေ",
|
||||
"ဆောင်ရွက်",
|
||||
"ရေတံခွန်",
|
||||
"လူရည်လည်",
|
||||
"အမြင်",
|
||||
"အသုံးပြု",
|
||||
"ရယူခြင်း",
|
||||
"ကြိုက်နှစ်သက်",
|
||||
"ထိုအခါ",
|
||||
"ပထမ",
|
||||
"တစ်ခုခု",
|
||||
"အလုပ်",
|
||||
"လောလောဆယ်",
|
||||
"မိခင်",
|
||||
"ဖခင်",
|
||||
"ပေးဝေ",
|
||||
"ပြီးဆုံး",
|
||||
"တွေးတော",
|
||||
"အများဆုံး",
|
||||
"စုစည်း",
|
||||
"ရှာဖွေ",
|
||||
"နေ့ရက်",
|
||||
"နှစ်ကာလ",
|
||||
"ပြီးနောက်",
|
||||
"နည်းလမ်း",
|
||||
"များစွာ",
|
||||
"မဖြစ်မနေ",
|
||||
"အသွင်အပြင်",
|
||||
"ဟင်းလျာ",
|
||||
"ကြီးမား",
|
||||
"လက်လုပ်လက်စား",
|
||||
"မှတဆင့်",
|
||||
"ရှည်လျား",
|
||||
"ဘယ်မှာ",
|
||||
"အများကြီး",
|
||||
"လုပ်သင့်လုပ်ထိုက်",
|
||||
"ရေတွင်း",
|
||||
"လူထု",
|
||||
"ဆင်းလျက်",
|
||||
"ကိုယ်ပိုင်",
|
||||
"အချစ်",
|
||||
"အဘယ်ကြောင့်",
|
||||
"ကောင်းတယ်",
|
||||
"ခါးသက်",
|
||||
"ကြည်လင်",
|
||||
"ခံစားမှု",
|
||||
"လည်းကောင်း",
|
||||
"ဘယ်လို",
|
||||
"မြင့်မားသော",
|
||||
"မြန်မာ",
|
||||
"နေရာ",
|
||||
"နည်းနည်း",
|
||||
"ကမ္ဘာ",
|
||||
"အလွန်",
|
||||
"ငြိမ်သက်",
|
||||
"နိုင်ငံ",
|
||||
"လက်",
|
||||
"အဟောင်း",
|
||||
"ဘဝ",
|
||||
"ဝန်ကြီးချုပ်",
|
||||
"စာအရေးအသား",
|
||||
"ဖြစ်လာသည်",
|
||||
"ဒီမှာ",
|
||||
"ပြဇာတ်",
|
||||
"စာစီစာကုံး",
|
||||
"လုံချည်",
|
||||
"အကြား",
|
||||
"လိုအပ်",
|
||||
"စိတ်လိုက်မာန်ပါ",
|
||||
"အလည်အပတ်",
|
||||
"ဖွံ့ဖြိုးတိုးတက်",
|
||||
"အောက်မေ့",
|
||||
"နောက်ဆုံး",
|
||||
"မှန်ကန်",
|
||||
"လှုပ်ရှား",
|
||||
"အရာရှိ",
|
||||
"အထွေထွေ",
|
||||
"ကျောင်း",
|
||||
"ဘယ်တော့မှ",
|
||||
"တူရိယာ",
|
||||
"အကြွင်းမဲ့",
|
||||
"အစပြု",
|
||||
"အချုပ်အခြာ",
|
||||
"နံပါတ်",
|
||||
"အစိတ်အပိုင်း",
|
||||
"အလှည့်",
|
||||
"အစစ်အမှန်",
|
||||
"ထွက်သွား",
|
||||
"တန်ခိုး",
|
||||
"အလိုရှိ",
|
||||
"အမှတ်",
|
||||
"ပုံစံ",
|
||||
"အချင်းများ",
|
||||
"ကလေး",
|
||||
"အနည်းငယ်",
|
||||
"သေးငယ်",
|
||||
"ကတည်းက",
|
||||
"ဆန့်ကျင်ဘက်",
|
||||
"မေးမြန်း",
|
||||
"နောက်ကျ",
|
||||
"အိမ်",
|
||||
"အကျိုးစီးပွား",
|
||||
"ကြီးကျယ်",
|
||||
"လူ",
|
||||
"ပိတ်သည်",
|
||||
"ဖွင့်သည်",
|
||||
"အများသူငှာ",
|
||||
"လိုက်နာ",
|
||||
"ကာလအတွင်း",
|
||||
"လက်ရှိ",
|
||||
"ကောင်းမွန်",
|
||||
"ထပ်မံ၍",
|
||||
"ကိုင်ဆုပ်",
|
||||
"အုပ်ချုပ်",
|
||||
"ပတ်ပတ်လည်",
|
||||
"ဖြစ်နိုင်သော",
|
||||
"ဦးခေါင်း",
|
||||
"စဉ်းစားဆင်ခြင်",
|
||||
"စကားလုံး",
|
||||
"ဆွဲဆောင်",
|
||||
"ပြဿနာ",
|
||||
"သို့သော်",
|
||||
"ခေါင်းဆောင်",
|
||||
"စနစ်",
|
||||
"သတ်မှတ်",
|
||||
"အမိန့်",
|
||||
"မျက်လုံး",
|
||||
"အစီအစဉ်",
|
||||
"ပြေးသည်",
|
||||
"စောင့်ရှောက်",
|
||||
"မျက်နှာ",
|
||||
"အချက်အလက်",
|
||||
"အစုအဖွဲ့",
|
||||
"ကစား",
|
||||
"ရပ်နေ",
|
||||
"တိုးမြှင့်",
|
||||
"စောစော",
|
||||
"သင်တန်း",
|
||||
"ပြောင်းလဲမှု",
|
||||
"အကူအညီ",
|
||||
"မျဉ်းကြောင်း"
|
||||
]
|
||||
}
|
||||
|
|
@ -1260,6 +1260,18 @@
|
|||
"source": "Wa Brontok, Hansip Sukrawetan Indramayu",
|
||||
"length": 209,
|
||||
"id": 210
|
||||
},
|
||||
{
|
||||
"text": "Saya manusia biasa, makan nasi.",
|
||||
"source": "Joko Widodo",
|
||||
"length": 31,
|
||||
"id": 211
|
||||
},
|
||||
{
|
||||
"text": "Kok kamu tanya begitu, siapa yang suruh?",
|
||||
"source": "Soeharto",
|
||||
"length": 40,
|
||||
"id": 212
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue