Files
bsky-social-app/src/lib/hooks/useTimeAgo.ts
T

192 lines
4.8 KiB
TypeScript

import {useCallback} from 'react'
import {type I18n} from '@lingui/core'
import {defineMessage, msg, plural} from '@lingui/core/macro'
import {useLingui} from '@lingui/react'
import {differenceInSeconds} from 'date-fns'
export type DateDiffFormat = 'long' | 'short'
type DateDiff = {
value: number
unit: 'now' | 'second' | 'minute' | 'hour' | 'day' | 'month'
earlier: Date
later: Date
}
const NOW = 5
const MINUTE = 60
const HOUR = MINUTE * 60
const DAY = HOUR * 24
const MONTH_30 = DAY * 30
export function useGetTimeAgo({future = false}: {future?: boolean} = {}) {
const {i18n} = useLingui()
return useCallback(
(
earlier: number | string | Date,
later: number | string | Date,
options?: {format: DateDiffFormat},
) => {
const diff = dateDiff(earlier, later, future ? 'up' : 'down')
return formatDateDiff({diff, i18n, format: options?.format})
},
[i18n, future],
)
}
/**
* Returns the difference between `earlier` and `later` dates, based on
* opinionated rules.
*
* - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - All values round down
*/
export function dateDiff(
earlier: number | string | Date,
later: number | string | Date,
rounding: 'up' | 'down' = 'down',
): DateDiff {
let diff = {
value: 0,
unit: 'now' as DateDiff['unit'],
}
const e = new Date(earlier)
const l = new Date(later)
const diffSeconds = differenceInSeconds(l, e)
if (diffSeconds < NOW) {
diff = {
value: 0,
unit: 'now',
}
} else if (diffSeconds < MINUTE) {
diff = {
value: diffSeconds,
unit: 'second',
}
} else if (diffSeconds < HOUR) {
const value =
rounding === 'up'
? Math.ceil(diffSeconds / MINUTE)
: Math.floor(diffSeconds / MINUTE)
diff = {
value,
unit: 'minute',
}
} else if (diffSeconds < DAY) {
const value =
rounding === 'up'
? Math.ceil(diffSeconds / HOUR)
: Math.floor(diffSeconds / HOUR)
diff = {
value,
unit: 'hour',
}
} else if (diffSeconds < MONTH_30) {
const value =
rounding === 'up'
? Math.ceil(diffSeconds / DAY)
: Math.floor(diffSeconds / DAY)
diff = {
value,
unit: 'day',
}
} else {
const value =
rounding === 'up'
? Math.ceil(diffSeconds / MONTH_30)
: Math.floor(diffSeconds / MONTH_30)
diff = {
value,
unit: 'month',
}
}
return {
...diff,
earlier: e,
later: l,
}
}
/**
* Accepts a `DateDiff` and teturns the difference between `earlier` and
* `later` dates, formatted as a natural language string.
*
* - All month are considered exactly 30 days.
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
* - Differences >= 360 days are returned as the "M/D/YYYY" string
* - All values round down
*/
export function formatDateDiff({
diff,
format = 'short',
i18n,
}: {
diff: DateDiff
format?: DateDiffFormat
i18n: I18n
}): string {
const long = format === 'long'
switch (diff.unit) {
case 'now': {
return i18n._(msg`now`)
}
case 'second': {
return long
? i18n._(plural(diff.value, {one: '# second', other: '# seconds'}))
: i18n._(
defineMessage({
message: `${diff.value}s`,
comment: `How many seconds have passed, displayed in a narrow form`,
}),
)
}
case 'minute': {
return long
? i18n._(plural(diff.value, {one: '# minute', other: '# minutes'}))
: i18n._(
defineMessage({
message: `${diff.value}m`,
comment: `How many minutes have passed, displayed in a narrow form`,
}),
)
}
case 'hour': {
return long
? i18n._(plural(diff.value, {one: '# hour', other: '# hours'}))
: i18n._(
defineMessage({
message: `${diff.value}h`,
comment: `How many hours have passed, displayed in a narrow form`,
}),
)
}
case 'day': {
return long
? i18n._(plural(diff.value, {one: '# day', other: '# days'}))
: i18n._(
defineMessage({
message: `${diff.value}d`,
comment: `How many days have passed, displayed in a narrow form`,
}),
)
}
case 'month': {
if (diff.value < 12) {
return long
? i18n._(plural(diff.value, {one: '# month', other: '# months'}))
: i18n._(
defineMessage({
message: plural(diff.value, {one: '#mo', other: '#mo'}),
comment: `How many months have passed, displayed in a narrow form`,
}),
)
}
return i18n.date(new Date(diff.earlier))
}
}
}