about summary refs log tree commit diff
path: root/src/lib/hooks
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/hooks')
-rw-r--r--src/lib/hooks/__tests__/useTimeAgo.test.ts102
-rw-r--r--src/lib/hooks/useTimeAgo.ts95
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
+    }
+  }
+}