refactor: shared user types (#5080)

* move user to shared definitions

this includes whatever user can have on it, tags, presets and so on

* profiles, mail and others

* fix logic

* yeet

* same as master for now

* tsc fixes

* remove comment

* fix tests

* chore: omit ips

* fix(language): remove some unnecessarily capitalised words in german 1k

* fix(typing): first space sometimes soft locking the website

* perf: speed up settings page loading

* fix: use selected typing speed unit on personal best popup (fehmer) (#5070)

* fix: Use selected typing speed unit on personal best popup

* refactor

* refactor

* test coverage

* use Format in more places

* Make config mockable

* dependency injection

* wip

* fix

* test

* touch

* fix(language): typos in russian_10k.json (kae) (#5082)

* Update russian_10k.json

- fixed typos
- removed duplicates

* - fixed extra typos

* remove duplicates

* fix(language): typos in russian_10k.json

* feat: add copy missed words to result screen (fehmer) (#5086)

* feat: Add copy missed words to result screen

* remove margin

* update icons

---------

Co-authored-by: Miodec <jack@monkeytype.com>

* impr(funbox): add 46 group languages to wikipedia funbox (RealCyGuy) (#5078)

* impr: provide all-time LB results during LB update (fehmer) (#5074)

Try to provide LB results during the LB update. There is a very small time-frame where
already running queries might fail during the update. For now we keep the 503 error in this
cases and monitor how often this happens on production.

* impr(funbox): add ` (grave accent, 96) and ~ (tilde, 126) to specials (#5073)

* impr: add testWords and wordsHistory to copy result stats (#5085)

* feat: add testWords and wordsHistory to copy result stats

* fix

* add fe ts dep

---------

Co-authored-by: Christian Fehmer <fehmer@users.noreply.github.com>
Co-authored-by: Andrey Kuznetsov <akuznetsov@outlook.com>
Co-authored-by: Cyrus Yip <cyruscmyip1@gmail.com>
Co-authored-by: fitzsim <fitzsim@fitzsim.org>
This commit is contained in:
Jack 2024-02-19 17:15:15 +01:00 committed by GitHub
parent 06c50deb3a
commit 01790d8a3a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
41 changed files with 514 additions and 408 deletions

View file

@ -157,7 +157,7 @@ describe("LeaderboardsDal", () => {
});
});
function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) {
function expectedLbEntry(rank: number, user: MonkeyTypes.DBUser, time: string) {
const lbBest: SharedTypes.PersonalBest =
user.lbPersonalBests?.time[time].english;
@ -178,13 +178,13 @@ function expectedLbEntry(rank: number, user: MonkeyTypes.User, time: string) {
async function createUser(
lbPersonalBests?: MonkeyTypes.LbPersonalBests,
userProperties?: Partial<MonkeyTypes.User>
): Promise<MonkeyTypes.User> {
userProperties?: Partial<MonkeyTypes.DBUser>
): Promise<MonkeyTypes.DBUser> {
const uid = new ObjectId().toHexString();
await UserDal.addUser("User " + uid, uid + "@example.com", uid);
await DB.getDb()
?.collection<MonkeyTypes.User>("users")
?.collection<MonkeyTypes.DBUser>("users")
.updateOne(
{ uid },
{

View file

@ -15,7 +15,8 @@ async function createDummyData(
timestamp: number,
tag?: string
): Promise<void> {
const dummyUser: MonkeyTypes.User = {
const dummyUser: MonkeyTypes.DBUser = {
_id: new ObjectId(),
uid,
addedAt: 0,
email: "test@example.com",

View file

@ -316,8 +316,8 @@ export async function updateEmail(
}
function getRelevantUserInfo(
user: MonkeyTypes.User
): Partial<MonkeyTypes.User> {
user: MonkeyTypes.DBUser
): Partial<MonkeyTypes.DBUser> {
return _.omit(user, [
"bananas",
"lbPersonalBests",
@ -336,7 +336,7 @@ export async function getUser(
): Promise<MonkeyResponse> {
const { uid } = req.ctx.decodedToken;
let userInfo: MonkeyTypes.User;
let userInfo: MonkeyTypes.DBUser;
try {
userInfo = await UserDAL.getUser(uid, "get user");
} catch (e) {
@ -786,7 +786,7 @@ export async function getProfile(
details: profileDetails,
allTimeLbs: alltimelbs,
uid: user.uid,
};
} as SharedTypes.UserProfile;
return new MonkeyResponse("Profile retrieved", profileData);
}
@ -811,10 +811,13 @@ export async function updateProfile(
}
});
const profileDetailsUpdates: Partial<MonkeyTypes.UserProfileDetails> = {
const profileDetailsUpdates: Partial<SharedTypes.UserProfileDetails> = {
bio: sanitizeString(bio),
keyboard: sanitizeString(keyboard),
socialProfiles: _.mapValues(socialProfiles, sanitizeString),
socialProfiles: _.mapValues(
socialProfiles,
sanitizeString
) as SharedTypes.UserProfileDetails["socialProfiles"],
};
await UserDAL.updateProfile(uid, profileDetailsUpdates, user.inventory);

View file

@ -14,7 +14,10 @@ const router = Router();
const checkIfUserIsQuoteMod = checkUserPermissions({
criteria: (user) => {
return !!user.quoteMod;
return (
user.quoteMod === true ||
(typeof user.quoteMod === "string" && user.quoteMod !== "")
);
},
});

View file

@ -79,7 +79,7 @@ export async function update(
leaderboardUpdating[`${language}_${mode}_${mode2}`] = true;
const start1 = performance.now();
const lb = db
.collection<MonkeyTypes.User>("users")
.collection<MonkeyTypes.DBUser>("users")
.aggregate<SharedTypes.LeaderboardEntry>(
[
{

View file

@ -13,7 +13,7 @@ export async function addResult(
uid: string,
result: DBResult
): Promise<{ insertedId: ObjectId }> {
let user: MonkeyTypes.User | null = null;
let user: MonkeyTypes.DBUser | null = null;
try {
user = await getUser(uid, "add result");
} catch (e) {

View file

@ -16,15 +16,15 @@ type Result = Omit<
>;
// Export for use in tests
export const getUsersCollection = (): Collection<WithId<MonkeyTypes.User>> =>
db.collection<MonkeyTypes.User>("users");
export const getUsersCollection = (): Collection<MonkeyTypes.DBUser> =>
db.collection<MonkeyTypes.DBUser>("users");
export async function addUser(
name: string,
email: string,
uid: string
): Promise<void> {
const newUserDocument: Partial<MonkeyTypes.User> = {
const newUserDocument: Partial<MonkeyTypes.DBUser> = {
name,
email,
uid,
@ -170,7 +170,7 @@ export async function optOutOfLeaderboards(uid: string): Promise<void> {
export async function updateQuoteRatings(
uid: string,
quoteRatings: MonkeyTypes.UserQuoteRatings
quoteRatings: SharedTypes.UserQuoteRatings
): Promise<boolean> {
await getUser(uid, "update quote ratings");
@ -191,13 +191,15 @@ export async function updateEmail(
export async function getUser(
uid: string,
stack: string
): Promise<MonkeyTypes.User> {
): Promise<MonkeyTypes.DBUser> {
const user = await getUsersCollection().findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", stack);
return user;
}
async function findByName(name: string): Promise<MonkeyTypes.User | undefined> {
async function findByName(
name: string
): Promise<MonkeyTypes.DBUser | undefined> {
return (
await getUsersCollection()
.find({ name })
@ -220,7 +222,7 @@ export async function isNameAvailable(
export async function getUserByName(
name: string,
stack: string
): Promise<MonkeyTypes.User> {
): Promise<MonkeyTypes.DBUser> {
const user = await findByName(name);
if (!user) throw new MonkeyError(404, "User not found", stack);
return user;
@ -284,7 +286,7 @@ export async function removeResultFilterPreset(
export async function addTag(
uid: string,
name: string
): Promise<MonkeyTypes.UserTag> {
): Promise<MonkeyTypes.DBUserTag> {
const user = await getUser(uid, "add tag");
if ((user?.tags?.length ?? 0) >= 15) {
@ -315,7 +317,7 @@ export async function addTag(
return toPush;
}
export async function getTags(uid: string): Promise<MonkeyTypes.UserTag[]> {
export async function getTags(uid: string): Promise<MonkeyTypes.DBUserTag[]> {
const user = await getUser(uid, "get tags");
return user.tags ?? [];
@ -396,9 +398,11 @@ export async function updateLbMemory(
const user = await getUser(uid, "update lb memory");
if (user.lbMemory === undefined) user.lbMemory = {};
if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {};
if (user.lbMemory[mode][mode2] === undefined) {
if (user.lbMemory[mode]?.[mode2] === undefined) {
//@ts-expect-error guarded above
user.lbMemory[mode][mode2] = {};
}
//@ts-expect-error guarded above
user.lbMemory[mode][mode2][language] = rank;
await getUsersCollection().updateOne(
{ uid },
@ -410,7 +414,7 @@ export async function updateLbMemory(
export async function checkIfPb(
uid: string,
user: MonkeyTypes.User,
user: MonkeyTypes.DBUser,
result: Result
): Promise<boolean> {
const { mode } = result;
@ -452,7 +456,7 @@ export async function checkIfPb(
export async function checkIfTagPb(
uid: string,
user: MonkeyTypes.User,
user: MonkeyTypes.DBUser,
result: Result
): Promise<string[]> {
if (user.tags === undefined || user.tags.length === 0) {
@ -466,7 +470,7 @@ export async function checkIfTagPb(
return [];
}
const tagsToCheck: MonkeyTypes.UserTag[] = [];
const tagsToCheck: MonkeyTypes.DBUserTag[] = [];
user.tags.forEach((userTag) => {
for (const resultTag of resultTags ?? []) {
if (resultTag === userTag._id.toHexString()) {
@ -553,7 +557,7 @@ export async function linkDiscord(
discordId: string,
discordAvatar?: string
): Promise<void> {
const updates: Partial<MonkeyTypes.User> = _.pickBy(
const updates: Partial<MonkeyTypes.DBUser> = _.pickBy(
{ discordId, discordAvatar },
_.identity
);
@ -672,7 +676,7 @@ export async function editTheme(uid: string, _id, theme): Promise<void> {
export async function getThemes(
uid: string
): Promise<MonkeyTypes.CustomTheme[]> {
): Promise<MonkeyTypes.DBCustomTheme[]> {
const user = await getUser(uid, "get themes");
return user.customThemes ?? [];
}
@ -705,7 +709,7 @@ export async function getStats(
export async function getFavoriteQuotes(
uid
): Promise<MonkeyTypes.User["favoriteQuotes"]> {
): Promise<MonkeyTypes.DBUser["favoriteQuotes"]> {
const user = await getUser(uid, "get favorite quotes");
return user.favoriteQuotes ?? {};
@ -789,7 +793,7 @@ export async function recordAutoBanEvent(
recentAutoBanTimestamps.push(now);
//update user, ban if needed
const updateObj: Partial<MonkeyTypes.User> = {
const updateObj: Partial<MonkeyTypes.DBUser> = {
autoBanTimestamps: recentAutoBanTimestamps,
};
let banningUser = false;
@ -810,8 +814,8 @@ export async function recordAutoBanEvent(
export async function updateProfile(
uid: string,
profileDetailUpdates: Partial<MonkeyTypes.UserProfileDetails>,
inventory?: MonkeyTypes.UserInventory
profileDetailUpdates: Partial<SharedTypes.UserProfileDetails>,
inventory?: SharedTypes.UserInventory
): Promise<void> {
const profileUpdates = _.omitBy(
flattenObjectDeep(profileDetailUpdates, "profileDetails"),
@ -837,14 +841,14 @@ export async function updateProfile(
export async function getInbox(
uid: string
): Promise<MonkeyTypes.User["inbox"]> {
): Promise<MonkeyTypes.DBUser["inbox"]> {
const user = await getUser(uid, "get inventory");
return user.inbox ?? [];
}
type AddToInboxBulkEntry = {
uid: string;
mail: MonkeyTypes.MonkeyMail[];
mail: SharedTypes.MonkeyMail[];
};
export async function addToInboxBulk(
@ -876,7 +880,7 @@ export async function addToInboxBulk(
export async function addToInbox(
uid: string,
mail: MonkeyTypes.MonkeyMail[],
mail: SharedTypes.MonkeyMail[],
inboxConfig: SharedTypes.Configuration["users"]["inbox"]
): Promise<void> {
const { enabled, maxMail } = inboxConfig;
@ -902,11 +906,11 @@ export async function addToInbox(
}
function buildRewardUpdates(
rewards: MonkeyTypes.AllRewards[],
rewards: SharedTypes.AllRewards[],
inventoryIsNull = false
): UpdateFilter<WithId<MonkeyTypes.User>> {
): UpdateFilter<MonkeyTypes.DBUser> {
let totalXp = 0;
const newBadges: MonkeyTypes.Badge[] = [];
const newBadges: SharedTypes.Badge[] = [];
rewards.forEach((reward) => {
if (reward.type === "xp") {
@ -954,7 +958,7 @@ export async function updateInbox(
const mailToReadSet = new Set(mailToRead);
const mailToDeleteSet = new Set(mailToDelete);
const allRewards: MonkeyTypes.AllRewards[] = [];
const allRewards: SharedTypes.AllRewards[] = [];
const newInbox = inbox
.filter((mail) => {
@ -988,7 +992,7 @@ export async function updateStreak(
timestamp: number
): Promise<number> {
const user = await getUser(uid, "calculate streak");
const streak: MonkeyTypes.UserStreak = {
const streak: SharedTypes.UserStreak = {
lastResultTimestamp: user.streak?.lastResultTimestamp ?? 0,
length: user.streak?.length ?? 0,
maxLength: user.streak?.maxLength ?? 0,
@ -1043,7 +1047,7 @@ export async function setBanned(uid: string, banned: boolean): Promise<void> {
export async function checkIfUserIsPremium(
uid: string,
userInfoOverride?: MonkeyTypes.User
userInfoOverride?: MonkeyTypes.DBUser
): Promise<boolean> {
const user = userInfoOverride ?? (await getUser(uid, "checkIfUserIsPremium"));
const expirationDate = user.premium?.expirationTimestamp;
@ -1056,7 +1060,7 @@ export async function checkIfUserIsPremium(
export async function logIpAddress(
uid: string,
ip: string,
userInfoOverride?: MonkeyTypes.User
userInfoOverride?: MonkeyTypes.DBUser
): Promise<void> {
const user = userInfoOverride ?? (await getUser(uid, "logIpAddress"));
const currentIps = user.ips ?? [];

View file

@ -72,7 +72,7 @@ function checkIfUserIsAdmin(): RequestHandler {
* Note that this middleware must be used after authentication in the middleware stack.
*/
function checkUserPermissions(
options: ValidationOptions<MonkeyTypes.User>
options: ValidationOptions<MonkeyTypes.DBUser>
): RequestHandler {
const { criteria, invalidMessage = "You don't have permission to do this." } =
options;

View file

@ -18,103 +18,27 @@ declare namespace MonkeyTypes {
ctx: Readonly<Context>;
} & ExpressRequest;
// Data Model
type UserProfileDetails = {
bio?: string;
keyboard?: string;
socialProfiles: {
twitter?: string;
github?: string;
website?: string;
};
};
type Reward<T> = {
type: string;
item: T;
};
type XpReward = {
type: "xp";
item: number;
} & Reward<number>;
type BadgeReward = {
type: "badge";
item: Badge;
} & Reward<Badge>;
type AllRewards = XpReward | BadgeReward;
type MonkeyMail = {
id: string;
subject: string;
body: string;
timestamp: number;
read: boolean;
rewards: AllRewards[];
};
type UserIpHistory = string[];
type User = {
autoBanTimestamps?: number[];
addedAt: number;
verified?: boolean;
bananas?: number;
completedTests?: number;
discordId?: string;
email: string;
lastNameChange?: number;
lbMemory?: object;
lbPersonalBests?: LbPersonalBests;
name: string;
customThemes?: CustomTheme[];
personalBests: SharedTypes.PersonalBests;
quoteRatings?: UserQuoteRatings;
startedTests?: number;
tags?: UserTag[];
timeTyping?: number;
uid: string;
quoteMod?: boolean;
configurationMod?: boolean;
admin?: boolean;
canReport?: boolean;
banned?: boolean;
canManageApeKeys?: boolean;
favoriteQuotes?: Record<string, string[]>;
needsToChangeName?: boolean;
discordAvatar?: string;
type DBUser = Omit<
SharedTypes.User,
"resultFilterPresets" | "tags" | "customThemes"
> & {
_id: ObjectId;
resultFilterPresets?: WithObjectIdArray<SharedTypes.ResultFilters[]>;
profileDetails?: UserProfileDetails;
inventory?: UserInventory;
xp?: number;
inbox?: MonkeyMail[];
streak?: UserStreak;
lastReultHashes?: string[];
lbOptOut?: boolean;
premium?: PremiumInfo;
ips?: UserIpHistory;
tags?: DBUserTag[];
lbPersonalBests?: LbPersonalBests;
customThemes?: DBCustomTheme[];
autoBanTimestamps?: number[];
inbox?: SharedTypes.MonkeyMail[];
ips?: string[];
canReport?: boolean;
lastNameChange?: number;
canManageApeKeys?: boolean;
bananas?: number;
};
type UserStreak = {
lastResultTimestamp: number;
length: number;
maxLength: number;
hourOffset?: number;
};
type DBCustomTheme = WithObjectId<SharedTypes.CustomTheme>;
type UserInventory = {
badges: Badge[];
};
type Badge = {
id: number;
selected?: boolean;
};
type UserQuoteRatings = Record<string, Record<string, number>>;
type DBUserTag = WithObjectId<SharedTypes.UserTag>;
type LbPersonalBests = {
time: Record<number, Record<string, SharedTypes.PersonalBest>>;
@ -129,18 +53,6 @@ declare namespace MonkeyTypes {
_id: ObjectId;
}[];
type UserTag = {
_id: ObjectId;
name: string;
personalBests: SharedTypes.PersonalBests;
};
type CustomTheme = {
_id: ObjectId;
name: string;
colors: string[];
};
type ApeKeyDB = SharedTypes.ApeKey & {
_id: ObjectId;
uid: string;
@ -188,9 +100,4 @@ declare namespace MonkeyTypes {
frontendForcedConfig?: Record<string, string[] | boolean[]>;
frontendFunctions?: string[];
};
type PremiumInfo = {
startTimestamp: number;
expirationTimestamp: number;
};
}

View file

@ -1,10 +1,10 @@
import { v4 } from "uuid";
type MonkeyMailOptions = Partial<Omit<MonkeyTypes.MonkeyMail, "id" | "read">>;
type MonkeyMailOptions = Partial<Omit<SharedTypes.MonkeyMail, "id" | "read">>;
export function buildMonkeyMail(
options: MonkeyMailOptions
): MonkeyTypes.MonkeyMail {
): SharedTypes.MonkeyMail {
return {
id: v4(),
subject: options.subject ?? "",

View file

@ -43,7 +43,7 @@ async function handleDailyLeaderboardResults(
if (inboxConfig.enabled && xpRewardBrackets.length > 0) {
const mailEntries: {
uid: string;
mail: MonkeyTypes.MonkeyMail[];
mail: SharedTypes.MonkeyMail[];
}[] = [];
allResults.forEach((entry) => {
@ -132,7 +132,7 @@ async function handleWeeklyXpLeaderboardResults(
const mailEntries: {
uid: string;
mail: MonkeyTypes.MonkeyMail[];
mail: SharedTypes.MonkeyMail[];
}[] = [];
allResults.forEach((entry) => {

View file

@ -80,6 +80,7 @@
"ts-jest": "29.1.2",
"ts-loader": "9.2.6",
"ts-node-dev": "2.0.0",
"typescript": "5.3.3",
"webpack": "5.72.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0",
@ -21347,17 +21348,16 @@
"dev": true
},
"node_modules/typescript": {
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",
"integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=4.2.0"
"node": ">=14.17"
}
},
"node_modules/unbox-primitive": {

View file

@ -69,6 +69,7 @@
"ts-jest": "29.1.2",
"ts-loader": "9.2.6",
"ts-node-dev": "2.0.0",
"typescript": "5.3.3",
"webpack": "5.72.0",
"webpack-bundle-analyzer": "4.5.0",
"webpack-cli": "4.10.0",

View file

@ -217,7 +217,7 @@ async function createFilterPresetCallback(name: string): Promise<void> {
const result = await Ape.users.addResultFilterPreset({ ...filters, name });
Loader.hide();
if (result.status === 200) {
addFilterPresetToSnapshot({ ...filters, name, _id: result.data });
addFilterPresetToSnapshot({ ...filters, name, _id: result.data as string });
void updateFilterPresets();
Notifications.add("Filter preset created", 1);
} else {
@ -299,7 +299,7 @@ function setAllFilters(
});
}
export function loadTags(tags: MonkeyTypes.Tag[]): void {
export function loadTags(tags: MonkeyTypes.UserTag[]): void {
tags.forEach((tag) => {
defaultResultFilters.tags[tag._id] = true;
});

View file

@ -5,7 +5,7 @@ export default class Users {
this.httpClient = httpClient;
}
async getData(): Ape.EndpointResponse {
async getData(): Ape.EndpointResponse<Ape.Users.GetUser> {
return await this.httpClient.get(BASE_PATH);
}
@ -14,7 +14,7 @@ export default class Users {
captcha: string,
email?: string,
uid?: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = {
email,
name,
@ -25,23 +25,23 @@ export default class Users {
return await this.httpClient.post(`${BASE_PATH}/signup`, { payload });
}
async getNameAvailability(name: string): Ape.EndpointResponse {
async getNameAvailability(name: string): Ape.EndpointResponse<null> {
return await this.httpClient.get(`${BASE_PATH}/checkName/${name}`);
}
async delete(): Ape.EndpointResponse {
async delete(): Ape.EndpointResponse<null> {
return await this.httpClient.delete(BASE_PATH);
}
async reset(): Ape.EndpointResponse {
async reset(): Ape.EndpointResponse<null> {
return await this.httpClient.patch(`${BASE_PATH}/reset`);
}
async optOutOfLeaderboards(): Ape.EndpointResponse {
async optOutOfLeaderboards(): Ape.EndpointResponse<null> {
return await this.httpClient.post(`${BASE_PATH}/optOutOfLeaderboards`);
}
async updateName(name: string): Ape.EndpointResponse {
async updateName(name: string): Ape.EndpointResponse<null> {
return await this.httpClient.patch(`${BASE_PATH}/name`, {
payload: { name },
});
@ -52,7 +52,7 @@ export default class Users {
mode2: SharedTypes.Config.Mode2<M>,
language: string,
rank: number
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = {
mode,
mode2,
@ -68,7 +68,7 @@ export default class Users {
async updateEmail(
newEmail: string,
previousEmail: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = {
newEmail,
previousEmail,
@ -77,31 +77,31 @@ export default class Users {
return await this.httpClient.patch(`${BASE_PATH}/email`, { payload });
}
async deletePersonalBests(): Ape.EndpointResponse {
async deletePersonalBests(): Ape.EndpointResponse<null> {
return await this.httpClient.delete(`${BASE_PATH}/personalBests`);
}
async addResultFilterPreset(
filter: SharedTypes.ResultFilters
): Ape.EndpointResponse {
): Ape.EndpointResponse<string> {
return await this.httpClient.post(`${BASE_PATH}/resultFilterPresets`, {
payload: filter,
});
}
async removeResultFilterPreset(id: string): Ape.EndpointResponse {
async removeResultFilterPreset(id: string): Ape.EndpointResponse<null> {
return await this.httpClient.delete(
`${BASE_PATH}/resultFilterPresets/${id}`
);
}
async createTag(tagName: string): Ape.EndpointResponse {
async createTag(tagName: string): Ape.EndpointResponse<SharedTypes.UserTag> {
return await this.httpClient.post(`${BASE_PATH}/tags`, {
payload: { tagName },
});
}
async editTag(tagId: string, newName: string): Ape.EndpointResponse {
async editTag(tagId: string, newName: string): Ape.EndpointResponse<null> {
const payload = {
tagId,
newName,
@ -110,24 +110,24 @@ export default class Users {
return await this.httpClient.patch(`${BASE_PATH}/tags`, { payload });
}
async deleteTag(tagId: string): Ape.EndpointResponse {
async deleteTag(tagId: string): Ape.EndpointResponse<null> {
return await this.httpClient.delete(`${BASE_PATH}/tags/${tagId}`);
}
async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse {
async deleteTagPersonalBest(tagId: string): Ape.EndpointResponse<null> {
return await this.httpClient.delete(
`${BASE_PATH}/tags/${tagId}/personalBest`
);
}
async getCustomThemes(): Ape.EndpointResponse {
async getCustomThemes(): Ape.EndpointResponse<SharedTypes.CustomTheme[]> {
return await this.httpClient.get(`${BASE_PATH}/customThemes`);
}
async editCustomTheme(
themeId: string,
newTheme: Partial<MonkeyTypes.CustomTheme>
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = {
themeId: themeId,
theme: {
@ -140,7 +140,7 @@ export default class Users {
});
}
async deleteCustomTheme(themeId: string): Ape.EndpointResponse {
async deleteCustomTheme(themeId: string): Ape.EndpointResponse<null> {
const payload = {
themeId: themeId,
};
@ -151,12 +151,12 @@ export default class Users {
async addCustomTheme(
newTheme: Partial<MonkeyTypes.CustomTheme>
): Ape.EndpointResponse {
): Ape.EndpointResponse<SharedTypes.CustomTheme> {
const payload = { name: newTheme.name, colors: newTheme.colors };
return await this.httpClient.post(`${BASE_PATH}/customThemes`, { payload });
}
async getOauthLink(): Ape.EndpointResponse {
async getOauthLink(): Ape.EndpointResponse<Ape.Users.GetOauthLink> {
return await this.httpClient.get(`${BASE_PATH}/discord/oauth`);
}
@ -164,20 +164,20 @@ export default class Users {
tokenType: string,
accessToken: string,
state: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<Ape.Users.LinkDiscord> {
return await this.httpClient.post(`${BASE_PATH}/discord/link`, {
payload: { tokenType, accessToken, state },
});
}
async unlinkDiscord(): Ape.EndpointResponse {
async unlinkDiscord(): Ape.EndpointResponse<null> {
return await this.httpClient.post(`${BASE_PATH}/discord/unlink`);
}
async addQuoteToFavorites(
language: string,
quoteId: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = { language, quoteId };
return await this.httpClient.post(`${BASE_PATH}/favoriteQuotes`, {
payload,
@ -187,25 +187,29 @@ export default class Users {
async removeQuoteFromFavorites(
language: string,
quoteId: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = { language, quoteId };
return await this.httpClient.delete(`${BASE_PATH}/favoriteQuotes`, {
payload,
});
}
async getProfileByUid(uid: string): Promise<Ape.EndpointResponse> {
async getProfileByUid(
uid: string
): Ape.EndpointResponse<SharedTypes.UserProfile> {
return await this.httpClient.get(`${BASE_PATH}/${uid}/profile?isUid`);
}
async getProfileByName(name: string): Promise<Ape.EndpointResponse> {
async getProfileByName(
name: string
): Ape.EndpointResponse<SharedTypes.UserProfile> {
return await this.httpClient.get(`${BASE_PATH}/${name}/profile`);
}
async updateProfile(
profileUpdates: Partial<MonkeyTypes.UserDetails>,
profileUpdates: Partial<SharedTypes.UserProfileDetails>,
selectedBadgeId?: number
): Promise<Ape.EndpointResponse> {
): Ape.EndpointResponse<null> {
return await this.httpClient.patch(`${BASE_PATH}/profile`, {
payload: {
...profileUpdates,
@ -214,14 +218,14 @@ export default class Users {
});
}
async getInbox(): Promise<Ape.EndpointResponse> {
async getInbox(): Ape.EndpointResponse<Ape.Users.GetInbox> {
return await this.httpClient.get(`${BASE_PATH}/inbox`);
}
async updateInbox(options: {
mailIdsToDelete?: string[];
mailIdsToMarkRead?: string[];
}): Promise<Ape.EndpointResponse> {
}): Ape.EndpointResponse<null> {
const payload = {
mailIdsToDelete: options.mailIdsToDelete,
mailIdsToMarkRead: options.mailIdsToMarkRead,
@ -234,7 +238,7 @@ export default class Users {
reason: string,
comment: string,
captcha: string
): Ape.EndpointResponse {
): Ape.EndpointResponse<null> {
const payload = {
uid,
reason,
@ -245,23 +249,23 @@ export default class Users {
return await this.httpClient.post(`${BASE_PATH}/report`, { payload });
}
async verificationEmail(): Ape.EndpointResponse {
async verificationEmail(): Ape.EndpointResponse<null> {
return await this.httpClient.get(`${BASE_PATH}/verificationEmail`);
}
async forgotPasswordEmail(email: string): Ape.EndpointResponse {
async forgotPasswordEmail(email: string): Ape.EndpointResponse<null> {
return await this.httpClient.post(`${BASE_PATH}/forgotPasswordEmail`, {
payload: { email },
});
}
async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse {
async setStreakHourOffset(hourOffset: number): Ape.EndpointResponse<null> {
return await this.httpClient.post(`${BASE_PATH}/setStreakHourOffset`, {
payload: { hourOffset },
});
}
async revokeAllTokens(): Ape.EndpointResponse {
async revokeAllTokens(): Ape.EndpointResponse<null> {
return await this.httpClient.post(`${BASE_PATH}/revokeAllTokens`);
}
}

View file

@ -26,8 +26,7 @@ declare namespace Ape {
data: TData | null;
};
// todo: remove any after all ape endpoints are typed
type EndpointResponse<TData = any> = Promise<HttpClientResponse<TData>>;
type EndpointResponse<TData> = Promise<HttpClientResponse<TData>>;
type HttpClient = {
get: HttpClientMethod;

18
frontend/src/ts/ape/types/users.d.ts vendored Normal file
View file

@ -0,0 +1,18 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
// for some reason when using the dot notaion, the types are not being recognized as used
declare namespace Ape.Users {
type GetUser = SharedTypes.User & {
inboxUnreadSize: number;
isPremium: boolean;
};
type GetOauthLink = {
url: string;
};
type LinkDiscord = {
discordId: string;
discordAvatar: string;
};
type GetInbox = {
inbox: SharedTypes.MonkeyMail[] | undefined;
};
}

View file

@ -22,7 +22,7 @@ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
export async function withRetry<ResponseDataType>(
fn: () => Ape.EndpointResponse<ResponseDataType>,
opts?: RetryOptions<ResponseDataType>
): Ape.EndpointResponse {
): Ape.EndpointResponse<ResponseDataType> {
const retry = async (
previousData: Ape.HttpClientResponse<ResponseDataType>,
completeOpts: Required<RetryOptions<ResponseDataType>>

View file

@ -805,7 +805,7 @@ $("footer").on("click", ".leftright .right .current-theme", (e) => {
if (e.shiftKey) {
if (!Config.customTheme) {
if (isAuthenticated()) {
if ((DB.getSnapshot()?.customThemes.length ?? 0) < 1) {
if ((DB.getSnapshot()?.customThemes?.length ?? 0) < 1) {
Notifications.add("No custom themes!", 0);
UpdateConfig.setCustomTheme(false);
// UpdateConfig.setCustomThemeId("");

View file

@ -33,7 +33,11 @@ export function update(): void {
if (!snapshot) return;
if (snapshot.customThemes.length === 0) {
if (snapshot.customThemes === undefined) {
return;
}
if (snapshot.customThemes?.length === 0) {
return;
}
snapshot.customThemes.forEach((theme) => {

View file

@ -1440,7 +1440,7 @@ export function setRandomTheme(
return false;
}
if (!DB.getSnapshot()) return true;
if (DB.getSnapshot()?.customThemes.length === 0) {
if (DB.getSnapshot()?.customThemes?.length === 0) {
Notifications.add("You need to create a custom theme first", 0);
config.randomTheme = "off";
return false;

View file

@ -1,3 +1,5 @@
import defaultConfig from "./default-config";
export const defaultSnap: MonkeyTypes.Snapshot = {
results: undefined,
personalBests: {
@ -8,13 +10,15 @@ export const defaultSnap: MonkeyTypes.Snapshot = {
custom: {},
},
name: "",
email: "",
uid: "",
isPremium: false,
config: defaultConfig,
customThemes: [],
presets: [],
tags: [],
favouriteThemes: [],
banned: undefined,
verified: undefined,
emailVerified: undefined,
lbMemory: { time: { 15: { english: 0 }, 60: { english: 0 } } },
typingStats: {
timeTyping: 0,

View file

@ -153,10 +153,7 @@ async function getDataAndInit(): Promise<boolean> {
const areConfigsEqual =
JSON.stringify(Config) === JSON.stringify(snapshot.config);
if (
snapshot.config &&
(UpdateConfig.localStorageConfig === undefined || !areConfigsEqual)
) {
if (UpdateConfig.localStorageConfig === undefined || !areConfigsEqual) {
console.log(
"no local config or local and db configs are different - applying db"
);

View file

@ -164,6 +164,10 @@ class QuotesController {
const quoteIds: string[] = [];
const { favoriteQuotes } = snapshot;
if (favoriteQuotes === undefined) {
return null;
}
Object.keys(favoriteQuotes).forEach((language) => {
if (removeLanguageSize(language) !== normalizedLanguage) {
return;
@ -190,6 +194,10 @@ class QuotesController {
const { favoriteQuotes } = snapshot;
if (favoriteQuotes === undefined) {
return false;
}
const normalizedQuoteLanguage = removeLanguageSize(quoteLanguage);
const matchedLanguage = Object.keys(favoriteQuotes).find((language) => {

View file

@ -262,7 +262,7 @@ async function changeThemeList(): Promise<void> {
return t.name;
});
} else if (Config.randomTheme === "custom" && DB.getSnapshot()) {
themesList = DB.getSnapshot()?.customThemes.map((ct) => ct._id) ?? [];
themesList = DB.getSnapshot()?.customThemes?.map((ct) => ct._id) ?? [];
}
Misc.shuffle(themesList);
randomThemeIndex = 0;
@ -284,7 +284,7 @@ export async function randomizeTheme(): Promise<void> {
let colorsOverride: string[] | undefined;
if (Config.randomTheme === "custom") {
const theme = DB.getSnapshot()?.customThemes.find(
const theme = DB.getSnapshot()?.customThemes?.find(
(ct) => ct._id === randomTheme
);
colorsOverride = theme?.colors;
@ -297,7 +297,7 @@ export async function randomizeTheme(): Promise<void> {
let name = randomTheme.replace(/_/g, " ");
if (Config.randomTheme === "custom") {
name = (
DB.getSnapshot()?.customThemes.find((ct) => ct._id === randomTheme)
DB.getSnapshot()?.customThemes?.find((ct) => ct._id === randomTheme)
?.name ?? "custom"
).replace(/_/g, " ");
}

View file

@ -84,10 +84,17 @@ export async function initSnapshot(): Promise<
};
}
const userData = userResponse.data;
const configData = configResponse.data;
const presetsData = presetsResponse.data;
const [userData] = [userResponse].map((response) => response.data);
if (userData === null) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw {
message: "Request was successful but user data is null",
responseCode: 200,
};
}
snap.name = userData.name;
snap.personalBests = userData.personalBests;
@ -110,15 +117,13 @@ export async function initSnapshot(): Promise<
snap.discordAvatar = userData.discordAvatar;
snap.needsToChangeName = userData.needsToChangeName;
snap.typingStats = {
timeTyping: userData.timeTyping,
startedTests: userData.startedTests,
completedTests: userData.completedTests,
timeTyping: userData.timeTyping ?? 0,
startedTests: userData.startedTests ?? 0,
completedTests: userData.completedTests ?? 0,
};
snap.quoteMod = userData.quoteMod;
snap.favoriteQuotes = userData.favoriteQuotes ?? {};
snap.quoteRatings = userData.quoteRatings;
snap.favouriteThemes =
userData.favouriteThemes === undefined ? [] : userData.favouriteThemes;
snap.details = userData.profileDetails;
snap.addedAt = userData.addedAt;
snap.inventory = userData.inventory;
@ -133,13 +138,7 @@ export async function initSnapshot(): Promise<
snap.streakHourOffset =
hourOffset === undefined || hourOffset === null ? undefined : hourOffset;
if (
userData.lbMemory?.time15 !== undefined ||
userData.lbMemory?.time60 !== undefined
) {
//old memory format
snap.lbMemory = {} as MonkeyTypes.LeaderboardMemory;
} else if (userData.lbMemory !== undefined) {
if (userData.lbMemory !== undefined) {
snap.lbMemory = userData.lbMemory;
}
// if (ActivePage.get() === "loading") {
@ -163,24 +162,43 @@ export async function initSnapshot(): Promise<
// LoadingPage.updateText("Downloading tags...");
snap.customThemes = userData.customThemes ?? [];
const userDataTags: MonkeyTypes.Tag[] = userData.tags ?? [];
// const userDataTags: MonkeyTypes.UserTagWithDisplay[] = userData.tags ?? [];
userDataTags.forEach((tag) => {
tag.display = tag.name.replaceAll("_", " ");
tag.personalBests ??= {
time: {},
words: {},
quote: {},
zen: {},
custom: {},
};
// userDataTags.forEach((tag) => {
// tag.display = tag.name.replaceAll("_", " ");
// tag.personalBests ??= {
// time: {},
// words: {},
// quote: {},
// zen: {},
// custom: {},
// };
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
}
});
// for (const mode of ["time", "words", "quote", "zen", "custom"]) {
// tag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
// }
// });
snap.tags = userDataTags;
// snap.tags = userDataTags;
snap.tags =
userData.tags?.map((tag) => {
const newTag = {
...tag,
display: tag.name.replaceAll("_", " "),
personalBests: {
time: {},
words: {},
quote: {},
zen: {},
custom: {},
},
};
for (const mode of ["time", "words", "quote", "zen", "custom"]) {
newTag.personalBests[mode as keyof SharedTypes.PersonalBests] ??= {};
}
return newTag;
}) ?? [];
snap.tags = snap.tags?.sort((a, b) => {
if (a.name > b.name) {
@ -304,7 +322,7 @@ export async function getUserResults(offset?: number): Promise<boolean> {
function _getCustomThemeById(
themeID: string
): MonkeyTypes.CustomTheme | undefined {
return dbSnapshot?.customThemes.find((t) => t._id === themeID);
return dbSnapshot?.customThemes?.find((t) => t._id === themeID);
}
export async function addCustomTheme(
@ -312,6 +330,10 @@ export async function addCustomTheme(
): Promise<boolean> {
if (!dbSnapshot) return false;
if (dbSnapshot.customThemes === undefined) {
dbSnapshot.customThemes = [];
}
if (dbSnapshot.customThemes.length >= 10) {
Notifications.add("Too many custom themes!", 0);
return false;
@ -323,9 +345,14 @@ export async function addCustomTheme(
return false;
}
if (response.data === null) {
Notifications.add("Error adding custom theme: No data returned", -1);
return false;
}
const newCustomTheme: MonkeyTypes.CustomTheme = {
...theme,
_id: response.data.theme._id as string,
_id: response.data._id as string,
};
dbSnapshot.customThemes.push(newCustomTheme);
@ -339,7 +366,11 @@ export async function editCustomTheme(
if (!isAuthenticated()) return false;
if (!dbSnapshot) return false;
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
if (dbSnapshot.customThemes === undefined) {
dbSnapshot.customThemes = [];
}
const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId);
if (!customTheme) {
Notifications.add(
"Editing failed: Custom theme with id: " + themeId + " does not exist",
@ -369,7 +400,7 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
if (!isAuthenticated()) return false;
if (!dbSnapshot) return false;
const customTheme = dbSnapshot.customThemes.find((t) => t._id === themeId);
const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId);
if (!customTheme) return false;
const response = await Ape.users.deleteCustomTheme(themeId);
@ -378,7 +409,7 @@ export async function deleteCustomTheme(themeId: string): Promise<boolean> {
return false;
}
dbSnapshot.customThemes = dbSnapshot.customThemes.filter(
dbSnapshot.customThemes = dbSnapshot.customThemes?.filter(
(t) => t._id !== themeId
);
@ -762,7 +793,7 @@ export async function saveLocalTagPB<M extends SharedTypes.Config.Mode>(
function cont(): void {
const filteredtag = dbSnapshot?.tags?.filter(
(t) => t._id === tagId
)[0] as MonkeyTypes.Tag;
)[0] as MonkeyTypes.UserTag;
filteredtag.personalBests ??= {
time: {},
@ -879,8 +910,14 @@ export async function updateLbMemory<M extends SharedTypes.Config.Mode>(
if (snapshot.lbMemory[timeMode][timeMode2] === undefined) {
snapshot.lbMemory[timeMode][timeMode2] = {};
}
const current = snapshot.lbMemory[timeMode][timeMode2][language];
snapshot.lbMemory[timeMode][timeMode2][language] = rank;
const current = snapshot.lbMemory?.[timeMode]?.[timeMode2]?.[language];
//this is protected above so not sure why it would be undefined
const mem = snapshot.lbMemory[timeMode][timeMode2] as Record<
string,
number
>;
mem[language] = rank;
if (api && current !== rank) {
await Ape.users.updateLeaderboardMemory(mode, mode2, language, rank);
}
@ -914,26 +951,17 @@ export function updateLocalStats(started: number, time: number): void {
const snapshot = getSnapshot();
if (!snapshot) return;
if (snapshot.typingStats === undefined) {
snapshot.typingStats = {} as MonkeyTypes.TypingStats;
}
if (snapshot?.typingStats !== undefined) {
if (snapshot.typingStats.timeTyping === undefined) {
snapshot.typingStats.timeTyping = time;
} else {
snapshot.typingStats.timeTyping += time;
}
if (snapshot.typingStats.startedTests === undefined) {
snapshot.typingStats.startedTests = started;
} else {
snapshot.typingStats.startedTests += started;
}
if (snapshot.typingStats.completedTests === undefined) {
snapshot.typingStats.completedTests = 1;
} else {
snapshot.typingStats.completedTests += 1;
}
snapshot.typingStats = {
timeTyping: 0,
startedTests: 0,
completedTests: 0,
};
}
snapshot.typingStats.timeTyping += time;
snapshot.typingStats.startedTests += started;
snapshot.typingStats.completedTests += 1;
setSnapshot(snapshot);
}
@ -948,7 +976,7 @@ export function addXp(xp: number): void {
setSnapshot(snapshot);
}
export function addBadge(badge: MonkeyTypes.Badge): void {
export function addBadge(badge: SharedTypes.Badge): void {
const snapshot = getSnapshot();
if (!snapshot) return;

View file

@ -251,7 +251,7 @@ function checkLbMemory(lb: LbKey): void {
side = "right";
}
const memory = DB.getSnapshot()?.lbMemory?.time?.[lb]?.["english"] ?? 0;
const memory = DB.getSnapshot()?.lbMemory?.["time"]?.[lb]?.["english"] ?? 0;
const rank = currentRank[lb]?.rank;
if (rank) {

View file

@ -9,15 +9,13 @@ import * as ActivePage from "../states/active-page";
import formatDistanceToNowStrict from "date-fns/formatDistanceToNowStrict";
type ProfileViewPaths = "profile" | "account";
type UserProfileOrSnapshot = SharedTypes.UserProfile | MonkeyTypes.Snapshot;
export type ProfileData = {
allTimeLbs: MonkeyTypes.LeaderboardMemory;
uid: string;
} & MonkeyTypes.Snapshot;
//this is probably the dirtiest code ive ever written
export async function update(
where: ProfileViewPaths,
profile: Partial<ProfileData>
profile: UserProfileOrSnapshot
): Promise<void> {
const elementClass = where.charAt(0).toUpperCase() + where.slice(1);
const profileElement = $(`.page${elementClass} .profile`);
@ -140,10 +138,11 @@ export async function update(
const results = DB.getSnapshot()?.results;
const lastResult = results?.[0];
const streakOffset = (profile as MonkeyTypes.Snapshot).streakHourOffset;
const dayInMilis = 1000 * 60 * 60 * 24;
let target =
Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis;
let target = Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis;
if (target < Date.now()) {
target += dayInMilis;
}
@ -154,9 +153,7 @@ export async function update(
console.debug("dayInMilis", dayInMilis);
console.debug(
"difTarget",
new Date(
Misc.getCurrentDayTimestamp(profile.streakHourOffset) + dayInMilis
)
new Date(Misc.getCurrentDayTimestamp(streakOffset) + dayInMilis)
);
console.debug("timeDif", timeDif);
console.debug(
@ -164,18 +161,12 @@ export async function update(
Misc.getCurrentDayTimestamp(),
new Date(Misc.getCurrentDayTimestamp())
);
console.debug("profile.streakHourOffset", profile.streakHourOffset);
console.debug("profile.streakHourOffset", streakOffset);
if (lastResult) {
//check if the last result is from today
const isToday = Misc.isToday(
lastResult.timestamp,
profile.streakHourOffset
);
const isYesterday = Misc.isYesterday(
lastResult.timestamp,
profile.streakHourOffset
);
const isToday = Misc.isToday(lastResult.timestamp, streakOffset);
const isYesterday = Misc.isYesterday(lastResult.timestamp, streakOffset);
console.debug(
"lastResult.timestamp",
@ -185,10 +176,8 @@ export async function update(
console.debug("isToday", isToday);
console.debug("isYesterday", isYesterday);
const offsetString = profile.streakHourOffset
? `(${profile.streakHourOffset > 0 ? "+" : ""}${
profile.streakHourOffset
} offset)`
const offsetString = streakOffset
? `(${streakOffset > 0 ? "+" : ""}${streakOffset} offset)`
: "";
if (isToday) {
@ -201,7 +190,7 @@ export async function update(
console.debug(hoverText);
if (profile.streakHourOffset === undefined) {
if (streakOffset === undefined) {
hoverText += `\n\nIf the streak reset time doesn't line up with your timezone, you can change it in Settings > Danger zone > Update streak hour offset.`;
}
}
@ -322,7 +311,10 @@ export async function update(
} else {
profileElement.find(".leaderboardsPositions").removeClass("hidden");
const lbPos = where === "profile" ? profile.allTimeLbs : profile.lbMemory;
const lbPos =
where === "profile"
? (profile as SharedTypes.UserProfile).allTimeLbs
: (profile as MonkeyTypes.Snapshot).lbMemory;
const t15 = lbPos?.time?.["15"]?.["english"];
const t60 = lbPos?.time?.["60"]?.["english"];

View file

@ -151,7 +151,7 @@ function reset(): void {
type UpdateOptions = {
uidOrName?: string;
data?: undefined | Profile.ProfileData;
data?: undefined | SharedTypes.UserProfile;
};
async function update(options: UpdateOptions): Promise<void> {
@ -159,14 +159,18 @@ async function update(options: UpdateOptions): Promise<void> {
if (options.data) {
$(".page.pageProfile .preloader").addClass("hidden");
await Profile.update("profile", options.data);
PbTables.update(options.data.personalBests, true);
PbTables.update(
// this cast is fine because pb tables can handle the partial data inside user profiles
options.data.personalBests as unknown as SharedTypes.PersonalBests,
true
);
} else if (options.uidOrName !== undefined && options.uidOrName !== "") {
const response = getParamExists
? await Ape.users.getProfileByUid(options.uidOrName)
: await Ape.users.getProfileByName(options.uidOrName);
$(".page.pageProfile .preloader").addClass("hidden");
if (response.status === 404) {
if (response.status === 404 || response.data === null) {
const message = getParamExists
? "User not found"
: `User ${options.uidOrName} not found`;
@ -181,10 +185,13 @@ async function update(options: UpdateOptions): Promise<void> {
);
} else {
window.history.replaceState(null, "", `/profile/${response.data.name}`);
await Profile.update("profile", response.data);
// this cast is fine because pb tables can handle the partial data inside user profiles
PbTables.update(
response.data.personalBests as unknown as SharedTypes.PersonalBests,
true
);
}
await Profile.update("profile", response.data);
PbTables.update(response.data.personalBests, true);
} else {
Notifications.add("Missing update parameter!", -1);
}
@ -199,7 +206,7 @@ $(".page.pageProfile").on("click", ".profile .userReportButton", () => {
void UserReportPopup.show({ uid, name, lbOptOut });
});
export const page = new Page<undefined | Profile.ProfileData>(
export const page = new Page<undefined | SharedTypes.UserProfile>(
"profile",
$(".page.pageProfile"),
"/profile",

View file

@ -1256,7 +1256,7 @@ $(".pageSettings .section.discordIntegration .getLinkAndGoToOauth").on(
"click",
() => {
void Ape.users.getOauthLink().then((res) => {
window.open(res.data.url, "_self");
window.open(res.data?.url as string, "_self");
});
}
);

View file

@ -98,8 +98,8 @@ async function apply(): Promise<void> {
const tags = DB.getSnapshot()?.tags ?? [];
const activeTagIds: string[] = tags
.filter((tag: MonkeyTypes.Tag) => tag.active)
.map((tag: MonkeyTypes.Tag) => tag._id);
.filter((tag: MonkeyTypes.UserTag) => tag.active)
.map((tag: MonkeyTypes.UserTag) => tag._id);
configChanges.tags = activeTagIds;
}

View file

@ -76,7 +76,7 @@ function hydrateInputs(): void {
websiteInput.val(socialProfiles?.website ?? "");
badgeIdsSelect.html("");
badges?.forEach((badge: MonkeyTypes.Badge) => {
badges?.forEach((badge: SharedTypes.Badge) => {
if (badge.selected) {
currentSelectedBadgeId = badge.id;
}
@ -108,14 +108,14 @@ function hydrateInputs(): void {
});
}
function buildUpdatesFromInputs(): MonkeyTypes.UserDetails {
function buildUpdatesFromInputs(): SharedTypes.UserProfileDetails {
const bio = (bioInput.val() ?? "") as string;
const keyboard = (keyboardInput.val() ?? "") as string;
const twitter = (twitterInput.val() ?? "") as string;
const github = (githubInput.val() ?? "") as string;
const website = (websiteInput.val() ?? "") as string;
const profileUpdates: MonkeyTypes.UserDetails = {
const profileUpdates: SharedTypes.UserProfileDetails = {
bio,
keyboard,
socialProfiles: {

View file

@ -100,6 +100,12 @@ async function apply(): Promise<void> {
-1
);
} else {
if (response.data === null) {
Notifications.add("Tag was added but data returned was null", -1);
Loader.hide();
return;
}
Notifications.add("Tag added", 1);
DB.getSnapshot()?.tags?.push({
display: propTagName,

View file

@ -230,7 +230,10 @@ export async function show(clearText = true): Promise<void> {
$("#quoteSearchPopup #toggleShowFavorites").removeClass("hidden");
}
if (DB.getSnapshot()?.quoteMod) {
const isQuoteMod =
DB.getSnapshot()?.quoteMod === true || DB.getSnapshot()?.quoteMod !== "";
if (isQuoteMod) {
$("#quoteSearchPopup #goToApproveQuotes").removeClass("hidden");
} else {
$("#quoteSearchPopup #goToApproveQuotes").addClass("hidden");
@ -431,10 +434,10 @@ $("#popups").on(
if (response.status === 200) {
$button.removeClass("fas").addClass("far");
const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf(
const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf(
quoteId
) as number;
dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1);
dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1);
}
} else {
// Add to favorites
@ -446,6 +449,9 @@ $("#popups").on(
if (response.status === 200) {
$button.removeClass("far").addClass("fas");
if (dbSnapshot.favoriteQuotes === undefined) {
dbSnapshot.favoriteQuotes = {};
}
if (!dbSnapshot.favoriteQuotes[quoteLang]) {
dbSnapshot.favoriteQuotes[quoteLang] = [];
}

View file

@ -1049,35 +1049,28 @@ list.clearTagPb = new SimplePopup(
};
}
if (response.data.resultCode === 1) {
const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0];
const tag = DB.getSnapshot()?.tags?.filter((t) => t._id === tagId)[0];
if (tag === undefined) {
return {
status: -1,
message: "Tag not found",
};
}
tag.personalBests = {
time: {},
words: {},
quote: {},
zen: {},
custom: {},
};
$(
`.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton`
).attr("aria-label", "No PB found");
return {
status: 1,
message: "Tag PB cleared",
};
} else {
if (tag === undefined) {
return {
status: -1,
message: "Failed to clear tag PB: " + response.data.message,
message: "Tag not found",
};
}
tag.personalBests = {
time: {},
words: {},
quote: {},
zen: {},
custom: {},
};
$(
`.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton`
).attr("aria-label", "No PB found");
return {
status: 1,
message: "Tag PB cleared",
};
},
(thisPopup) => {
thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`;
@ -1539,7 +1532,7 @@ list.updateCustomTheme = new SimplePopup(
};
}
const customTheme = snapshot.customThemes.find(
const customTheme = snapshot.customThemes?.find(
(t) => t._id === _thisPopup.parameters[0]
);
if (customTheme === undefined) {
@ -1585,7 +1578,7 @@ list.updateCustomTheme = new SimplePopup(
const snapshot = DB.getSnapshot();
if (!snapshot) return;
const customTheme = snapshot.customThemes.find(
const customTheme = snapshot.customThemes?.find(
(t) => t._id === _thisPopup.parameters[0]
);
if (!customTheme) return;

View file

@ -341,7 +341,7 @@ $(".pageSettings").on("click", " .section.themes .customTheme.button", (e) => {
if ($(e.target).hasClass("delButton")) return;
if ($(e.target).hasClass("editButton")) return;
const customThemeId = $(e.currentTarget).attr("customThemeId") ?? "";
const theme = DB.getSnapshot()?.customThemes.find(
const theme = DB.getSnapshot()?.customThemes?.find(
(e) => e._id === customThemeId
);

View file

@ -417,7 +417,7 @@ export async function updateCrown(): Promise<void> {
}
async function updateTags(dontSave: boolean): Promise<void> {
const activeTags: MonkeyTypes.Tag[] = [];
const activeTags: MonkeyTypes.UserTag[] = [];
const userTagsCount = DB.getSnapshot()?.tags?.length ?? 0;
try {
DB.getSnapshot()?.tags?.forEach((tag) => {
@ -885,10 +885,10 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => {
if (response.status === 200) {
$button.removeClass("fas").addClass("far");
const quoteIndex = dbSnapshot.favoriteQuotes[quoteLang]?.indexOf(
const quoteIndex = dbSnapshot.favoriteQuotes?.[quoteLang]?.indexOf(
quoteId
) as number;
dbSnapshot.favoriteQuotes[quoteLang]?.splice(quoteIndex, 1);
dbSnapshot.favoriteQuotes?.[quoteLang]?.splice(quoteIndex, 1);
}
} else {
// Add to favorites
@ -900,6 +900,9 @@ $(".pageTest #favoriteQuoteButton").on("click", async () => {
if (response.status === 200) {
$button.removeClass("far").addClass("fas");
if (dbSnapshot.favoriteQuotes === undefined) {
dbSnapshot.favoriteQuotes = {};
}
if (!dbSnapshot.favoriteQuotes[quoteLang]) {
dbSnapshot.favoriteQuotes[quoteLang] = [];
}

View file

@ -170,14 +170,6 @@ declare namespace MonkeyTypes {
display: string;
};
type Tag = {
_id: string;
name: string;
display: string;
personalBests: SharedTypes.PersonalBests;
active?: boolean;
};
type RawCustomTheme = {
name: string;
colors: string[];
@ -187,12 +179,6 @@ declare namespace MonkeyTypes {
_id: string;
} & RawCustomTheme;
type TypingStats = {
timeTyping: number;
startedTests: number;
completedTests: number;
};
type ConfigChanges = {
tags?: string[];
} & Partial<SharedTypes.Config>;
@ -211,60 +197,41 @@ declare namespace MonkeyTypes {
type QuoteRatings = Record<string, Record<number, number>>;
type Snapshot = {
banned?: boolean;
emailVerified?: boolean;
quoteRatings?: QuoteRatings;
results?: SharedTypes.Result<SharedTypes.Config.Mode>[];
verified?: boolean;
personalBests: SharedTypes.PersonalBests;
name: string;
customThemes: CustomTheme[];
presets?: SnapshotPreset[];
tags: Tag[];
favouriteThemes?: string[];
lbMemory?: LeaderboardMemory;
typingStats?: TypingStats;
quoteMod?: boolean;
discordId?: string;
config?: SharedTypes.Config;
favoriteQuotes: FavoriteQuotes;
needsToChangeName?: boolean;
discordAvatar?: string;
details?: UserDetails;
inventory?: UserInventory;
addedAt: number;
filterPresets: SharedTypes.ResultFilters[];
xp: number;
type UserTag = SharedTypes.UserTag & {
active?: boolean;
display: string;
};
type Snapshot = Omit<
SharedTypes.User,
| "timeTyping"
| "startedTests"
| "completedTests"
| "profileDetails"
| "streak"
| "resultFilterPresets"
| "tags"
| "xp"
> & {
typingStats: {
timeTyping: number;
startedTests: number;
completedTests: number;
};
details?: SharedTypes.UserProfileDetails;
inboxUnreadSize: number;
streak: number;
maxStreak: number;
filterPresets: SharedTypes.ResultFilters[];
isPremium: boolean;
streakHourOffset?: number;
lbOptOut?: boolean;
isPremium?: boolean;
config: SharedTypes.Config;
tags: UserTag[];
presets: SnapshotPreset[];
results?: SharedTypes.Result<SharedTypes.Config.Mode>[];
xp: number;
};
type UserDetails = {
bio?: string;
keyboard?: string;
socialProfiles: {
twitter?: string;
github?: string;
website?: string;
};
};
type UserInventory = {
badges: Badge[];
};
type Badge = {
id: number;
selected?: boolean;
};
type FavoriteQuotes = Record<string, string[]>;
type Group<
G extends keyof SharedTypes.ResultFilters = keyof SharedTypes.ResultFilters
> = G extends G ? SharedTypes.ResultFilters[G] : never;
@ -466,8 +433,8 @@ declare namespace MonkeyTypes {
type BadgeReward = {
type: "badge";
item: Badge;
} & Reward<Badge>;
item: SharedTypes.Badge;
} & Reward<SharedTypes.Badge>;
type AllRewards = XpReward | BadgeReward;

View file

@ -32,6 +32,13 @@ export async function linkDiscord(hashOverride: string): Promise<void> {
);
}
if (response.data === null) {
return Notifications.add(
"Failed to link Discord: data returned was null",
-1
);
}
Notifications.add(response.message, 1);
const snapshot = DB.getSnapshot();

View file

@ -35,7 +35,13 @@ const BASE_CONFIG = {
},
module: {
rules: [
{ test: /\.tsx?$/, loader: "ts-loader" },
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true,
},
},
{
test: /\.s[ac]ss$/i,
use: [

View file

@ -459,4 +459,142 @@ declare namespace SharedTypes {
xpBreakdown: Record<string, number>;
streak: number;
};
type UserStreak = {
lastResultTimestamp: number;
length: number;
maxLength: number;
hourOffset?: number;
};
type UserTag = {
_id: string;
name: string;
personalBests: PersonalBests;
};
type UserProfileDetails = {
bio?: string;
keyboard?: string;
socialProfiles: {
twitter?: string;
github?: string;
website?: string;
};
};
type CustomTheme = {
_id: string;
name: string;
colors: string[];
};
type PremiumInfo = {
startTimestamp: number;
expirationTimestamp: number;
};
type UserQuoteRatings = Record<string, Record<string, number>>;
type UserLbMemory = Record<string, Record<string, Record<string, number>>>;
type UserInventory = {
badges: Badge[];
};
type Badge = {
id: number;
selected?: boolean;
};
type User = {
name: string;
email: string;
uid: string;
addedAt: number;
personalBests: PersonalBests;
lastReultHashes?: string[]; //todo: fix typo (its in the db too)
completedTests?: number;
startedTests?: number;
timeTyping?: number;
streak?: UserStreak;
xp?: number;
discordId?: string;
discordAvatar?: string;
tags?: UserTag[];
profileDetails?: UserProfileDetails;
customThemes?: CustomTheme[];
premium?: PremiumInfo;
quoteRatings?: UserQuoteRatings;
favoriteQuotes?: Record<string, string[]>;
lbMemory?: UserLbMemory;
inventory?: UserInventory;
banned?: boolean;
lbOptOut?: boolean;
verified?: boolean;
needsToChangeName?: boolean;
quoteMod?: boolean | string;
resultFilterPresets?: ResultFilters[];
};
type Reward<T> = {
type: string;
item: T;
};
type XpReward = {
type: "xp";
item: number;
} & Reward<number>;
type BadgeReward = {
type: "badge";
item: SharedTypes.Badge;
} & Reward<SharedTypes.Badge>;
type AllRewards = XpReward | BadgeReward;
type MonkeyMail = {
id: string;
subject: string;
body: string;
timestamp: number;
read: boolean;
rewards: AllRewards[];
};
type UserProfile = Pick<
User,
| "name"
| "banned"
| "addedAt"
| "discordId"
| "discordAvatar"
| "xp"
| "lbOptOut"
| "inventory"
| "uid"
> & {
typingStats: {
completedTests: User["completedTests"];
startedTests: User["startedTests"];
timeTyping: User["timeTyping"];
};
streak: UserStreak["length"];
maxStreak: UserStreak["maxLength"];
details: UserProfileDetails;
allTimeLbs: {
time: Record<string, Record<string, number | null>>;
};
personalBests: {
time: Pick<
Record<`${number}`, SharedTypes.PersonalBest[]>,
"15" | "30" | "60" | "120"
>;
words: Pick<
Record<`${number}`, SharedTypes.PersonalBest[]>,
"10" | "25" | "50" | "100"
>;
};
};
}