diff --git a/app/features/scrims/actions/scrims.$id.server.ts b/app/features/scrims/actions/scrims.$id.server.ts index 42ecbfe05..9a73ed2e5 100644 --- a/app/features/scrims/actions/scrims.$id.server.ts +++ b/app/features/scrims/actions/scrims.$id.server.ts @@ -219,7 +219,13 @@ async function loadMapByMapContext({ ScrimMapListRepository.findMapListsByScrimPostId(post.id), ]); - if (Scrim.isTrackingLocked(maps, mapLists)) { + if ( + Scrim.isTrackingLocked({ + startTime: Scrim.getStartTime(post), + maps, + mapLists, + }) + ) { errorToast("Tracking is locked"); } diff --git a/app/features/scrims/core/Scrim.test.ts b/app/features/scrims/core/Scrim.test.ts index 4f81d4622..d5896e389 100644 --- a/app/features/scrims/core/Scrim.test.ts +++ b/app/features/scrims/core/Scrim.test.ts @@ -532,46 +532,78 @@ describe("sideOfUser", () => { describe("isTrackingLocked", () => { const ONE_HOUR_MS = 60 * 60 * 1000; const lockWindowMs = SCRIM_TRACKING_AUTO_LOCK_HOURS * ONE_HOUR_MS; + const now = 1_000_000_000; + const secondsAgo = (ms: number) => (now - ms) / 1000; test("returns false when no map list submitted yet", () => { - expect(isTrackingLocked([], [], Date.now())).toBe(false); + expect(isTrackingLocked({ startTime: secondsAgo(ONE_HOUR_MS), now })).toBe( + false, + ); }); test("returns false just inside the auto-lock window from list submission", () => { - const now = 1_000_000_000; - const updatedAt = (now - (lockWindowMs - ONE_HOUR_MS)) / 1000; - expect(isTrackingLocked([], [{ updatedAt }], now)).toBe(false); + const startTime = secondsAgo(lockWindowMs); + const updatedAt = secondsAgo(lockWindowMs - ONE_HOUR_MS); + expect( + isTrackingLocked({ startTime, mapLists: [{ updatedAt }], now }), + ).toBe(false); }); test("returns true just past the auto-lock window from list submission", () => { - const now = 1_000_000_000; - const updatedAt = (now - (lockWindowMs + ONE_HOUR_MS)) / 1000; - expect(isTrackingLocked([], [{ updatedAt }], now)).toBe(true); + const startTime = secondsAgo(lockWindowMs * 2); + const updatedAt = secondsAgo(lockWindowMs + ONE_HOUR_MS); + expect( + isTrackingLocked({ startTime, mapLists: [{ updatedAt }], now }), + ).toBe(true); }); test("uses the most recent reported map as the reference point", () => { - const now = 1_000_000_000; - const oldUpdatedAt = (now - lockWindowMs * 2) / 1000; - const recentMapSeconds = (now - ONE_HOUR_MS) / 1000; + const startTime = secondsAgo(lockWindowMs * 2); expect( - isTrackingLocked( - [{ reportedAt: recentMapSeconds }], - [{ updatedAt: oldUpdatedAt }], + isTrackingLocked({ + startTime, + maps: [{ reportedAt: secondsAgo(ONE_HOUR_MS) }], + mapLists: [{ updatedAt: secondsAgo(lockWindowMs * 2) }], now, - ), + }), ).toBe(false); }); test("uses the most recent list update when there are no reported maps", () => { - const now = 1_000_000_000; - const oldUpdatedAt = (now - lockWindowMs * 2) / 1000; - const recentUpdatedAt = (now - ONE_HOUR_MS) / 1000; + const startTime = secondsAgo(lockWindowMs * 2); expect( - isTrackingLocked( - [], - [{ updatedAt: oldUpdatedAt }, { updatedAt: recentUpdatedAt }], + isTrackingLocked({ + startTime, + mapLists: [ + { updatedAt: secondsAgo(lockWindowMs * 2) }, + { updatedAt: secondsAgo(ONE_HOUR_MS) }, + ], now, - ), + }), + ).toBe(false); + }); + + test("returns false when the map list was submitted long before a scrim that just started", () => { + const startTime = secondsAgo(ONE_HOUR_MS); + const updatedAt = secondsAgo(lockWindowMs * 10); + expect( + isTrackingLocked({ startTime, mapLists: [{ updatedAt }], now }), + ).toBe(false); + }); + + test("returns true once the auto-lock window has elapsed since the start time", () => { + const startTime = secondsAgo(lockWindowMs + ONE_HOUR_MS); + const updatedAt = secondsAgo(lockWindowMs * 10); + expect( + isTrackingLocked({ startTime, mapLists: [{ updatedAt }], now }), + ).toBe(true); + }); + + test("returns false for a scrim that has not started yet", () => { + const startTime = (now + lockWindowMs) / 1000; + const updatedAt = secondsAgo(lockWindowMs * 10); + expect( + isTrackingLocked({ startTime, mapLists: [{ updatedAt }], now }), ).toBe(false); }); }); diff --git a/app/features/scrims/core/Scrim.ts b/app/features/scrims/core/Scrim.ts index e800487ed..98b572575 100644 --- a/app/features/scrims/core/Scrim.ts +++ b/app/features/scrims/core/Scrim.ts @@ -157,23 +157,33 @@ export function sideOfUser(post: ScrimPost, userId: number): ScrimSide | null { /** * Returns true when map-by-map tracking is locked: the auto-lock window has * elapsed since the last activity (most recent reported map, falling back to - * the most recently updated submitted map list). Returns false when no map - * list has been submitted yet (tracking is not active). + * the most recently updated submitted map list). Activity happening before the + * scrim starts does not begin the window, it only starts running once the + * scrim is under way. Returns false when no map list has been submitted yet + * (tracking is not active). */ -export function isTrackingLocked( - maps: Pick[] = [], - mapLists: Pick[] = [], - now: number = Date.now(), -): boolean { +export function isTrackingLocked({ + startTime, + maps = [], + mapLists = [], + now = Date.now(), +}: { + startTime: number; + maps?: Pick[]; + mapLists?: Pick[]; + now?: number; +}): boolean { const latestReported = R.firstBy( maps.filter((m) => m.reportedAt !== null), [(m) => m.reportedAt!, "desc"], ); const latestList = R.firstBy(mapLists, [(l) => l.updatedAt, "desc"]); - const referenceSeconds = + const latestActivitySeconds = latestReported?.reportedAt ?? latestList?.updatedAt ?? null; - if (referenceSeconds === null) return false; + if (latestActivitySeconds === null) return false; + + const referenceSeconds = Math.max(latestActivitySeconds, startTime); const elapsedHours = (now - referenceSeconds * 1000) / (60 * 60 * 1000); diff --git a/app/features/scrims/loaders/scrims.$id.server.ts b/app/features/scrims/loaders/scrims.$id.server.ts index 89773e90b..09a6621fe 100644 --- a/app/features/scrims/loaders/scrims.$id.server.ts +++ b/app/features/scrims/loaders/scrims.$id.server.ts @@ -83,7 +83,11 @@ async function resolveMapByMap({ const pool = mapLists.length > 0 ? ScrimMapByMap.unionPool(mapLists) : null; const currentMap = maps.find((m) => m.reportedAt === null) ?? null; const viewerSide = Scrim.sideOfUser(post, user.id); - const locked = Scrim.isTrackingLocked(maps, mapLists); + const locked = Scrim.isTrackingLocked({ + startTime: Scrim.getStartTime(post), + maps, + mapLists, + }); const ownList = viewerSide ? mapLists.find((l) => l.side === viewerSide)