From 6860c276dd937cddf4abcc135ef6fd6fcc97fe1b Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Thu, 27 Aug 2026 08:17:42 +0200 Subject: [PATCH] Stop re-parsing dates in isStatusStillActive This runs per post, per render while scrolling, and showed up in the scroll profile as parseISO plus two Date objects. Date.parse returns NaN for an unparseable value and NaN > n is false, which is the same "not active" answer the date-fns version gave. Moved to liveNow/utils.ts alongside the other pure helpers so it can be tested without pulling in the feature's React and query imports. index.tsx already re-exports utils, so every existing import site is unchanged. Co-Authored-By: Claude Opus 5 --- src/features/liveNow/index.tsx | 15 +++----- .../liveNow/isStatusStillActive.test.ts | 35 +++++++++++++++++++ src/features/liveNow/utils.ts | 13 +++++++ 3 files changed, 53 insertions(+), 10 deletions(-) create mode 100644 src/features/liveNow/isStatusStillActive.test.ts diff --git a/src/features/liveNow/index.tsx b/src/features/liveNow/index.tsx index 0959b068bd..355a646f0e 100644 --- a/src/features/liveNow/index.tsx +++ b/src/features/liveNow/index.tsx @@ -6,7 +6,6 @@ import {moderateStatus} from '@bsky/sdk/moderation' import {msg} from '@lingui/core/macro' import {useLingui} from '@lingui/react' import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query' -import {isAfter, parseISO} from 'date-fns' import {uploadBlob} from '#/lib/api' import {imageToThumb} from '#/lib/api/resolve' @@ -23,7 +22,11 @@ import {useTickEveryMinute} from '#/state/shell' import {useDialogContext} from '#/components/Dialog' import * as Toast from '#/components/Toast' import {useAnalytics} from '#/analytics' -import {getLiveNowHost, getLiveServiceNames} from '#/features/liveNow/utils' +import { + getLiveNowHost, + getLiveServiceNames, + isStatusStillActive, +} from '#/features/liveNow/utils' import {app, com} from '#/lexicons' import * as bsky from '#/types/bsky' @@ -151,14 +154,6 @@ export function useActorStatus(actor?: bsky.profile.AnyProfileView) { }, [shadowed, config, tick, moderation]) } -export function isStatusStillActive(timeStr: string | undefined) { - if (!timeStr) return false - const now = new Date() - const expiry = parseISO(timeStr) - - return isAfter(expiry, now) -} - /** * Validates whether the live status is valid for display in the app. Does NOT * validate if the status is valid for the acting user e.g. as they go live. diff --git a/src/features/liveNow/isStatusStillActive.test.ts b/src/features/liveNow/isStatusStillActive.test.ts new file mode 100644 index 0000000000..79eac563eb --- /dev/null +++ b/src/features/liveNow/isStatusStillActive.test.ts @@ -0,0 +1,35 @@ +import {isStatusStillActive} from '#/features/liveNow/utils' + +describe('isStatusStillActive', () => { + const iso = (offsetMs: number) => + new Date(Date.now() + offsetMs).toISOString() + + it('is active for a future expiry', () => { + expect(isStatusStillActive(iso(60_000))).toBe(true) + }) + + it('is not active for a past expiry', () => { + expect(isStatusStillActive(iso(-60_000))).toBe(false) + }) + + it('is not active without a value', () => { + expect(isStatusStillActive(undefined)).toBe(false) + expect(isStatusStillActive('')).toBe(false) + }) + + /* + * The predicate reads an atproto `datetime`, which always carries an offset. + * An unparseable value yields NaN, and every NaN comparison is false, so a + * bad timestamp reads as "not live" rather than throwing or showing a stale + * live badge. + */ + it('is not active for an unparseable value', () => { + expect(isStatusStillActive('not-a-date')).toBe(false) + }) + + it('handles offsets and fractional seconds', () => { + const future = new Date(Date.now() + 3_600_000) + expect(isStatusStillActive(future.toISOString())).toBe(true) + expect(isStatusStillActive('2000-01-01T00:00:00.123+00:00')).toBe(false) + }) +}) diff --git a/src/features/liveNow/utils.ts b/src/features/liveNow/utils.ts index a4fb0bbf62..47e4398ea1 100644 --- a/src/features/liveNow/utils.ts +++ b/src/features/liveNow/utils.ts @@ -97,3 +97,16 @@ export function getLiveNowHost(url: string) { const {hostname} = new URL(url) return sanitizeLiveNowHost(hostname) } + +/** + * Whether a live status has not yet expired. + * + * Called per post, per render while scrolling, so this stays on primitives - + * `parseISO` plus two `Date` objects showed up in the scroll profile. + * `Date.parse` returns `NaN` for an unparseable value and `NaN > n` is `false`, + * which is the same "not active" answer the date-fns version gave. + */ +export function isStatusStillActive(timeStr: string | undefined) { + if (!timeStr) return false + return Date.parse(timeStr) > Date.now() +}