feat: followers

This commit is contained in:
zkldi
2022-12-14 01:46:59 +00:00
parent 98271923ec
commit 0266eaa758
7 changed files with 228 additions and 70 deletions
@@ -5,8 +5,8 @@ import { UserDocument } from "tachi-common";
export default function DashboardActivity({ user }: { user: UserDocument }) {
return (
<>
<div className="display-4 mb-4">Here's what you and your rivals have been up to!</div>
<Activity url={`/users/${user.id}/activity?includeRivals=true`} />
<div className="display-4 mb-4">Here's what's been happening.</div>
<Activity url={`/users/${user.id}/activity?includeRivals=true&includeFollowers=true`} />
</>
);
}
+84 -65
View File
@@ -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({
/>
</div>
{/* 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 && (
<div className="col-12 w-100 d-flex justify-content-center mt-8">
{settings.rivals.includes(reqUser.id) ? (
<Button
variant="outline-danger"
onClick={async () => {
const newRivals = settings.rivals.filter((e) => e !== reqUser.id);
{/* give them the option to add them as a rival or follow them */}
{userSettings && reqUser.id !== userSettings.userID && (
<div
className="col-12 w-100 d-flex justify-content-center mt-8"
style={{
gap: "30px",
}}
>
{/* has the logged in user played this game? */}
{/* otherwise, they can't rival. */}
{settings && (
<>
{settings.rivals.includes(reqUser.id) ? (
<Button
variant="outline-danger"
onClick={async () => {
const newRivals = settings.rivals.filter(
(e) => e !== reqUser.id
);
const res = await APIFetchV1(
`/users/${settings.userID}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: newRivals,
}),
headers: {
"Content-Type": "application/json",
},
}
);
const res = await APIFetchV1(
`/users/${settings.userID}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: newRivals,
}),
headers: {
"Content-Type": "application/json",
},
}
);
if (res.success) {
SendSuccessToast(
`Removed ${reqUser.username} from your rivals.`
);
setSettings({
...settings,
rivals: newRivals,
});
} else {
SendErrorToast(HumaniseError(res.description));
}
}}
>
Remove as Rival
</Button>
) : (
<Button
variant="outline-success"
onClick={async () => {
const newRivals = [...settings.rivals, reqUser.id];
if (res.success) {
SendSuccessToast(
`Removed ${reqUser.username} from your rivals.`
);
setSettings({
...settings,
rivals: newRivals,
});
} else {
SendErrorToast(HumaniseError(res.description));
}
}}
>
Remove as Rival
</Button>
) : (
<Button
variant="outline-success"
onClick={async () => {
const newRivals = [...settings.rivals, reqUser.id];
const res = await APIFetchV1(
`/users/${settings.userID}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: newRivals,
}),
headers: {
"Content-Type": "application/json",
},
}
);
const res = await APIFetchV1(
`/users/${settings.userID}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: newRivals,
}),
headers: {
"Content-Type": "application/json",
},
}
);
if (res.success) {
SendSuccessToast(`Added ${reqUser.username} to your rivals!`);
setSettings({
...settings,
rivals: newRivals,
});
} else {
SendErrorToast(HumaniseError(res.description));
}
}}
>
Add as Rival
</Button>
if (res.success) {
SendSuccessToast(
`Added ${reqUser.username} to your rivals!`
);
setSettings({
...settings,
rivals: newRivals,
});
} else {
SendErrorToast(HumaniseError(res.description));
}
}}
>
Add as Rival
</Button>
)}
</>
)}
<FollowUserButton userToFollow={reqUser} />
</div>
)}
</>
+11
View File
@@ -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 (
<>
<div className="col-12 col-lg-3">
@@ -78,6 +81,14 @@ export function UserHeaderBody({ reqUser }: { reqUser: UserDocument }) {
</li>
</ul>
</div>
{loggedInUser && reqUser.id !== loggedInUser.id && (
<div className="col-12 mt-8">
<Divider />
<div className="d-flex w-100 justify-content-center ">
<FollowUserButton userToFollow={reqUser} />
</div>
</div>
)}
</>
);
}
@@ -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 (
<Button
variant="outline-danger"
onClick={async () => {
const res = await APIFetchV1(
`/users/${userSettings.userID}/following/remove`,
{
method: "POST",
body: JSON.stringify({
userID: userToFollow.id,
}),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);
if (res.success) {
const newFollowing = userSettings.following.filter(
(e) => e !== userToFollow.id
);
setUserSettings({
...userSettings,
following: newFollowing,
});
}
}}
>
Unfollow
</Button>
);
}
return (
<QuickTooltip tooltipContent="Following a user will mean you'll see their sessions and updates in your feed.">
<Button
variant="outline-success"
onClick={async () => {
const res = await APIFetchV1(
`/users/${userSettings.userID}/following/add`,
{
method: "POST",
body: JSON.stringify({
userID: userToFollow.id,
}),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);
if (res.success) {
const newFollowing = [...userSettings.following, userToFollow.id];
setUserSettings({
...userSettings,
following: newFollowing,
});
}
}}
>
Follow
</Button>
</QuickTooltip>
);
}
+3
View File
@@ -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;
@@ -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) {
@@ -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<Record<IDStrings, unknown>> = {};
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<integer> = [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