feat: rival activity

This commit is contained in:
zkldi
2022-11-11 02:50:32 +00:00
parent fef5e3fc01
commit 2a32fb5db2
34 changed files with 613 additions and 81 deletions
@@ -0,0 +1,35 @@
import { ClumpActivity } from "util/activity";
import ApiError from "components/util/ApiError";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import React, { useState } from "react";
import { ActivityReturn } from "types/api-returns";
import { UGPT } from "types/react";
import UGPTActivity from "components/user/UGPTActivity";
export default function ActivityPage({ reqUser, game, playtype }: UGPT) {
const [duration, setDuration] = useState<"month" | "3month" | "year">("month");
const { data, error } = useApiQuery<ActivityReturn>(
`/users/${reqUser.id}/games/${game}/${playtype}/activity?duration=${duration}`
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const joined = ClumpActivity(data);
return (
<UGPTActivity
data={joined}
users={data.users}
duration={duration}
setDuration={setDuration}
/>
);
}
@@ -27,9 +27,9 @@ import {
GetGamePTConfig,
UserDocument,
TableDocument,
Playtype,
} from "tachi-common";
import { FolderStatsInfo, UGPTTableReturns } from "types/api-returns";
import { Playtype } from "types/tachi";
interface Props {
reqUser: UserDocument;
@@ -37,7 +37,7 @@ import {
} from "tachi-common";
import { UGPTFolderReturns } from "types/api-returns";
import { FolderDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import FolderComparePage from "./FolderComparePage";
interface Props {
@@ -1,6 +1,35 @@
import React from "react";
import { ClumpActivity } from "util/activity";
import ApiError from "components/util/ApiError";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import React, { useState } from "react";
import { ActivityReturn } from "types/api-returns";
import { UGPT } from "types/react";
import UGPTActivity from "components/user/UGPTActivity";
export default function RivalsActivityPage({ reqUser, game, playtype }: UGPT) {
return <div>RivalsActivityPage</div>;
const [duration, setDuration] = useState<"month" | "3month" | "year">("month");
const { data, error } = useApiQuery<ActivityReturn>(
`/users/${reqUser.id}/games/${game}/${playtype}/rivals/activity?duration=${duration}`
);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
const joined = ClumpActivity(data);
return (
<UGPTActivity
data={joined}
users={data.users}
duration={duration}
setDuration={setDuration}
/>
);
}
@@ -47,7 +47,7 @@ export default function RivalsMainPage({
</SelectLinkButton>
<SelectLinkButton to={`${base}/rivals`}>
<Icon type="list" />
Activity
Rival Activity
</SelectLinkButton>
<SelectLinkButton to={`${base}/rivals/tracking`}>
<Icon type="thumbtack" />
@@ -1,5 +1,6 @@
import { APIFetchV1 } from "util/api";
import { SendErrorToast } from "util/toaster";
import { ChangeOpacity } from "util/color-opacity";
import Card from "components/layout/page/Card";
import ApiError from "components/util/ApiError";
import Divider from "components/util/Divider";
@@ -11,8 +12,8 @@ import useApiQuery from "components/util/query/useApiQuery";
import UserIcon from "components/util/UserIcon";
import { UserContext } from "context/UserContext";
import React, { useContext, useState } from "react";
import { Button, Col } from "react-bootstrap";
import { FormatGame, Game, GetGameConfig, Playtype, UserDocument } from "tachi-common";
import { Alert, Button, Col } from "react-bootstrap";
import { COLOUR_SET, FormatGame, Game, GetGameConfig, Playtype, UserDocument } from "tachi-common";
import useLUGPTSettings from "components/util/useLUGPTSettings";
import useSetSubheader from "components/layout/header/useSetSubheader";
@@ -105,6 +106,57 @@ function RivalsOverviewPage({
return (
<>
{isRequestingUser && currentRivals.toString() !== rivals.toString() && (
<>
{/* kind of a stupid way to check whether the array has changed or not, but who cares. */}
<Col xs={12}>
<Divider />
</Col>
<Col xs={12}>
<Alert
className="w-100 d-flex justify-content-center"
style={{
backgroundColor: ChangeOpacity(COLOUR_SET.vibrantYellow, 0.2),
flexDirection: "column",
flexWrap: "wrap",
}}
>
<div style={{ color: "white" }}>You have unsaved changes!</div>
<Divider />
<Button
size="lg"
onClick={async () => {
const res = await APIFetchV1(
`/users/${reqUser.id}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: rivals.map((e) => e.id),
}),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);
if (res.success) {
setCurrentRivals(rivals);
setSettings({
...settings,
rivals: rivals.map((e) => e.id),
});
}
}}
variant="primary"
>
Save Changes
</Button>
</Alert>
</Col>
</>
)}
<Card header={`${isRequestingUser ? "Your" : `${reqUser.username}'s`} Rivals`}>
<Col xs={12} className="d-flex justify-content-center flex-wrap">
{rivals.map((e) => (
@@ -144,47 +196,6 @@ function RivalsOverviewPage({
)}
</Col>
{/* kind of a stupid way to check whether the array has changed or not, but who cares. */}
{currentRivals.toString() !== rivals.toString() && (
<>
<Col xs={12}>
<Divider />
</Col>
<Col xs={12} className="d-flex justify-content-center">
<Button
size="lg"
onClick={async () => {
const res = await APIFetchV1(
`/users/${reqUser.id}/games/${game}/${playtype}/rivals`,
{
method: "PUT",
body: JSON.stringify({
rivalIDs: rivals.map((e) => e.id),
}),
headers: {
"Content-Type": "application/json",
},
},
true,
true
);
if (res.success) {
setCurrentRivals(rivals);
setSettings({
...settings,
rivals: rivals.map((e) => e.id),
});
}
}}
variant="primary"
>
Save Changes
</Button>
</Col>
</>
)}
<UserSelectModal
callback={(user) => {
if (rivals.length >= 5) {
@@ -202,7 +213,7 @@ function RivalsOverviewPage({
setShow={setShow}
url={`/games/${game}/${playtype}/players`}
excludeSet={rivals.map((e) => e.id)}
excludeMsg="Already Rivaled!"
excludeMsg="Added!"
/>
</>
)}
@@ -6,8 +6,8 @@ import useUGPTBase from "components/util/useUGPTBase";
import React from "react";
import { Col, Row } from "react-bootstrap";
import { Route, Switch } from "react-router-dom";
import { FormatGame, GetGameConfig, UserDocument } from "tachi-common";
import { GamePT, UGPT } from "types/react";
import { FormatGame, GetGameConfig } from "tachi-common";
import { UGPT } from "types/react";
import GoalsPage from "./GoalsPage";
export default function TargetsPage({ reqUser, game, playtype }: UGPT) {
+1 -1
View File
@@ -24,7 +24,7 @@ import {
UserGameStats,
} from "tachi-common";
import { ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
interface Data {
import: ImportDocument;
@@ -8,7 +8,7 @@ import React, { useEffect, useMemo, useRef, useState } from "react";
import { Link, useLocation } from "react-router-dom";
import { Game, GetGameConfig, integer, UserDocument, SongDocument } from "tachi-common";
import { GamePT, JustChildren } from "types/react";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import QuickTooltip from "../misc/QuickTooltip";
interface SearchReturns {
@@ -1,7 +1,7 @@
import { FormatMillions } from "util/misc";
import React from "react";
import { Game, PBScoreDocument, ScoreDocument } from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import ScoreCell from "./ScoreCell";
export default function ScoreCellLarge(props: {
@@ -1,5 +1,5 @@
import { Game } from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import { BMSGraphsComponent } from "./components/BMSScoreDropdownParts";
import { IIDXGraphsComponent } from "./components/IIDXScoreDropdownParts";
import { ITGGraphsComponent } from "./components/ITGScoreDropdownParts";
@@ -6,7 +6,7 @@ import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import React, { useState } from "react";
import { Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { FolderDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
@@ -17,7 +17,7 @@ import {
RivalChartDataset,
ScoreDataset,
} from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import SelectableRating from "../components/SelectableRating";
import { Header, ZTableTHProps } from "../components/TachiTable";
@@ -10,7 +10,7 @@ import {
ScoreDocument,
} from "tachi-common";
import { ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import TimestampCell from "../cells/TimestampCell";
import TachiTable from "../components/TachiTable";
import ScoreCoreCells from "../game-core-cells/ScoreCoreCells";
+1 -1
View File
@@ -5,7 +5,7 @@ import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import React, { useState } from "react";
import { Game, IDStrings, ScoreCalculatedDataLookup } from "tachi-common";
import { PBDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import DropdownIndicatorCell from "../cells/DropdownIndicatorCell";
import IndexCell from "../cells/IndexCell";
import RankingCell, { RankingViewMode } from "../cells/RankingCell";
@@ -2,9 +2,8 @@ import { NumericSOV, StrSOV } from "util/sorts";
import { CreateDefaultScoreSearchParams } from "util/tables/create-search";
import useScoreRatingAlg from "components/util/useScoreRatingAlg";
import React, { useState } from "react";
import { Game, IDStrings, integer, ScoreCalculatedDataLookup } from "tachi-common";
import { Game, IDStrings, integer, ScoreCalculatedDataLookup, Playtype } from "tachi-common";
import { ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import DifficultyCell from "../cells/DifficultyCell";
import IndicatorsCell from "../cells/IndicatorsCell";
import TimestampCell from "../cells/TimestampCell";
@@ -27,6 +26,7 @@ export default function ScoreTable<I extends IDStrings = IDStrings>({
userCol = false,
game,
alg,
noTopDisplayStr,
}: {
dataset: ScoreDataset<I>;
pageLen?: integer;
@@ -34,6 +34,7 @@ export default function ScoreTable<I extends IDStrings = IDStrings>({
userCol?: boolean;
game: Game;
alg?: ScoreCalculatedDataLookup[I];
noTopDisplayStr?: boolean;
}) {
const defaultRating = useScoreRatingAlg(game, playtype);
const [rating, setRating] = useState(alg ?? defaultRating);
@@ -53,6 +54,7 @@ export default function ScoreTable<I extends IDStrings = IDStrings>({
return (
<TachiTable
noTopDisplayStr={noTopDisplayStr}
dataset={dataset}
pageLen={pageLen}
headers={headers}
@@ -14,7 +14,7 @@ import {
SessionCalculatedDataLookup,
SessionDocument,
} from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import IndexCell from "../cells/IndexCell";
import SelectableRating from "../components/SelectableRating";
import TachiTable, { Header, ZTableTHProps } from "../components/TachiTable";
+242
View File
@@ -0,0 +1,242 @@
import { CreateUserMap } from "util/data";
import { FormatTime, MillisToSince } from "util/time";
import { NO_OP } from "util/misc";
import Icon from "components/util/Icon";
import React, { useState } from "react";
import { Col, Row } from "react-bootstrap";
import { FormatChart, SessionDocument, UserDocument } from "tachi-common";
import ScoreTable from "components/tables/scores/ScoreTable";
import { ScoreDataset } from "types/tables";
import Divider from "components/util/Divider";
import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown";
import useApiQuery from "components/util/query/useApiQuery";
import ApiError from "components/util/ApiError";
import Loading from "components/util/Loading";
import { SessionReturns } from "types/api-returns";
import LinkButton from "components/util/LinkButton";
import { ClumpedActivity, ClumpedActivityScores } from "types/tachi";
import { SetState } from "types/react";
import Select from "components/util/Select";
// Records activity for a group of users on a GPT. Also used for single users, sometimes.P
export default function UGPTActivity({
data,
users,
duration,
setDuration,
}: {
data: ClumpedActivity;
users: Array<UserDocument>;
duration: "month" | "3month" | "year";
setDuration: SetState<"month" | "3month" | "year">;
}) {
const rivalMap = CreateUserMap(users);
return (
<>
<Select value={duration} setValue={setDuration}>
<option value="month">Past Month</option>
<option value="3month">Past 3 Months</option>
<option value="year">Past Year</option>
</Select>
<Col xs={12} className="text-center">
<div className="timeline timeline-2">
<div className="timeline-bar"></div>
{data.map((e) => {
const rival = rivalMap.get(
e.type === "SCORES" ? e.scores[0]?.userID : e.userID
);
if (!rival) {
return <div>This user doesn't exist? Whoops.</div>;
}
return e.type === "SCORES" ? (
<ScoresActivity data={e} rival={rival} />
) : (
<SessionActivity data={e} rival={rival} />
);
})}
<div className="timeline-item">
<div className="timeline-item">
<div className="timeline-badge bg-success"></div>
<div
className="timeline-content d-flex"
style={{
flexDirection: "column",
flexWrap: "wrap",
marginRight: "2rem",
}}
>
This is the end of the recent activity!
</div>
</div>
</div>
</div>
</Col>
</>
);
}
function ScoresActivity({ data, rival }: { data: ClumpedActivityScores; rival: UserDocument }) {
const { game, playtype } = data.scores[0];
const [show, setShow] = useState(false);
let subMessage;
if (data.scores.length === 1) {
const score0 = data.scores[0];
subMessage = `a score on ${FormatChart(
score0.game,
score0.__related.song,
score0.__related.chart
)}`;
} else {
subMessage = `${data.scores.length} scores`;
}
const dataset: ScoreDataset = data.scores.map((e, i) => ({
...e,
__related: {
...e.__related,
index: i,
user: rival,
},
}));
return (
<div className="timeline-item timeline-hover my-4">
<div className="timeline-badge bg-warning"></div>
<div
className="timeline-content d-flex"
style={{
flexDirection: "column",
flexWrap: "wrap",
marginRight: "2rem",
}}
>
<div
className="d-flex align-items-center justify-content-between"
onClick={() => setShow(!show)}
>
<div className="mr-3" style={{ width: "70%", textAlign: "left" }}>
<Icon
type={`chevron-${show ? "down" : "right"}`}
style={{
fontSize: "0.75rem",
}}
/>
<span style={{ fontSize: "1.15rem" }} className="ml-2">
{rival.username} highlighted {subMessage}!
</span>
</div>
<div style={{ textAlign: "right" }}>
{MillisToSince(data.scores[0].timeAchieved ?? 0)}
<br />
<span className="text-muted font-italic text-right">
{FormatTime(data.scores[0].timeAchieved ?? 0)}
</span>
</div>
</div>
{show && (
<>
<Divider />
<ScoreTable
noTopDisplayStr
dataset={dataset}
game={game}
playtype={playtype}
/>
</>
)}
</div>
</div>
);
}
function SessionActivity({
data,
rival,
}: {
data: { type: "SESSION" } & SessionDocument;
rival: UserDocument;
}) {
const [show, setShow] = useState(false);
return (
<div className="timeline-item timeline-hover">
<div className="timeline-badge bg-secondary"></div>
<div
className="timeline-content d-flex"
style={{
flexDirection: "column",
flexWrap: "wrap",
marginRight: "2rem",
}}
>
<div
className="d-flex align-items-center justify-content-between"
onClick={() => setShow(!show)}
>
<div className="mr-3" style={{ width: "70%", textAlign: "left" }}>
<Icon
type={`chevron-${show ? "down" : "right"}`}
style={{
fontSize: "0.75rem",
}}
/>
<span className="ml-2">
{rival.username} had a session '{data.name}' with{" "}
{data.scoreInfo.length}{" "}
{data.scoreInfo.length === 1 ? "score" : "scores"}.
</span>
<br />
{data.desc && <span className="text-muted">{data.desc}</span>}
</div>
<div style={{ textAlign: "right" }} className="mr-1">
{MillisToSince(data.timeStarted ?? 0)}
<br />
<span className="text-muted font-italic text-right">
{FormatTime(data.timeStarted ?? 0)}
</span>
</div>
</div>
{show && <SessionShower sessionID={data.sessionID} />}
</div>
</div>
);
}
function SessionShower({ sessionID }: { sessionID: string }) {
const { data, error } = useApiQuery<SessionReturns>(`/sessions/${sessionID}`);
if (error) {
return <ApiError error={error} />;
}
if (!data) {
return <Loading />;
}
return (
<Row>
<SessionRaiseBreakdown sessionData={data} setScores={NO_OP} />
<Col xs={12}>
<Divider />
</Col>
<div className="d-flex w-100 justify-content-center">
<LinkButton
className="btn-outline-primary"
to={`/dashboard/users/${data.user.username}/games/${data.session.game}/${data.session.playtype}/sessions/${sessionID}`}
>
View Full Session
</LinkButton>
</div>
</Row>
);
}
+1 -1
View File
@@ -6,7 +6,7 @@ import Divider from "components/util/Divider";
import React from "react";
import { Game, UserDocument } from "tachi-common";
import { UGPTStatsReturn } from "types/api-returns";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import ProfileBadges from "./ProfileBadges";
import ProfilePicture from "./ProfilePicture";
import RankingData from "./UGPTRankingData";
@@ -1,12 +1,10 @@
import { APIFetchV1 } from "util/api";
import Loading from "components/util/Loading";
import useApiQuery from "components/util/query/useApiQuery";
import { UserContext } from "context/UserContext";
import React, { useContext } from "react";
import React from "react";
import { useQuery } from "react-query";
import { integer, UserDocument, ShowcaseStatDetails } from "tachi-common";
import { integer, ShowcaseStatDetails } from "tachi-common";
import { UGPTPreferenceStatsReturn } from "types/api-returns";
import { GamePT, UGPT } from "types/react";
import { UGPT } from "types/react";
import { StatDisplay } from "./UGPTStatShowcase";
export default function UGPTStatContainer({
@@ -12,7 +12,7 @@ import {
ShowcaseStatDetails,
} from "tachi-common";
import { GamePT, SetState } from "types/react";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import { SongChartsSearch } from "types/api-returns";
import DebounceSearch from "components/util/DebounceSearch";
import Muted from "components/util/Muted";
@@ -26,7 +26,7 @@ import {
} from "tachi-common";
import { UGPTPreferenceStatsReturn } from "types/api-returns";
import { GamePT, UGPT } from "types/react";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import UGPTStatContainer from "./UGPTStatContainer";
import UGPTStatCreator from "./UGPTStatCreator";
+1 -1
View File
@@ -1,5 +1,5 @@
import { Game, GetGamePTConfig } from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import useLUGPTSettings from "./useLUGPTSettings";
export function useBucket(game: Game, playtype: Playtype) {
@@ -6,7 +6,7 @@ import {
SessionCalculatedDataLookup,
UGSRatingsLookup,
} from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
import useLUGPTSettings from "./useLUGPTSettings";
export default function useScoreRatingAlg<I extends IDStrings = IDStrings>(
+1 -2
View File
@@ -1,6 +1,5 @@
import { useMemo } from "react";
import { UserDocument } from "tachi-common";
import { GamePT, UGPT } from "types/react";
import { UGPT } from "types/react";
export default function useUGPTBase({ reqUser, game, playtype }: UGPT) {
return useMemo(
+10
View File
@@ -218,3 +218,13 @@ export interface ImportsReturn {
imports: Array<ImportDocument>;
users: Array<UserDocument>;
}
export interface ActivityReturn {
recentSessions: Array<SessionDocument>;
songs: Array<SongDocument>;
charts: Array<ChartDocument>;
recentlyHighlightedScores: Array<ScoreDocument>;
users: Array<UserDocument>;
}
+12 -2
View File
@@ -1,3 +1,13 @@
import { Game, Playtypes } from "tachi-common";
import { ChartDocument, ScoreDocument, SessionDocument, SongDocument } from "tachi-common";
export type Playtype = Playtypes[Game];
export type ClumpedActivityScores = {
type: "SCORES";
scores: Array<ScoreDocument & { __related: { song: SongDocument; chart: ChartDocument } }>;
};
export type ClumpedActivity = Array<
| ({
type: "SESSION";
} & SessionDocument)
| ClumpedActivityScores
>;
+70
View File
@@ -0,0 +1,70 @@
import { ActivityReturn } from "types/api-returns";
import { ClumpedActivity, ClumpedActivityScores } from "types/tachi";
import { ONE_HOUR } from "./constants/time";
import { CreateSongMap, CreateChartMap } from "./data";
import { NumericSOV } from "./sorts";
/**
* Given recent activity data, clump it together so it's easier to work with,
* by joining repeat-highlighted scores from the same user.
*/
export function ClumpActivity(data: ActivityReturn): ClumpedActivity {
const songMap = CreateSongMap(data.songs);
const chartMap = CreateChartMap(data.charts);
const clumped: ClumpedActivity = [];
let clump: ClumpedActivityScores["scores"] = [];
let curUserID: number | null = null;
let lastTime: number | null = null;
for (const score of data.recentlyHighlightedScores
.slice(0)
.sort(NumericSOV((x) => x.timeAchieved ?? -Infinity, true))) {
const song = songMap.get(score.songID);
const chart = chartMap.get(score.chartID);
if (!song || !chart) {
console.warn(`Failed to resolve song or chart ${score.songID}, ${chart?.chartID}.`);
continue;
}
if (
(curUserID !== score.userID && curUserID !== null) ||
(lastTime !== null && lastTime - (score.timeAchieved ?? 0) > ONE_HOUR * 8)
) {
clumped.push({ type: "SCORES", scores: clump });
clump = [];
}
curUserID = score.userID;
lastTime = score.timeAchieved;
clump.push({
...score,
__related: {
song,
chart,
},
});
}
if (clump.length !== 0) {
clumped.push({ type: "SCORES", scores: clump });
}
for (const session of data.recentSessions) {
clumped.push({
type: "SESSION",
...session,
});
}
return clumped.sort(
NumericSOV(
(x) => (x.type === "SESSION" ? x.timeEnded : x.scores[0]!.timeAchieved ?? -Infinity),
true
)
);
}
+1 -1
View File
@@ -12,8 +12,8 @@ import {
ScoreCalculatedDataLookup,
ScoreDocument,
SessionDocument,
Playtype,
} from "tachi-common";
import { Playtype } from "types/tachi";
import fjsh from "fast-json-stable-hash";
export function RFA<T>(arr: T[]): T {
+1 -1
View File
@@ -9,7 +9,7 @@ import {
IDStrings,
} from "tachi-common";
import { ComparePBsDataset, FolderDataset, PBDataset, ScoreDataset } from "types/tables";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
function GetBMSTableVal(chart: ChartDocument<"bms:7K" | "bms:14K">, key: string) {
for (const table of chart.data.tableFolders) {
+1 -1
View File
@@ -1,5 +1,5 @@
import { Game, GPTTierlists, IDStrings, IIDX_LAMPS, ScoreDocument, SDVX_LAMPS } from "tachi-common";
import { Playtype } from "types/tachi";
import { Playtype } from "tachi-common";
type ScaleAchievedFns = {
[I in IDStrings]: {
+67
View File
@@ -0,0 +1,67 @@
import db from "external/mongo/db";
import { ONE_MONTH } from "lib/constants/time";
import { GetRelevantSongsAndCharts } from "utils/db";
import { GetUsersWithIDs } from "utils/user";
import type { Game, integer, Playtype } from "tachi-common";
/**
* Retrieves recent activity for this group of users for this GPT.
*
* At the moment, this retrieves the following events:
* - Recent Sessions
* - Recent Highlighted Scores
*
* It will support:
* - Recently achieved goals
* - Recently achieved quests
*/
export async function GetRecentActivity(
userIDs: Array<integer>,
game: Game,
playtype: Playtype,
timespan = ONE_MONTH
) {
// by default, limit to only things that happened in the past 31 days.
const RECENT = Date.now() - timespan;
const recentlyHighlightedScores = await db.scores.find(
{
userID: { $in: userIDs },
game,
playtype,
highlight: true,
timeAchieved: { $gte: RECENT },
},
{
sort: {
timeAchieved: -1,
},
}
);
const { songs, charts } = await GetRelevantSongsAndCharts(recentlyHighlightedScores, game);
const recentSessions = await db.sessions.find(
{
userID: { $in: userIDs },
game,
playtype,
timeStarted: { $gte: RECENT },
},
{
sort: {
timeAchieved: -1,
},
}
);
const users = await GetUsersWithIDs(userIDs);
return {
recentSessions,
recentlyHighlightedScores,
songs,
charts,
users,
};
}
+13 -1
View File
@@ -1,4 +1,5 @@
import db from "external/mongo/db";
import { GetRecentActivity } from "lib/activity/activity";
import { SetRivalsFailReasons } from "lib/constants/err-codes";
import CreateLogCtx from "lib/logger/logger";
import { SendSetRivalNotification } from "lib/notifications/notification-wrappers";
@@ -6,7 +7,7 @@ import { FormatGame } from "tachi-common";
import { ArrayDiff } from "utils/misc";
import { GetUsersWithIDs, GetUserWithIDGuaranteed } from "utils/user";
import type { BulkWriteUpdateOneOperation } from "mongodb";
import type { Game, integer, Playtype, PBScoreDocument } from "tachi-common";
import type { Game, integer, PBScoreDocument, Playtype, UserDocument } from "tachi-common";
const logger = CreateLogCtx(__filename);
@@ -270,3 +271,14 @@ export async function UpdatePlayersRivalRankings(userID: integer, game: Game, pl
await db["personal-bests"].bulkWrite(bwrite, { ordered: false });
}
export async function GetRivalRecentActivity(
user: UserDocument,
game: Game,
playtype: Playtype,
timespan: number
) {
const rivalIDs = await GetRivalIDs(user.id, game, playtype);
return GetRecentActivity(rivalIDs, game, playtype, timespan);
}
@@ -2,7 +2,14 @@ import { RequireAuthedAsUser } from "../../../../middleware";
import { Router } from "express";
import db from "external/mongo/db";
import { SetRivalsFailReasons } from "lib/constants/err-codes";
import { GetChallengerUsers, GetRivalIDs, GetRivalUsers, SetRivals } from "lib/rivals/rivals";
import { ONE_MONTH, ONE_YEAR } from "lib/constants/time";
import {
GetChallengerUsers,
GetRivalIDs,
GetRivalRecentActivity,
GetRivalUsers,
SetRivals,
} from "lib/rivals/rivals";
import p from "prudence";
import { RequirePermissions } from "server/middleware/auth";
import prValidate from "server/middleware/prudence-validate";
@@ -167,4 +174,44 @@ router.get("/pb-leaderboard", async (req, res) => {
});
});
/**
* Retrieve activity for this user's ste of rivals.
*
* @name GET /api/v1/users/:userID/games/:game/:playtype/rivals/activity
*/
router.get(
"/activity",
prValidate({ duration: p.optional(p.isIn("month", "3month", "year")) }),
async (req, res) => {
const { user, game, playtype } = GetUGPT(req);
const duration = (req.query.duration as "3month" | "month" | "year" | undefined) ?? "month";
let timespan;
switch (duration) {
case "month": {
timespan = ONE_MONTH;
break;
}
case "3month": {
timespan = ONE_MONTH * 3;
break;
}
case "year":
timespan = ONE_YEAR;
}
const recentActivity = await GetRivalRecentActivity(user, game, playtype, timespan);
return res.status(200).json({
success: true,
description: `Retrieved recent rival activity.`,
body: recentActivity,
});
}
);
export default router;