Bin
2025-12-17 dcf780a91c16b6be28635b6e2e0e702060ee19f2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import { format, formatDistanceToNow } from "date-fns";
import type React from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
 
const SECS = 1000;
const MINS = 60 * SECS;
 
// Each stage is a tuple of limit and step
// limit is a time from base date, until which stage is active.
// step is a time-step which we do between two adjusted ticks.
// stages are chosen to match formatDistanceToNow function from date-fns
const STAGES: [limit: number, step: number][] = [
  // from 0 seconds
  [30 * SECS, 30 * SECS], // to 30 seconds with 30 seconds step
  [44 * MINS + 30 * SECS, MINS], // to 44 minutes 30 seconds with 1 minute step
  [Number.MAX_SAFE_INTEGER, 30 * MINS], // to 30 seconds with 30 seconds step
];
 
function getNextTick(passedTime = 0) {
  const idx = STAGES.findIndex(([timeLimit], idx) => {
    return timeLimit > passedTime || idx === STAGES.length - 1;
  });
  const baseLimit = idx > 0 ? STAGES[idx - 1][0] : 0;
  const baseStep = STAGES[idx][1];
 
  return Math.ceil((passedTime - baseLimit + 1) / baseStep) * baseStep + baseLimit;
}
 
type TimeAgoProps = React.ComponentPropsWithoutRef<"time"> & {
  date: number | string | Date;
};
 
export const TimeAgo = ({ date, ...rest }: TimeAgoProps) => {
  const [timestamp, forceUpdate] = useState(Date.now());
  const fromTS = useMemo(() => {
    return new Date(date).valueOf();
  }, [date]);
  const timeoutId = useRef<number>();
  const scheduleNext = useCallback(() => {
    const passedTime = Date.now() - fromTS;
    const tickValue = getNextTick(passedTime);
 
    timeoutId.current = window.setTimeout(() => {
      forceUpdate(Date.now());
    }, tickValue - passedTime);
  }, [date]);
 
  useEffect(() => {
    scheduleNext();
    return () => {
      clearTimeout(timeoutId.current);
    };
  }, [date, timestamp]);
 
  // Replace the longer english text when less than a minute in time. This is done this way due to a limiting API
  // with the date-fns function. If we require an entire overhaul to the messaging for the en-US locale, revisit this and replace with an entire locale override option.
  const text =
    formatDistanceToNow(fromTS, { addSuffix: true }) === "less than a minute ago"
      ? "seconds ago"
      : formatDistanceToNow(fromTS, { addSuffix: true });
 
  return (
    <time dateTime={format(fromTS, "yyyy-MM-dd'T'HH:mm:ss.SSSxxx")} title={format(fromTS, "PPpp")} {...rest}>
      {text}
    </time>
  );
};