mirror of
https://github.com/zkldi/Tachi.git
synced 2026-10-11 08:48:15 +03:00
feat: rival activity
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
+1
-1
@@ -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;
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
+31
-2
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+1
-1
@@ -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" />
|
||||
|
||||
+55
-44
@@ -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) {
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,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,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(
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
>;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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]: {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+48
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user