diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 06ad7340d..ef6655fa4 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -14,6 +14,7 @@ import { } from "../elements/test-activity-calendar"; import { Preset } from "@monkeytype/schemas/presets"; import { Language } from "@monkeytype/schemas/languages"; +import { FriendRequestStatus } from "@monkeytype/schemas/friends"; export type SnapshotUserTag = UserTag & { active?: boolean; @@ -84,6 +85,7 @@ export type Snapshot = Omit< xp: number; testActivity?: ModifiableTestActivityCalendar; testActivityByYear?: { [key: string]: TestActivityCalendar }; + friends: Record; }; export type SnapshotPreset = Preset & { @@ -131,6 +133,7 @@ const defaultSnap = { 60: { english: { count: 0, rank: 0 } }, }, }, + friends: {}, } as Snapshot; export function getDefaultSnapshot(): Snapshot { diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index dff0d111f..ddccd1fca 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -1,6 +1,6 @@ import Ape from "./ape"; import * as Notifications from "./elements/notifications"; -import { isAuthenticated } from "./firebase"; +import { isAuthenticated, getAuthenticatedUser } from "./firebase"; import * as ConnectionState from "./states/connection"; import { lastElementFromArray } from "./utils/arrays"; import { migrateConfig } from "./utils/config"; @@ -85,11 +85,13 @@ export async function initSnapshot(): Promise { try { if (!isAuthenticated()) return false; - const [userResponse, configResponse, presetsResponse] = await Promise.all([ - Ape.users.get(), - Ape.configs.get(), - Ape.presets.get(), - ]); + const [userResponse, configResponse, presetsResponse, friendsResponse] = + await Promise.all([ + Ape.users.get(), + Ape.configs.get(), + Ape.presets.get(), + Ape.friends.getRequests(), + ]); if (userResponse.status !== 200) { throw new SnapshotInitError( @@ -109,10 +111,17 @@ export async function initSnapshot(): Promise { presetsResponse.status ); } + if (friendsResponse.status !== 200) { + throw new SnapshotInitError( + `${friendsResponse.body.message} (friendRequests)`, + friendsResponse.status + ); + } const userData = userResponse.body.data; const configData = configResponse.body.data; const presetsData = presetsResponse.body.data; + const friendsData = friendsResponse.body.data; if (userData === null) { throw new SnapshotInitError( @@ -249,6 +258,16 @@ export async function initSnapshot(): Promise { ); } + snap.friends = Object.fromEntries( + friendsData.map((friend) => [ + // oxlint-disable-next-line no-non-null-assertion + friend.initiatorUid === getAuthenticatedUser()!.uid + ? friend.friendUid + : friend.initiatorUid, + friend.status, + ]) + ); + dbSnapshot = snap; return dbSnapshot; } catch (e) { diff --git a/frontend/src/ts/elements/account-settings/blocked-user-table.ts b/frontend/src/ts/elements/account-settings/blocked-user-table.ts index 1d60c491f..d64eca550 100644 --- a/frontend/src/ts/elements/account-settings/blocked-user-table.ts +++ b/frontend/src/ts/elements/account-settings/blocked-user-table.ts @@ -3,6 +3,8 @@ import { FriendRequest } from "@monkeytype/schemas/friends"; import Ape from "../../ape"; import { format } from "date-fns/format"; import { isAuthenticated } from "../../firebase"; +import { getFriendUid } from "../../pages/friends"; +import * as DB from "../../db"; let blockedUsers: FriendRequest[] = []; const element = $("#pageAccountSettings .tab[data-tab='blockedUsers']"); @@ -56,7 +58,7 @@ function refreshList(): void { } const content = blockedUsers.map( (blocked) => ` - + ${blocked.initiatorName} @@ -86,5 +88,17 @@ element.on("click", "table button.delete", async (e) => { } else { blockedUsers = blockedUsers.filter((it) => it._id !== id); refreshList(); + + const snapshot = DB.getSnapshot(); + if (snapshot) { + const uid = (e.target as HTMLElement).parentElement?.parentElement + ?.dataset["uid"]; + if (uid === undefined) { + throw new Error("Cannot find uid of target."); + } + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete, @typescript-eslint/no-unsafe-member-access + delete snapshot.friends[uid]; + } } }); diff --git a/frontend/src/ts/elements/profile.ts b/frontend/src/ts/elements/profile.ts index a280120b8..253aff9b6 100644 --- a/frontend/src/ts/elements/profile.ts +++ b/frontend/src/ts/elements/profile.ts @@ -313,9 +313,19 @@ export async function update( if (profile.uid === getAuthenticatedUser()?.uid) { profileElement.find(".userReportButton").addClass("hidden"); + profileElement.find(".addFriendButton").addClass("hidden"); } else { profileElement.find(".userReportButton").removeClass("hidden"); } + if ( + profile.uid !== undefined && + (profile.uid === getAuthenticatedUser()?.uid || + DB.getSnapshot()?.friends[profile.uid] !== undefined) + ) { + profileElement.find(".addFriendButton").addClass("hidden"); + } else { + profileElement.find(".addFriendButton").removeClass("hidden"); + } //structure diff --git a/frontend/src/ts/pages/friends.ts b/frontend/src/ts/pages/friends.ts index 575b11d31..48ed193db 100644 --- a/frontend/src/ts/pages/friends.ts +++ b/frontend/src/ts/pages/friends.ts @@ -16,16 +16,41 @@ import { secondsToString } from "../utils/date-and-time"; import { PersonalBest } from "@monkeytype/schemas/shared"; import Format from "../utils/format"; import { getHtmlByUserFlags } from "../controllers/user-flag-controller"; -import { Friend } from "@monkeytype/schemas/friends"; +import { Friend, FriendRequest } from "@monkeytype/schemas/friends"; import { SortedTable } from "../utils/sorted-table"; import { getAvatarElement } from "../utils/discord-avatar"; import { formatTypingStatsRatio } from "../utils/misc"; import { getLanguageDisplayString } from "../utils/strings"; +import * as DB from "../db"; +import { Auth } from "../firebase"; const pageElement = $(".page.pageFriends"); let friendsTable: SortedTable | undefined = undefined; +export function getFriendUid( + friendRequest: Pick +): string { + if (Auth?.currentUser?.uid === friendRequest.initiatorUid) + return friendRequest.friendUid; + return friendRequest.initiatorUid; +} + +export async function addFriend(friendName: string): Promise { + const result = await Ape.friends.createRequest({ body: { friendName } }); + + if (result.status !== 200) { + return `Friend request failed: ${result.body.message}`; + } else { + const snapshot = DB.getSnapshot(); + if (snapshot !== undefined) { + const friendUid = getFriendUid(result.body.data); + snapshot.friends[friendUid] = result.body.data.status; + } + return true; + } +} + const addFriendModal = new SimpleModal({ id: "addFriend", title: "Add a friend", @@ -33,12 +58,12 @@ const addFriendModal = new SimpleModal({ buttonText: "request", onlineOnly: true, execFn: async (_thisPopup, friendName) => { - const result = await Ape.friends.createRequest({ body: { friendName } }); + const result = await addFriend(friendName); - if (result.status !== 200) { + if (result !== true) { return { status: -1, - message: `Friend request failed: ${result.body.message}`, + message: result, }; } else { return { status: 1, message: `Request send to ${friendName}` }; @@ -66,7 +91,9 @@ async function updatePendingRequests(): Promise { const html = result.body.data .map( - (item) => ` + (item) => ` ${item.initiatorName} @@ -311,9 +338,22 @@ $(".pageFriends .pendingRequests table").on("click", async (e) => { const row = e.target.parentElement?.parentElement; const count = row?.parentElement?.childElementCount; row?.remove(); + + const snapshot = DB.getSnapshot(); + if (action === "rejected" && snapshot) { + const friendUid = + e.target.parentElement?.parentElement?.dataset["friendUid"]; + if (friendUid === undefined) { + throw new Error("Cannot find friendUid of target."); + } + + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete, @typescript-eslint/no-unsafe-member-access + delete snapshot.friends[friendUid]; + } if (count === 1) { $(".pageFriends .pendingRequests").addClass("hidden"); } + DB.getSnapshot(); } }); diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index 20457410e..b306b2564 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -11,6 +11,7 @@ import { PersonalBests } from "@monkeytype/schemas/shared"; import * as TestActivity from "../elements/test-activity"; import { TestActivityCalendar } from "../elements/test-activity-calendar"; import { getFirstDayOfTheWeek } from "../utils/date-and-time"; +import { addFriend } from "./friends"; const firstDayOfTheWeek = getFirstDayOfTheWeek(); @@ -80,6 +81,13 @@ function reset(): void { > +
@@ -236,6 +244,18 @@ $(".page.pageProfile").on("click", ".profile .userReportButton", () => { void UserReportModal.show({ uid, name, lbOptOut }); }); +$(".page.pageProfile").on("click", ".profile .addFriendButton", async () => { + const friendName = $(".page.pageProfile .profile").attr("name") ?? ""; + + const result = await addFriend(friendName); + + if (result === true) { + Notifications.add(`Request send to ${friendName}`); + $(".profile .details .addFriendButton").addClass("hidden"); + } else { + Notifications.add(result, -1); + } +}); export const page = new Page({ id: "profile",