diff --git a/client/src/components/activity/Activity.tsx b/client/src/components/activity/Activity.tsx index 7ad7d81d3..fc665a694 100644 --- a/client/src/components/activity/Activity.tsx +++ b/client/src/components/activity/Activity.tsx @@ -9,7 +9,6 @@ import SessionRaiseBreakdown from "components/sessions/SessionRaiseBreakdown"; import ScoreTable from "components/tables/scores/ScoreTable"; import ApiError from "components/util/ApiError"; import Divider from "components/util/Divider"; -import GoalLink from "components/util/GoalLink"; import Icon from "components/util/Icon"; import LinkButton from "components/util/LinkButton"; import Loading from "components/util/Loading"; @@ -31,6 +30,7 @@ import { ClumpedActivityScores, ClumpedActivitySession, } from "types/tachi"; +import { InnerQuestSectionGoal } from "components/targets/quests/Quest"; // Records activity for a group of users on a GPT. Also used for single users. export default function Activity({ @@ -161,7 +161,7 @@ function ActivityInner({ user={user} /> ); - case "GOAL_ACHIEVEMENT": + case "GOAL_ACHIEVEMENTS": return ( e.__related.goal.name).join(", "), 100); + } return (
-
+
setShow(!show)}>
- + + achieved{" "} - - {prettyGame && ` in ${prettyGame}`}! + {subMessage}! + {mutedText && ( + <> +
+ {mutedText} + + )}
- {MillisToSince(data.sub.timeAchieved ?? 0)} + {MillisToSince(data.goals[0]?.timeAchieved ?? 0)}
- {FormatTime(data.sub.timeAchieved ?? 0)} + {FormatTime(data.goals[0]?.timeAchieved ?? 0)}
+ + {show && ( + <> + +
+ {data.goals.map((e) => ( + + ))} +
+ + )}
); diff --git a/client/src/components/targets/quests/Quest.tsx b/client/src/components/targets/quests/Quest.tsx index 97481e318..d74cda11b 100644 --- a/client/src/components/targets/quests/Quest.tsx +++ b/client/src/components/targets/quests/Quest.tsx @@ -9,7 +9,13 @@ import Muted from "components/util/Muted"; import { UserContext } from "context/UserContext"; import React, { useContext, useState } from "react"; import { Badge, Button } from "react-bootstrap"; -import { FormatGame, GoalDocument, QuestDocument, QuestSection } from "tachi-common"; +import { + FormatGame, + GoalDocument, + GoalSubscriptionDocument, + QuestDocument, + QuestSection, +} from "tachi-common"; import { GamePT } from "types/react"; import { TargetsContext } from "context/TargetsContext"; import QuickTooltip from "components/layout/misc/QuickTooltip"; @@ -195,14 +201,16 @@ export function InnerQuestSectionGoal({ goal, note, dependencies, + goalSubOverride, }: { goal: GoalDocument; note?: string; dependencies?: string[]; + goalSubOverride?: GoalSubscriptionDocument; }) { const { goalSubs } = useContext(TargetsContext); - const goalSub = goalSubs.get(goal.goalID); + const goalSub = goalSubOverride ?? goalSubs.get(goal.goalID); if (!goalSub) { return ( diff --git a/client/src/types/tachi.ts b/client/src/types/tachi.ts index 17c5e3382..fd1778fcd 100644 --- a/client/src/types/tachi.ts +++ b/client/src/types/tachi.ts @@ -25,11 +25,10 @@ export type ClumpedActivityClassAchievement = { } & ClassAchievementDocument; export type ClumpedActivityGoalAchievement = { - type: "GOAL_ACHIEVEMENT"; + type: "GOAL_ACHIEVEMENTS"; // redundant, but convenient. userID: integer; - sub: GoalSubscriptionDocument; - goal: GoalDocument; + goals: Array; }; export type ClumpedActivityQuestAchievement = { diff --git a/client/src/util/activity.ts b/client/src/util/activity.ts index d9d47cc6f..c1abdf4f6 100644 --- a/client/src/util/activity.ts +++ b/client/src/util/activity.ts @@ -1,5 +1,9 @@ import { ActivityReturn, RecordActivityReturn } from "types/api-returns"; -import { ClumpedActivity, ClumpedActivityScores } from "types/tachi"; +import { + ClumpedActivity, + ClumpedActivityGoalAchievement, + ClumpedActivityScores, +} from "types/tachi"; import { ONE_HOUR } from "./constants/time"; import { CreateSongMap, CreateChartMap, CreateGoalMap } from "./data"; import { CreateQuestMap } from "./misc"; @@ -13,7 +17,8 @@ const SORT_ACTIVITY = NumericSOV((x: ClumpedActivity[0]) => { return x.scores[0]?.timeAchieved ?? -Infinity; case "CLASS_ACHIEVEMENT": return x.timeAchieved; - case "GOAL_ACHIEVEMENT": + case "GOAL_ACHIEVEMENTS": + return x.goals[0]?.timeAchieved ?? -Infinity; case "QUEST_ACHIEVEMENT": return x.sub.timeAchieved ?? -Infinity; } @@ -95,24 +100,46 @@ export function ClumpActivity(data: ActivityReturn | RecordActivityReturn): Clum } const goalMap = CreateGoalMap(data.goals); - for (const sub of data.goalSubs) { + + let goalClump: ClumpedActivityGoalAchievement["goals"] = []; + // reset last clumps. I realise this is super copypasta, but we'll live. + curUserID = null; + lastTime = null; + + for (const sub of data.goalSubs + .slice(0) + .sort(NumericSOV((x) => x.timeAchieved ?? -Infinity, true))) { const goal = goalMap.get(sub.goalID); if (!goal) { - console.warn( - `Couldn't find parent goal for recently-achieved-sub '${sub.goalID}. Skipping.'` - ); + console.warn(`Failed to resolve goal ${sub.goalID}. Skipping.`); continue; } - clumped.push({ - type: "GOAL_ACHIEVEMENT", - userID: sub.userID, - sub, - goal, + if ( + (curUserID !== sub.userID && curUserID !== null) || + (lastTime !== null && lastTime - (sub.timeAchieved ?? 0) > ONE_HOUR * 8) + ) { + clumped.push({ type: "GOAL_ACHIEVEMENTS", userID: sub.userID, goals: goalClump }); + + goalClump = []; + } + + curUserID = sub.userID; + lastTime = sub.timeAchieved; + + goalClump.push({ + ...sub, + __related: { + goal, + }, }); } + if (goalClump.length !== 0) { + clumped.push({ type: "GOAL_ACHIEVEMENTS", userID: goalClump[0].userID, goals: goalClump }); + } + const questMap = CreateQuestMap(data.quests); for (const sub of data.questSubs) {