diff options
Diffstat (limited to 'src/lib/hooks')
-rw-r--r-- | src/lib/hooks/__tests__/useTimeAgo.test.ts | 102 | ||||
-rw-r--r-- | src/lib/hooks/useTimeAgo.ts | 95 |
2 files changed, 197 insertions, 0 deletions
diff --git a/src/lib/hooks/__tests__/useTimeAgo.test.ts b/src/lib/hooks/__tests__/useTimeAgo.test.ts new file mode 100644 index 000000000..e74f9c62d --- /dev/null +++ b/src/lib/hooks/__tests__/useTimeAgo.test.ts @@ -0,0 +1,102 @@ +import {describe, expect, it} from '@jest/globals' +import {MessageDescriptor} from '@lingui/core' +import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns' + +import {dateDiff} from '../useTimeAgo' + +const lingui: any = (obj: MessageDescriptor) => obj.message + +const base = new Date('2024-06-17T00:00:00Z') + +describe('dateDiff', () => { + it(`works with numbers`, () => { + expect(dateDiff(subDays(base, 3), Number(base), {lingui})).toEqual('3d') + }) + it(`works with strings`, () => { + expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d') + }) + it(`works with dates`, () => { + expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d') + }) + + it(`equal values return now`, () => { + expect(dateDiff(base, base, {lingui})).toEqual('now') + }) + it(`future dates return now`, () => { + expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now') + }) + + it(`values < 5 seconds ago return now`, () => { + const then = subSeconds(base, 4) + expect(dateDiff(then, base, {lingui})).toEqual('now') + }) + it(`values >= 5 seconds ago return seconds`, () => { + const then = subSeconds(base, 5) + expect(dateDiff(then, base, {lingui})).toEqual('5s') + }) + + it(`values < 1 min return seconds`, () => { + const then = subSeconds(base, 59) + expect(dateDiff(then, base, {lingui})).toEqual('59s') + }) + it(`values >= 1 min return minutes`, () => { + const then = subSeconds(base, 60) + expect(dateDiff(then, base, {lingui})).toEqual('1m') + }) + it(`minutes round down`, () => { + const then = subSeconds(base, 119) + expect(dateDiff(then, base, {lingui})).toEqual('1m') + }) + + it(`values < 1 hour return minutes`, () => { + const then = subMinutes(base, 59) + expect(dateDiff(then, base, {lingui})).toEqual('59m') + }) + it(`values >= 1 hour return hours`, () => { + const then = subMinutes(base, 60) + expect(dateDiff(then, base, {lingui})).toEqual('1h') + }) + it(`hours round down`, () => { + const then = subMinutes(base, 119) + expect(dateDiff(then, base, {lingui})).toEqual('1h') + }) + + it(`values < 1 day return hours`, () => { + const then = subHours(base, 23) + expect(dateDiff(then, base, {lingui})).toEqual('23h') + }) + it(`values >= 1 day return days`, () => { + const then = subHours(base, 24) + expect(dateDiff(then, base, {lingui})).toEqual('1d') + }) + it(`days round down`, () => { + const then = subHours(base, 47) + expect(dateDiff(then, base, {lingui})).toEqual('1d') + }) + + it(`values < 30 days return days`, () => { + const then = subDays(base, 29) + expect(dateDiff(then, base, {lingui})).toEqual('29d') + }) + it(`values >= 30 days return months`, () => { + const then = subDays(base, 30) + expect(dateDiff(then, base, {lingui})).toEqual('1mo') + }) + it(`months round down`, () => { + const then = subDays(base, 59) + expect(dateDiff(then, base, {lingui})).toEqual('1mo') + }) + it(`values are rounded by increments of 30`, () => { + const then = subDays(base, 61) + expect(dateDiff(then, base, {lingui})).toEqual('2mo') + }) + + it(`values < 360 days return months`, () => { + const then = subDays(base, 359) + expect(dateDiff(then, base, {lingui})).toEqual('11mo') + }) + it(`values >= 360 days return the earlier value`, () => { + const then = subDays(base, 360) + expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString()) + }) +}) diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts new file mode 100644 index 000000000..5f0782f96 --- /dev/null +++ b/src/lib/hooks/useTimeAgo.ts @@ -0,0 +1,95 @@ +import {useCallback, useMemo} from 'react' +import {msg, plural} from '@lingui/macro' +import {I18nContext, useLingui} from '@lingui/react' +import {differenceInSeconds} from 'date-fns' + +export type TimeAgoOptions = { + lingui: I18nContext['_'] + format?: 'long' | 'short' +} + +export function useGetTimeAgo() { + const {_} = useLingui() + return useCallback( + ( + date: number | string | Date, + options?: Omit<TimeAgoOptions, 'lingui'>, + ) => { + return dateDiff(date, Date.now(), {lingui: _, format: options?.format}) + }, + [_], + ) +} + +export function useTimeAgo( + date: number | string | Date, + options?: Omit<TimeAgoOptions, 'lingui'>, +): string { + const timeAgo = useGetTimeAgo() + return useMemo(() => { + return timeAgo(date, {...options}) + }, [date, options, timeAgo]) +} + +const NOW = 5 +const MINUTE = 60 +const HOUR = MINUTE * 60 +const DAY = HOUR * 24 +const MONTH_30 = DAY * 30 + +/** + * Returns 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 dateDiff( + earlier: number | string | Date, + later: number | string | Date, + options: TimeAgoOptions, +): string { + const _ = options.lingui + const format = options?.format || 'short' + const long = format === 'long' + const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier)) + + if (diffSeconds < NOW) { + return _(msg`now`) + } else if (diffSeconds < MINUTE) { + return `${diffSeconds}${ + long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's' + }` + } else if (diffSeconds < HOUR) { + const diff = Math.floor(diffSeconds / MINUTE) + return `${diff}${ + long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm' + }` + } else if (diffSeconds < DAY) { + const diff = Math.floor(diffSeconds / HOUR) + return `${diff}${ + long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h' + }` + } else if (diffSeconds < MONTH_30) { + const diff = Math.floor(diffSeconds / DAY) + return `${diff}${ + long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd' + }` + } else { + const diff = Math.floor(diffSeconds / MONTH_30) + if (diff < 12) { + return `${diff}${ + long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo' + }` + } else { + const str = new Date(earlier).toLocaleDateString() + + if (long) { + return _(msg`on ${str}`) + } + return str + } + } +} |