about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/hooks/__tests__/useTimeAgo.test.ts102
-rw-r--r--src/lib/hooks/useTimeAgo.ts95
-rw-r--r--src/lib/strings/time.ts42
3 files changed, 197 insertions, 42 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
+    }
+  }
+}
diff --git a/src/lib/strings/time.ts b/src/lib/strings/time.ts
index 8de4b52ae..1194e0240 100644
--- a/src/lib/strings/time.ts
+++ b/src/lib/strings/time.ts
@@ -1,45 +1,3 @@
-const NOW = 5
-const MINUTE = 60
-const HOUR = MINUTE * 60
-const DAY = HOUR * 24
-const MONTH_30 = DAY * 30
-const MONTH = DAY * 30.41675 // This results in 365.001 days in a year, which is close enough for nearly all cases
-export function ago(date: number | string | Date): string {
-  let ts: number
-  if (typeof date === 'string') {
-    ts = Number(new Date(date))
-  } else if (date instanceof Date) {
-    ts = Number(date)
-  } else {
-    ts = date
-  }
-  const diffSeconds = Math.floor((Date.now() - ts) / 1e3)
-  if (diffSeconds < NOW) {
-    return `now`
-  } else if (diffSeconds < MINUTE) {
-    return `${diffSeconds}s`
-  } else if (diffSeconds < HOUR) {
-    return `${Math.floor(diffSeconds / MINUTE)}m`
-  } else if (diffSeconds < DAY) {
-    return `${Math.floor(diffSeconds / HOUR)}h`
-  } else if (diffSeconds < MONTH_30) {
-    return `${Math.round(diffSeconds / DAY)}d`
-  } else {
-    let months = diffSeconds / MONTH
-    if (months % 1 >= 0.9) {
-      months = Math.ceil(months)
-    } else {
-      months = Math.floor(months)
-    }
-
-    if (months < 12) {
-      return `${months}mo`
-    } else {
-      return new Date(ts).toLocaleDateString()
-    }
-  }
-}
-
 export function niceDate(date: number | string | Date) {
   const d = new Date(date)
   return `${d.toLocaleDateString('en-us', {