diff options
Diffstat (limited to 'src/lib/hooks')
-rw-r--r-- | src/lib/hooks/__tests__/useTimeAgo.test.ts | 161 | ||||
-rw-r--r-- | src/lib/hooks/useTimeAgo.ts | 192 |
2 files changed, 278 insertions, 75 deletions
diff --git a/src/lib/hooks/__tests__/useTimeAgo.test.ts b/src/lib/hooks/__tests__/useTimeAgo.test.ts index e74f9c62d..68eb6e43a 100644 --- a/src/lib/hooks/__tests__/useTimeAgo.test.ts +++ b/src/lib/hooks/__tests__/useTimeAgo.test.ts @@ -1,102 +1,213 @@ 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') + const earlier = subDays(base, 3) + expect(dateDiff(earlier, Number(base))).toEqual({ + value: 3, + unit: 'day', + earlier, + later: base, + }) }) it(`works with strings`, () => { - expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d') + const earlier = subDays(base, 3) + expect(dateDiff(earlier, base.toString())).toEqual({ + value: 3, + unit: 'day', + earlier, + later: base, + }) }) it(`works with dates`, () => { - expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d') + const earlier = subDays(base, 3) + expect(dateDiff(earlier, base)).toEqual({ + value: 3, + unit: 'day', + earlier, + later: base, + }) }) it(`equal values return now`, () => { - expect(dateDiff(base, base, {lingui})).toEqual('now') + expect(dateDiff(base, base)).toEqual({ + value: 0, + unit: 'now', + earlier: base, + later: base, + }) }) it(`future dates return now`, () => { - expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now') + const earlier = addDays(base, 3) + expect(dateDiff(earlier, base)).toEqual({ + value: 0, + unit: 'now', + earlier, + later: base, + }) }) it(`values < 5 seconds ago return now`, () => { const then = subSeconds(base, 4) - expect(dateDiff(then, base, {lingui})).toEqual('now') + expect(dateDiff(then, base)).toEqual({ + value: 0, + unit: 'now', + earlier: then, + later: base, + }) }) it(`values >= 5 seconds ago return seconds`, () => { const then = subSeconds(base, 5) - expect(dateDiff(then, base, {lingui})).toEqual('5s') + expect(dateDiff(then, base)).toEqual({ + value: 5, + unit: 'second', + earlier: then, + later: base, + }) }) it(`values < 1 min return seconds`, () => { const then = subSeconds(base, 59) - expect(dateDiff(then, base, {lingui})).toEqual('59s') + expect(dateDiff(then, base)).toEqual({ + value: 59, + unit: 'second', + earlier: then, + later: base, + }) }) it(`values >= 1 min return minutes`, () => { const then = subSeconds(base, 60) - expect(dateDiff(then, base, {lingui})).toEqual('1m') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'minute', + earlier: then, + later: base, + }) }) it(`minutes round down`, () => { const then = subSeconds(base, 119) - expect(dateDiff(then, base, {lingui})).toEqual('1m') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'minute', + earlier: then, + later: base, + }) }) it(`values < 1 hour return minutes`, () => { const then = subMinutes(base, 59) - expect(dateDiff(then, base, {lingui})).toEqual('59m') + expect(dateDiff(then, base)).toEqual({ + value: 59, + unit: 'minute', + earlier: then, + later: base, + }) }) it(`values >= 1 hour return hours`, () => { const then = subMinutes(base, 60) - expect(dateDiff(then, base, {lingui})).toEqual('1h') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'hour', + earlier: then, + later: base, + }) }) it(`hours round down`, () => { const then = subMinutes(base, 119) - expect(dateDiff(then, base, {lingui})).toEqual('1h') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'hour', + earlier: then, + later: base, + }) }) it(`values < 1 day return hours`, () => { const then = subHours(base, 23) - expect(dateDiff(then, base, {lingui})).toEqual('23h') + expect(dateDiff(then, base)).toEqual({ + value: 23, + unit: 'hour', + earlier: then, + later: base, + }) }) it(`values >= 1 day return days`, () => { const then = subHours(base, 24) - expect(dateDiff(then, base, {lingui})).toEqual('1d') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'day', + earlier: then, + later: base, + }) }) it(`days round down`, () => { const then = subHours(base, 47) - expect(dateDiff(then, base, {lingui})).toEqual('1d') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'day', + earlier: then, + later: base, + }) }) it(`values < 30 days return days`, () => { const then = subDays(base, 29) - expect(dateDiff(then, base, {lingui})).toEqual('29d') + expect(dateDiff(then, base)).toEqual({ + value: 29, + unit: 'day', + earlier: then, + later: base, + }) }) it(`values >= 30 days return months`, () => { const then = subDays(base, 30) - expect(dateDiff(then, base, {lingui})).toEqual('1mo') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'month', + earlier: then, + later: base, + }) }) it(`months round down`, () => { const then = subDays(base, 59) - expect(dateDiff(then, base, {lingui})).toEqual('1mo') + expect(dateDiff(then, base)).toEqual({ + value: 1, + unit: 'month', + earlier: then, + later: base, + }) }) it(`values are rounded by increments of 30`, () => { const then = subDays(base, 61) - expect(dateDiff(then, base, {lingui})).toEqual('2mo') + expect(dateDiff(then, base)).toEqual({ + value: 2, + unit: 'month', + earlier: then, + later: base, + }) }) it(`values < 360 days return months`, () => { const then = subDays(base, 359) - expect(dateDiff(then, base, {lingui})).toEqual('11mo') + expect(dateDiff(then, base)).toEqual({ + value: 11, + unit: 'month', + earlier: then, + later: base, + }) }) it(`values >= 360 days return the earlier value`, () => { const then = subDays(base, 360) - expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString()) + expect(dateDiff(then, base)).toEqual({ + value: 12, + unit: 'month', + earlier: then, + later: base, + }) }) }) diff --git a/src/lib/hooks/useTimeAgo.ts b/src/lib/hooks/useTimeAgo.ts index efcb4754b..3a8bf49bc 100644 --- a/src/lib/hooks/useTimeAgo.ts +++ b/src/lib/hooks/useTimeAgo.ts @@ -1,86 +1,178 @@ import {useCallback} from 'react' -import {msg, plural} from '@lingui/macro' -import {I18nContext, useLingui} from '@lingui/react' +import {I18n} from '@lingui/core' +import {defineMessage, msg, plural} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {differenceInSeconds} from 'date-fns' -export type TimeAgoOptions = { - lingui: I18nContext['_'] - format?: 'long' | 'short' +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() { - const {_} = useLingui() + const {i18n} = useLingui() return useCallback( ( earlier: number | string | Date, later: number | string | Date, - options?: Omit<TimeAgoOptions, 'lingui'>, + options?: {format: DateDiffFormat}, ) => { - return dateDiff(earlier, later, {lingui: _, format: options?.format}) + const diff = dateDiff(earlier, later) + return formatDateDiff({diff, i18n, format: options?.format}) }, - [_], + [i18n], ) } -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. + * 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'. - * - 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)) +): 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) { - return _(msg`now`) + diff = { + value: 0, + unit: 'now' as DateDiff['unit'], + } } else if (diffSeconds < MINUTE) { - return `${diffSeconds}${ - long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's' - }` + diff = { + value: diffSeconds, + unit: 'second' as DateDiff['unit'], + } } else if (diffSeconds < HOUR) { - const diff = Math.floor(diffSeconds / MINUTE) - return `${diff}${ - long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm' - }` + const value = Math.floor(diffSeconds / MINUTE) + diff = { + value, + unit: 'minute' as DateDiff['unit'], + } } else if (diffSeconds < DAY) { - const diff = Math.floor(diffSeconds / HOUR) - return `${diff}${ - long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h' - }` + const value = Math.floor(diffSeconds / HOUR) + diff = { + value, + unit: 'hour' as DateDiff['unit'], + } } else if (diffSeconds < MONTH_30) { - const diff = Math.floor(diffSeconds / DAY) - return `${diff}${ - long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd' - }` + const value = Math.floor(diffSeconds / DAY) + diff = { + value, + unit: 'day' as DateDiff['unit'], + } } 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() + const value = Math.floor(diffSeconds / MONTH_30) + diff = { + value, + unit: 'month' as DateDiff['unit'], + } + } + + 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' - if (long) { - return _(msg`on ${str}`) + 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: `${diff.value}mo`, + comment: `How many months have passed, displayed in a narrow form`, + }), + ) } - return str + return i18n.date(new Date(diff.earlier)) } } } |