From 2a32fb5db2c1cc793202321affd732d9c824a3f9 Mon Sep 17 00:00:00 2001 From: zkldi <20380519+zkldi@users.noreply.github.com> Date: Fri, 11 Nov 2022 02:50:32 +0000 Subject: [PATCH] feat: rival activity --- .../games/_game/_playtype/ActivityPage.tsx | 35 +++ .../_playtype/folders/FolderTablePage.tsx | 2 +- .../_playtype/folders/SpecificFolderPage.tsx | 2 +- .../_playtype/rivals/RivalsActivityPage.tsx | 33 ++- .../_game/_playtype/rivals/RivalsMainPage.tsx | 2 +- .../_playtype/rivals/RivalsManagePage.tsx | 99 +++---- .../_game/_playtype/targets/TargetsPage.tsx | 4 +- client/src/components/imports/ImportInfo.tsx | 2 +- .../components/layout/header/SearchBar.tsx | 2 +- .../tables/cells/ScoreCellLarge.tsx | 2 +- .../tables/dropdowns/GPTDropdownSettings.tsx | 2 +- .../components/tables/folders/FolderTable.tsx | 2 +- .../components/tables/headers/GameHeaders.tsx | 2 +- .../history-scores/HistoryScoreTable.tsx | 2 +- client/src/components/tables/pbs/PBTable.tsx | 2 +- .../components/tables/scores/ScoreTable.tsx | 6 +- .../tables/sessions/GenericSessionTable.tsx | 2 +- client/src/components/user/UGPTActivity.tsx | 242 ++++++++++++++++++ client/src/components/user/UGPTHeader.tsx | 2 +- .../src/components/user/UGPTStatContainer.tsx | 8 +- .../src/components/user/UGPTStatCreator.tsx | 2 +- .../src/components/user/UGPTStatShowcase.tsx | 2 +- client/src/components/util/useBucket.tsx | 2 +- .../src/components/util/useScoreRatingAlg.tsx | 2 +- client/src/components/util/useUGPTBase.tsx | 3 +- client/src/types/api-returns.ts | 10 + client/src/types/tachi.ts | 14 +- client/src/util/activity.ts | 70 +++++ client/src/util/misc.ts | 2 +- client/src/util/tables/create-search.ts | 2 +- client/src/util/tierlist.ts | 2 +- server/src/lib/activity/activity.ts | 67 +++++ server/src/lib/rivals/rivals.ts | 14 +- .../games/_game/_playtype/rivals/router.ts | 49 +++- 34 files changed, 613 insertions(+), 81 deletions(-) create mode 100644 client/src/app/pages/dashboard/users/games/_game/_playtype/ActivityPage.tsx create mode 100644 client/src/components/user/UGPTActivity.tsx create mode 100644 client/src/util/activity.ts create mode 100644 server/src/lib/activity/activity.ts diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/ActivityPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/ActivityPage.tsx new file mode 100644 index 000000000..b5ab0702c --- /dev/null +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/ActivityPage.tsx @@ -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( + `/users/${reqUser.id}/games/${game}/${playtype}/activity?duration=${duration}` + ); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + const joined = ClumpActivity(data); + + return ( + + ); +} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx index c3757b39c..c9fd4dea9 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/FolderTablePage.tsx @@ -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; diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx index 93033952e..9bc97bb55 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/folders/SpecificFolderPage.tsx @@ -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 { diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsActivityPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsActivityPage.tsx index 5dc4403a6..3199a17bd 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsActivityPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsActivityPage.tsx @@ -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
RivalsActivityPage
; + const [duration, setDuration] = useState<"month" | "3month" | "year">("month"); + + const { data, error } = useApiQuery( + `/users/${reqUser.id}/games/${game}/${playtype}/rivals/activity?duration=${duration}` + ); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + const joined = ClumpActivity(data); + + return ( + + ); } diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx index a09328a1d..94c479e55 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsMainPage.tsx @@ -47,7 +47,7 @@ export default function RivalsMainPage({ - Activity + Rival Activity diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx index 6298cfd15..9c20916f4 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/rivals/RivalsManagePage.tsx @@ -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. */} + + + + + +
You have unsaved changes!
+ + +
+ + + )} {rivals.map((e) => ( @@ -144,47 +196,6 @@ function RivalsOverviewPage({ )} - {/* kind of a stupid way to check whether the array has changed or not, but who cares. */} - {currentRivals.toString() !== rivals.toString() && ( - <> - - - - - - - - )} - { 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!" /> )} diff --git a/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/TargetsPage.tsx b/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/TargetsPage.tsx index 6e6e0b69b..aa01bf178 100644 --- a/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/TargetsPage.tsx +++ b/client/src/app/pages/dashboard/users/games/_game/_playtype/targets/TargetsPage.tsx @@ -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) { diff --git a/client/src/components/imports/ImportInfo.tsx b/client/src/components/imports/ImportInfo.tsx index f7da9a300..1b76fb8a7 100644 --- a/client/src/components/imports/ImportInfo.tsx +++ b/client/src/components/imports/ImportInfo.tsx @@ -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; diff --git a/client/src/components/layout/header/SearchBar.tsx b/client/src/components/layout/header/SearchBar.tsx index f9b6a8ed8..8ba7b3f14 100644 --- a/client/src/components/layout/header/SearchBar.tsx +++ b/client/src/components/layout/header/SearchBar.tsx @@ -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 { diff --git a/client/src/components/tables/cells/ScoreCellLarge.tsx b/client/src/components/tables/cells/ScoreCellLarge.tsx index fd9ce2417..1cf955c6e 100644 --- a/client/src/components/tables/cells/ScoreCellLarge.tsx +++ b/client/src/components/tables/cells/ScoreCellLarge.tsx @@ -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: { diff --git a/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx b/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx index ea9a32037..0c05b513e 100644 --- a/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx +++ b/client/src/components/tables/dropdowns/GPTDropdownSettings.tsx @@ -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"; diff --git a/client/src/components/tables/folders/FolderTable.tsx b/client/src/components/tables/folders/FolderTable.tsx index 6ec8df73d..acc1b4edd 100644 --- a/client/src/components/tables/folders/FolderTable.tsx +++ b/client/src/components/tables/folders/FolderTable.tsx @@ -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"; diff --git a/client/src/components/tables/headers/GameHeaders.tsx b/client/src/components/tables/headers/GameHeaders.tsx index 08e11cf2a..423f1513e 100644 --- a/client/src/components/tables/headers/GameHeaders.tsx +++ b/client/src/components/tables/headers/GameHeaders.tsx @@ -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"; diff --git a/client/src/components/tables/history-scores/HistoryScoreTable.tsx b/client/src/components/tables/history-scores/HistoryScoreTable.tsx index 1de8582f8..0d535c50c 100644 --- a/client/src/components/tables/history-scores/HistoryScoreTable.tsx +++ b/client/src/components/tables/history-scores/HistoryScoreTable.tsx @@ -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"; diff --git a/client/src/components/tables/pbs/PBTable.tsx b/client/src/components/tables/pbs/PBTable.tsx index aadc52f87..29ba45861 100644 --- a/client/src/components/tables/pbs/PBTable.tsx +++ b/client/src/components/tables/pbs/PBTable.tsx @@ -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"; diff --git a/client/src/components/tables/scores/ScoreTable.tsx b/client/src/components/tables/scores/ScoreTable.tsx index a26714491..d21c2b76b 100644 --- a/client/src/components/tables/scores/ScoreTable.tsx +++ b/client/src/components/tables/scores/ScoreTable.tsx @@ -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({ userCol = false, game, alg, + noTopDisplayStr, }: { dataset: ScoreDataset; pageLen?: integer; @@ -34,6 +34,7 @@ export default function ScoreTable({ 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({ return ( ; + duration: "month" | "3month" | "year"; + setDuration: SetState<"month" | "3month" | "year">; +}) { + const rivalMap = CreateUserMap(users); + + return ( + <> + + +
+
+ {data.map((e) => { + const rival = rivalMap.get( + e.type === "SCORES" ? e.scores[0]?.userID : e.userID + ); + + if (!rival) { + return
This user doesn't exist? Whoops.
; + } + + return e.type === "SCORES" ? ( + + ) : ( + + ); + })} +
+
+
+
+ This is the end of the recent activity! +
+
+
+
+ + + ); +} + +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 ( +
+
+
+
setShow(!show)} + > +
+ + + {rival.username} highlighted {subMessage}! + +
+ +
+ {MillisToSince(data.scores[0].timeAchieved ?? 0)} +
+ + {FormatTime(data.scores[0].timeAchieved ?? 0)} + +
+
+ + {show && ( + <> + + + + )} +
+
+ ); +} + +function SessionActivity({ + data, + rival, +}: { + data: { type: "SESSION" } & SessionDocument; + rival: UserDocument; +}) { + const [show, setShow] = useState(false); + + return ( +
+
+
+
setShow(!show)} + > +
+ + + {rival.username} had a session '{data.name}' with{" "} + {data.scoreInfo.length}{" "} + {data.scoreInfo.length === 1 ? "score" : "scores"}. + +
+ {data.desc && {data.desc}} +
+ +
+ {MillisToSince(data.timeStarted ?? 0)} +
+ + {FormatTime(data.timeStarted ?? 0)} + +
+
+ {show && } +
+
+ ); +} + +function SessionShower({ sessionID }: { sessionID: string }) { + const { data, error } = useApiQuery(`/sessions/${sessionID}`); + + if (error) { + return ; + } + + if (!data) { + return ; + } + + return ( + + + + + +
+ + View Full Session + +
+
+ ); +} diff --git a/client/src/components/user/UGPTHeader.tsx b/client/src/components/user/UGPTHeader.tsx index 96f450c8b..ead27d65d 100644 --- a/client/src/components/user/UGPTHeader.tsx +++ b/client/src/components/user/UGPTHeader.tsx @@ -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"; diff --git a/client/src/components/user/UGPTStatContainer.tsx b/client/src/components/user/UGPTStatContainer.tsx index 134068ed4..1dcd5ec7b 100644 --- a/client/src/components/user/UGPTStatContainer.tsx +++ b/client/src/components/user/UGPTStatContainer.tsx @@ -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({ diff --git a/client/src/components/user/UGPTStatCreator.tsx b/client/src/components/user/UGPTStatCreator.tsx index 35ccd2a23..84360959b 100644 --- a/client/src/components/user/UGPTStatCreator.tsx +++ b/client/src/components/user/UGPTStatCreator.tsx @@ -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"; diff --git a/client/src/components/user/UGPTStatShowcase.tsx b/client/src/components/user/UGPTStatShowcase.tsx index 3b3b95396..333630e47 100644 --- a/client/src/components/user/UGPTStatShowcase.tsx +++ b/client/src/components/user/UGPTStatShowcase.tsx @@ -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"; diff --git a/client/src/components/util/useBucket.tsx b/client/src/components/util/useBucket.tsx index 3a63c977b..ce6fa688d 100644 --- a/client/src/components/util/useBucket.tsx +++ b/client/src/components/util/useBucket.tsx @@ -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) { diff --git a/client/src/components/util/useScoreRatingAlg.tsx b/client/src/components/util/useScoreRatingAlg.tsx index 9f6e80c1a..f5782ab65 100644 --- a/client/src/components/util/useScoreRatingAlg.tsx +++ b/client/src/components/util/useScoreRatingAlg.tsx @@ -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( diff --git a/client/src/components/util/useUGPTBase.tsx b/client/src/components/util/useUGPTBase.tsx index 191a9d89f..3bc8da7c0 100644 --- a/client/src/components/util/useUGPTBase.tsx +++ b/client/src/components/util/useUGPTBase.tsx @@ -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( diff --git a/client/src/types/api-returns.ts b/client/src/types/api-returns.ts index 1cb169955..2979c1edc 100644 --- a/client/src/types/api-returns.ts +++ b/client/src/types/api-returns.ts @@ -218,3 +218,13 @@ export interface ImportsReturn { imports: Array; users: Array; } + +export interface ActivityReturn { + recentSessions: Array; + + songs: Array; + charts: Array; + recentlyHighlightedScores: Array; + + users: Array; +} diff --git a/client/src/types/tachi.ts b/client/src/types/tachi.ts index 046d57806..3a54324c4 100644 --- a/client/src/types/tachi.ts +++ b/client/src/types/tachi.ts @@ -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; +}; + +export type ClumpedActivity = Array< + | ({ + type: "SESSION"; + } & SessionDocument) + | ClumpedActivityScores +>; diff --git a/client/src/util/activity.ts b/client/src/util/activity.ts new file mode 100644 index 000000000..be41546f0 --- /dev/null +++ b/client/src/util/activity.ts @@ -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 + ) + ); +} diff --git a/client/src/util/misc.ts b/client/src/util/misc.ts index 7234f1660..dff85b12d 100644 --- a/client/src/util/misc.ts +++ b/client/src/util/misc.ts @@ -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(arr: T[]): T { diff --git a/client/src/util/tables/create-search.ts b/client/src/util/tables/create-search.ts index 3f611b100..52ca170fa 100644 --- a/client/src/util/tables/create-search.ts +++ b/client/src/util/tables/create-search.ts @@ -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) { diff --git a/client/src/util/tierlist.ts b/client/src/util/tierlist.ts index d137e63bb..5c7a04122 100644 --- a/client/src/util/tierlist.ts +++ b/client/src/util/tierlist.ts @@ -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]: { diff --git a/server/src/lib/activity/activity.ts b/server/src/lib/activity/activity.ts new file mode 100644 index 000000000..1a11bf74c --- /dev/null +++ b/server/src/lib/activity/activity.ts @@ -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, + 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, + }; +} diff --git a/server/src/lib/rivals/rivals.ts b/server/src/lib/rivals/rivals.ts index 94b68a709..a2a53be9f 100644 --- a/server/src/lib/rivals/rivals.ts +++ b/server/src/lib/rivals/rivals.ts @@ -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); +} diff --git a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts index 95ae0519e..38f03e392 100644 --- a/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts +++ b/server/src/server/router/api/v1/users/_userID/games/_game/_playtype/rivals/router.ts @@ -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;