UNPKG

sanity

Version:

Sanity is a real-time content infrastructure with a scalable, hosted backend featuring a Graph Oriented Query Language (GROQ), asset pipelines and fast edge caches

196 lines (166 loc) 5.52 kB
import { differenceInDays, differenceInHours, differenceInMinutes, differenceInMonths, differenceInSeconds, differenceInWeeks, differenceInYears, } from 'date-fns' import {useCallback, useEffect, useReducer} from 'react' import {useCurrentLocale, useTranslation} from '../i18n' import {intlCache} from '../i18n/intlCache' interface TimeSpec { timestamp: string refreshInterval: number | null } const FIVE_SECONDS = 1000 * 5 const TWENTY_SECONDS = 1000 * 20 const ONE_MINUTE = 1000 * 60 const ONE_HOUR = ONE_MINUTE * 60 const NO_YEAR_DATE_ONLY_FORMAT: Intl.DateTimeFormatOptions = { month: 'short', day: 'numeric', } const DATE_ONLY_FORMAT: Intl.DateTimeFormatOptions = { ...NO_YEAR_DATE_ONLY_FORMAT, year: 'numeric', } const FULL_DATE_FORMAT: Intl.DateTimeFormatOptions = { ...DATE_ONLY_FORMAT, hour: 'numeric', minute: 'numeric', } /** @internal */ export interface RelativeTimeOptions { minimal?: boolean useTemporalPhrase?: boolean relativeTo?: Date timeZone?: string } /** @internal */ export function useRelativeTime(time: Date | string, options: RelativeTimeOptions = {}): string { const resolved = useFormatRelativeTime(time, options) const [, forceUpdate] = useReducer((x) => x + 1, 0) useEffect(() => { let timerId: number | null function tick(interval: number) { timerId = window.setTimeout(() => { forceUpdate() // avoid pile-up of setInterval callbacks, // e.g. schedule the next update at `refreshInterval` *after* the previous one finishes timerId = window.setTimeout(() => tick(interval), interval) }, interval) } if (resolved.refreshInterval !== null) { tick(resolved.refreshInterval) } return () => { if (timerId !== null) { clearTimeout(timerId) } } }, [forceUpdate, resolved.refreshInterval]) return resolved.timestamp } function useFormatRelativeTime(date: Date | string, opts: RelativeTimeOptions = {}): TimeSpec { const {t} = useTranslation() const currentLocale = useCurrentLocale().id const {timeZone, minimal} = opts const parsedDate = date instanceof Date ? date : new Date(date) const useTemporalPhrase = Boolean(opts.useTemporalPhrase) const format = useCallback( function formatWithUnit(count: number, unit: Intl.RelativeTimeFormatUnit): string { const isNextOrPrevDay = unit === 'day' && Math.abs(count) === 1 const isNextOrPrevWeek = unit === 'week' && Math.abs(count) === 1 if (useTemporalPhrase || isNextOrPrevDay) { return intlCache .relativeTimeFormat(currentLocale, { // Force 'long' formatting for dates within the next/previous week as `Intl.RelativeTimeFormat` // will display these as `next wk.` or `last wk.` – which we don't want! // Idiomatic dates should always be displayed in full. There may be a more elegant way to handle this. style: minimal && !isNextOrPrevWeek ? 'short' : 'long', numeric: 'auto', }) .format(count, unit) } return intlCache .numberFormat(currentLocale, {style: 'unit', unit, unitDisplay: minimal ? 'short' : 'long'}) .format(Math.abs(count)) }, [currentLocale, useTemporalPhrase, minimal], ) // Invalid date? Return empty timestamp and `null` as refresh interval, to save us from // continuously trying to format an invalid date. The `useEffect` calls in the hook will // trigger a re-evaluation of the timestamp when the date changes, so this is safe. if (!parsedDate.getTime()) { return { timestamp: '', refreshInterval: null, } } const now = opts.relativeTo || Date.now() const diffMonths = differenceInMonths(now, parsedDate) const diffYears = differenceInYears(now, parsedDate) if (diffMonths || diffYears) { if (opts.minimal && diffYears === 0) { // same year return { timestamp: intlCache .dateTimeFormat(currentLocale, {...NO_YEAR_DATE_ONLY_FORMAT, timeZone}) .format(parsedDate), refreshInterval: null, } } if (opts.minimal) { return { timestamp: intlCache .dateTimeFormat(currentLocale, {...DATE_ONLY_FORMAT, timeZone}) .format(parsedDate), refreshInterval: null, } } return { timestamp: intlCache .dateTimeFormat(currentLocale, {...FULL_DATE_FORMAT, timeZone}) .format(parsedDate), refreshInterval: null, } } const diffWeeks = differenceInWeeks(parsedDate, now) if (diffWeeks) { return { timestamp: format(diffWeeks, 'week'), refreshInterval: ONE_HOUR, } } const diffDays = differenceInDays(parsedDate, now) if (diffDays) { return { timestamp: format(diffDays, 'day'), refreshInterval: ONE_HOUR, } } const diffHours = differenceInHours(parsedDate, now) if (diffHours) { return { timestamp: format(diffHours, 'hour'), refreshInterval: ONE_MINUTE, } } const diffMins = differenceInMinutes(parsedDate, now) if (diffMins) { return { timestamp: format(diffMins, 'minute'), refreshInterval: TWENTY_SECONDS, } } const diffSeconds = differenceInSeconds(parsedDate, now) if (Math.abs(diffSeconds) > 10) { return { timestamp: format(diffSeconds, 'second'), refreshInterval: FIVE_SECONDS, } } return {timestamp: t('relative-time.just-now'), refreshInterval: FIVE_SECONDS} }