From 0266eaa758e28a1721519dde9db083fae2af6330 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Wed, 14 Dec 2022 01:46:59 +0000 Subject: [PATCH] feat: followers --- .../dashboard/DashboardActivity.tsx | 4 +- client/src/components/user/UGPTHeader.tsx | 149 ++++++++++-------- client/src/components/user/UserHeader.tsx | 11 ++ .../src/components/util/FollowUserButton.tsx | 92 +++++++++++ server/src/lib/setup/config.ts | 3 + .../api/v1/users/_userID/following/router.ts | 15 ++ .../router/api/v1/users/_userID/router.ts | 24 ++- 7 files changed, 228 insertions(+), 70 deletions(-) create mode 100644 client/src/components/util/FollowUserButton.tsx diff --git a/client/src/components/dashboard/DashboardActivity.tsx b/client/src/components/dashboard/DashboardActivity.tsx index 41d309f09..cdcf136f8 100644 --- a/client/src/components/dashboard/DashboardActivity.tsx +++ b/client/src/components/dashboard/DashboardActivity.tsx @@ -5,8 +5,8 @@ import { UserDocument } from "tachi-common"; export default function DashboardActivity({ user }: { user: UserDocument }) { return ( <> -
Here's what you and your rivals have been up to!
- +
Here's what's been happening.
+ ); } diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx index a63e3b0ff..e824b1f44 100644 --- a/client/src/components/user/UGPTHeader.tsx +++ b/client/src/components/user/UGPTHeader.tsx @@ -12,6 +12,8 @@ import { Game, Playtype, UserDocument } from "tachi-common"; import { UGPTStatsReturn } from "types/api-returns"; import { UserContext } from "context/UserContext"; import useLUGPTSettings from "components/util/useLUGPTSettings"; +import { UserSettingsContext } from "context/UserSettingsContext"; +import FollowUserButton from "components/util/FollowUserButton"; import ProfileBadges from "./ProfileBadges"; import ProfilePicture from "./ProfilePicture"; import RankingData from "./UGPTRankingData"; @@ -29,6 +31,7 @@ export function UGPTHeaderBody({ stats: UGPTStatsReturn; }) { const { settings, setSettings } = useLUGPTSettings(); + const { settings: userSettings } = useContext(UserSettingsContext); return ( <> @@ -81,76 +84,92 @@ export function UGPTHeaderBody({ /> {/* if someone is logged in and they aren't the user they're viewing */} - {/* give them the option to add them as a rival */} - {settings && reqUser.id !== settings.userID && ( -
- {settings.rivals.includes(reqUser.id) ? ( - - ) : ( - + ) : ( + + if (res.success) { + SendSuccessToast( + `Added ${reqUser.username} to your rivals!` + ); + setSettings({ + ...settings, + rivals: newRivals, + }); + } else { + SendErrorToast(HumaniseError(res.description)); + } + }} + > + Add as Rival + + )} + )} +
)} diff --git a/client/src/components/user/UserHeader.tsx b/client/src/components/user/UserHeader.tsx index 3e945f868..75ca563c3 100644 --- a/client/src/components/user/UserHeader.tsx +++ b/client/src/components/user/UserHeader.tsx @@ -12,6 +12,7 @@ import React, { useContext, useState } from "react"; import { Button, Form, Modal } from "react-bootstrap"; import { UserDocument } from "tachi-common"; import { SetState } from "types/react"; +import FollowUserButton from "components/util/FollowUserButton"; import ProfileBadges from "./ProfileBadges"; import ProfilePicture from "./ProfilePicture"; @@ -40,6 +41,8 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) { ); } + const { user: loggedInUser } = useContext(UserContext); + return ( <>
@@ -78,6 +81,14 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) {
+ {loggedInUser && reqUser.id !== loggedInUser.id && ( +
+ +
+ +
+
+ )} ); } diff --git a/client/src/components/util/FollowUserButton.tsx b/client/src/components/util/FollowUserButton.tsx new file mode 100644 index 000000000..53639b4b5 --- /dev/null +++ b/client/src/components/util/FollowUserButton.tsx @@ -0,0 +1,92 @@ +import { APIFetchV1 } from "util/api"; +import { UserSettingsContext } from "context/UserSettingsContext"; +import React, { useContext } from "react"; +import { Button } from "react-bootstrap"; +import { UserDocument } from "tachi-common"; +import QuickTooltip from "components/layout/misc/QuickTooltip"; + +export default function FollowUserButton({ userToFollow }: { userToFollow: UserDocument }) { + const { settings: userSettings, setSettings: setUserSettings } = + useContext(UserSettingsContext); + + if (!userSettings) { + return null; + } + + // can't follow yourself + if (userSettings.userID === userToFollow.id) { + return null; + } + + if (userSettings.following.includes(userToFollow.id)) { + return ( + + ); + } + + return ( + + + + ); +} diff --git a/server/src/lib/setup/config.ts b/server/src/lib/setup/config.ts index 7d26d5330..12f39db72 100644 --- a/server/src/lib/setup/config.ts +++ b/server/src/lib/setup/config.ts @@ -90,6 +90,7 @@ export interface TachiServerConfig { BEATORAJA_QUEUE_SIZE: integer; MAX_GOAL_SUBSCRIPTIONS: integer; MAX_QUEST_SUBSCRIPTIONS: integer; + MAX_FOLLOWING_AMOUNT: integer; OUR_URL: string; ALLOW_RUNNING_OFFLINE?: boolean; INVITE_CODE_CONFIG?: { @@ -168,6 +169,7 @@ const err = p(config, { BEATORAJA_QUEUE_SIZE: p.optional(p.gteInt(2)), MAX_GOAL_SUBSCRIPTIONS: p.optional(p.isPositiveInteger), MAX_QUEST_SUBSCRIPTIONS: p.optional(p.isPositiveInteger), + MAX_FOLLOWING_AMOUNT: p.optional(p.isPositiveInteger), OUR_URL: "string", INVITE_CODE_CONFIG: p.optional({ BATCH_SIZE: p.isPositiveInteger, @@ -231,6 +233,7 @@ tachiServerConfig.USC_QUEUE_SIZE ??= 3; tachiServerConfig.BEATORAJA_QUEUE_SIZE ??= 3; tachiServerConfig.MAX_GOAL_SUBSCRIPTIONS ??= 1_000; tachiServerConfig.MAX_QUEST_SUBSCRIPTIONS ??= 100; +tachiServerConfig.MAX_FOLLOWING_AMOUNT ??= 1_000; tachiServerConfig.USE_EXTERNAL_SCORE_IMPORT_WORKER ??= false; export const TachiConfig = tachiServerConfig.TACHI_CONFIG; diff --git a/server/src/server/router/api/v1/users/_userID/following/router.ts b/server/src/server/router/api/v1/users/_userID/following/router.ts index 024e3b3d8..82608362d 100644 --- a/server/src/server/router/api/v1/users/_userID/following/router.ts +++ b/server/src/server/router/api/v1/users/_userID/following/router.ts @@ -1,6 +1,7 @@ import { Router } from "express"; import db from "external/mongo/db"; import CreateLogCtx from "lib/logger/logger"; +import { ServerConfig } from "lib/setup/config"; import p from "prudence"; import prValidate from "server/middleware/prudence-validate"; import { GetUser } from "utils/req-tachi-data"; @@ -55,6 +56,13 @@ router.post("/add", prValidate({ userID: p.isPositiveInteger }), async (req, res const { userID: toFollow } = req.safeBody as { userID: integer }; + if (user.id === toFollow) { + return res.status(400).json({ + success: false, + description: `Can't follow yourself. Bit self-indulgent!`, + }); + } + const settings = await db["user-settings"].findOne({ userID: user.id }); if (!settings) { @@ -73,6 +81,13 @@ router.post("/add", prValidate({ userID: p.isPositiveInteger }), async (req, res }); } + if (settings.following.length >= ServerConfig.MAX_FOLLOWING_AMOUNT) { + return res.status(400).json({ + success: false, + description: `You are following too many people. The max is ${ServerConfig.MAX_FOLLOWING_AMOUNT}.`, + }); + } + const userToFollow = await GetUserWithID(toFollow); if (!userToFollow) { diff --git a/server/src/server/router/api/v1/users/_userID/router.ts b/server/src/server/router/api/v1/users/_userID/router.ts index 80138567b..9cf69cd67 100644 --- a/server/src/server/router/api/v1/users/_userID/router.ts +++ b/server/src/server/router/api/v1/users/_userID/router.ts @@ -425,11 +425,13 @@ router.use( prValidate({ startTime: "*string", includeRivals: p.optional(p.isIn("true", "false")), + includeFollowers: p.optional(p.isIn("true", "false")), }), async (req, res) => { const qStartTime = req.query.startTime as string | undefined; const includeRivals = req.query.includeRivals === "true"; + const includeFollowers = req.query.includeFollowers === "true"; const startTime = qStartTime ? Number(qStartTime) : null; @@ -448,14 +450,30 @@ router.use( const data: Partial> = {}; + const settings = await db["user-settings"].findOne({ userID: user.id }); + + if (!settings) { + logger.error(`User ${FormatUserDoc(user)} doesn't have any settings?`); + return res.status(500).json({ + success: false, + description: `This user has no settings.`, + }); + } + await Promise.all( gpts.map(async (e) => { - let userID: ActivityConstraint["userID"] = user.id; + const userIDs: Array = [user.id]; if (includeRivals) { const rivalIDs = await GetRivalIDs(user.id, e.game, e.playtype); - userID = { $in: [user.id, ...rivalIDs] }; + userIDs.push(...rivalIDs); + } + + // n.b. it is intentional behaviour that you only get updates for + // people you follow on games you play. + if (includeFollowers) { + userIDs.push(...settings.following); } const activity = await GetRecentActivity( @@ -463,7 +481,7 @@ router.use( { game: e.game, playtype: e.playtype, - userID, + userID: { $in: userIDs }, }, 30, startTime