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
text/typescript
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}
}