Localize dates, counts (#5027)
* refactor: consistent localized formatting * refactor: localized date time * refactor: localize relative time with strings * chore: fix typo from copy-paste * Clean up useTimeAgo * Remove old ago * Const * Reuse * Prettier --------- Co-authored-by: Mary <git@mary.my.id>zio/stable
parent
d5a7618374
commit
8651f31ebb
|
@ -377,7 +377,7 @@ function Inner({
|
||||||
hide: () => void
|
hide: () => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const moderation = React.useMemo(
|
const moderation = React.useMemo(
|
||||||
() => moderateProfile(profile, moderationOpts),
|
() => moderateProfile(profile, moderationOpts),
|
||||||
|
@ -393,8 +393,8 @@ function Inner({
|
||||||
profile.viewer?.blocking ||
|
profile.viewer?.blocking ||
|
||||||
profile.viewer?.blockedBy ||
|
profile.viewer?.blockedBy ||
|
||||||
profile.viewer?.blockingByList
|
profile.viewer?.blockingByList
|
||||||
const following = formatCount(profile.followsCount || 0)
|
const following = formatCount(i18n, profile.followsCount || 0)
|
||||||
const followers = formatCount(profile.followersCount || 0)
|
const followers = formatCount(i18n, profile.followersCount || 0)
|
||||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||||
one: 'follower',
|
one: 'follower',
|
||||||
other: 'followers',
|
other: 'followers',
|
||||||
|
|
|
@ -43,7 +43,7 @@ function EmbedDialogInner({
|
||||||
timestamp,
|
timestamp,
|
||||||
}: Omit<EmbedDialogProps, 'control'>) {
|
}: Omit<EmbedDialogProps, 'control'>) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const ref = useRef<TextInput>(null)
|
const ref = useRef<TextInput>(null)
|
||||||
const [copied, setCopied] = useState(false)
|
const [copied, setCopied] = useState(false)
|
||||||
|
|
||||||
|
@ -86,9 +86,9 @@ function EmbedDialogInner({
|
||||||
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
|
)} (<a href="${escapeHtml(profileHref)}">@${escapeHtml(
|
||||||
postAuthor.handle,
|
postAuthor.handle,
|
||||||
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
)}</a>) <a href="${escapeHtml(href)}">${escapeHtml(
|
||||||
niceDate(timestamp),
|
niceDate(i18n, timestamp),
|
||||||
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
)}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>`
|
||||||
}, [postUri, postCid, record, timestamp, postAuthor])
|
}, [i18n, postUri, postCid, record, timestamp, postAuthor])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
<Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
RichText as RichTextAPI,
|
RichText as RichTextAPI,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {I18n} from '@lingui/core'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
@ -153,14 +154,14 @@ let MessageItemMetadata = ({
|
||||||
)
|
)
|
||||||
|
|
||||||
const relativeTimestamp = useCallback(
|
const relativeTimestamp = useCallback(
|
||||||
(timestamp: string) => {
|
(i18n: I18n, timestamp: string) => {
|
||||||
const date = new Date(timestamp)
|
const date = new Date(timestamp)
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
|
|
||||||
const time = new Intl.DateTimeFormat(undefined, {
|
const time = i18n.date(date, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
}).format(date)
|
})
|
||||||
|
|
||||||
const diff = now.getTime() - date.getTime()
|
const diff = now.getTime() - date.getTime()
|
||||||
|
|
||||||
|
@ -182,13 +183,13 @@ let MessageItemMetadata = ({
|
||||||
return _(msg`Yesterday, ${time}`)
|
return _(msg`Yesterday, ${time}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Intl.DateTimeFormat(undefined, {
|
return i18n.date(date, {
|
||||||
hour: 'numeric',
|
hour: 'numeric',
|
||||||
minute: 'numeric',
|
minute: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
}).format(date)
|
})
|
||||||
},
|
},
|
||||||
[_],
|
[_],
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {android, atoms as a, useTheme, web} from '#/alf'
|
import {android, atoms as a, useTheme, web} from '#/alf'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
import * as TextField from '#/components/forms/TextField'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {localizeDate} from './utils'
|
|
||||||
|
|
||||||
// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
|
// looks like a TextField.Input, but is just a button. It'll do something different on each platform on press
|
||||||
// iOS: open a dialog with an inline date picker
|
// iOS: open a dialog with an inline date picker
|
||||||
|
@ -25,6 +25,7 @@ export function DateFieldButton({
|
||||||
isInvalid?: boolean
|
isInvalid?: boolean
|
||||||
accessibilityHint?: string
|
accessibilityHint?: string
|
||||||
}) {
|
}) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
@ -91,7 +92,7 @@ export function DateFieldButton({
|
||||||
t.atoms.text,
|
t.atoms.text,
|
||||||
{lineHeight: a.text_md.fontSize * 1.1875},
|
{lineHeight: a.text_md.fontSize * 1.1875},
|
||||||
]}>
|
]}>
|
||||||
{localizeDate(value)}
|
{i18n.date(value, {timeZone: 'UTC'})}
|
||||||
</Text>
|
</Text>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -1,16 +1,5 @@
|
||||||
import {getLocales} from 'expo-localization'
|
|
||||||
|
|
||||||
const LOCALE = getLocales()[0]
|
|
||||||
|
|
||||||
// we need the date in the form yyyy-MM-dd to pass to the input
|
// we need the date in the form yyyy-MM-dd to pass to the input
|
||||||
export function toSimpleDateString(date: Date | string): string {
|
export function toSimpleDateString(date: Date | string): string {
|
||||||
const _date = typeof date === 'string' ? new Date(date) : date
|
const _date = typeof date === 'string' ? new Date(date) : date
|
||||||
return _date.toISOString().split('T')[0]
|
return _date.toISOString().split('T')[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function localizeDate(date: Date | string): string {
|
|
||||||
const _date = typeof date === 'string' ? new Date(date) : date
|
|
||||||
return new Intl.DateTimeFormat(LOCALE.languageTag, {
|
|
||||||
timeZone: 'UTC',
|
|
||||||
}).format(_date)
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,102 +1,213 @@
|
||||||
import {describe, expect, it} from '@jest/globals'
|
import {describe, expect, it} from '@jest/globals'
|
||||||
import {MessageDescriptor} from '@lingui/core'
|
|
||||||
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
|
import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns'
|
||||||
|
|
||||||
import {dateDiff} from '../useTimeAgo'
|
import {dateDiff} from '../useTimeAgo'
|
||||||
|
|
||||||
const lingui: any = (obj: MessageDescriptor) => obj.message
|
|
||||||
|
|
||||||
const base = new Date('2024-06-17T00:00:00Z')
|
const base = new Date('2024-06-17T00:00:00Z')
|
||||||
|
|
||||||
describe('dateDiff', () => {
|
describe('dateDiff', () => {
|
||||||
it(`works with numbers`, () => {
|
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`, () => {
|
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`, () => {
|
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`, () => {
|
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`, () => {
|
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`, () => {
|
it(`values < 5 seconds ago return now`, () => {
|
||||||
const then = subSeconds(base, 4)
|
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`, () => {
|
it(`values >= 5 seconds ago return seconds`, () => {
|
||||||
const then = subSeconds(base, 5)
|
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`, () => {
|
it(`values < 1 min return seconds`, () => {
|
||||||
const then = subSeconds(base, 59)
|
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`, () => {
|
it(`values >= 1 min return minutes`, () => {
|
||||||
const then = subSeconds(base, 60)
|
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`, () => {
|
it(`minutes round down`, () => {
|
||||||
const then = subSeconds(base, 119)
|
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`, () => {
|
it(`values < 1 hour return minutes`, () => {
|
||||||
const then = subMinutes(base, 59)
|
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`, () => {
|
it(`values >= 1 hour return hours`, () => {
|
||||||
const then = subMinutes(base, 60)
|
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`, () => {
|
it(`hours round down`, () => {
|
||||||
const then = subMinutes(base, 119)
|
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`, () => {
|
it(`values < 1 day return hours`, () => {
|
||||||
const then = subHours(base, 23)
|
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`, () => {
|
it(`values >= 1 day return days`, () => {
|
||||||
const then = subHours(base, 24)
|
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`, () => {
|
it(`days round down`, () => {
|
||||||
const then = subHours(base, 47)
|
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`, () => {
|
it(`values < 30 days return days`, () => {
|
||||||
const then = subDays(base, 29)
|
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`, () => {
|
it(`values >= 30 days return months`, () => {
|
||||||
const then = subDays(base, 30)
|
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`, () => {
|
it(`months round down`, () => {
|
||||||
const then = subDays(base, 59)
|
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`, () => {
|
it(`values are rounded by increments of 30`, () => {
|
||||||
const then = subDays(base, 61)
|
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`, () => {
|
it(`values < 360 days return months`, () => {
|
||||||
const then = subDays(base, 359)
|
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`, () => {
|
it(`values >= 360 days return the earlier value`, () => {
|
||||||
const then = subDays(base, 360)
|
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,
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,25 +1,16 @@
|
||||||
import {useCallback} from 'react'
|
import {useCallback} from 'react'
|
||||||
import {msg, plural} from '@lingui/macro'
|
import {I18n} from '@lingui/core'
|
||||||
import {I18nContext, useLingui} from '@lingui/react'
|
import {defineMessage, msg, plural} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {differenceInSeconds} from 'date-fns'
|
import {differenceInSeconds} from 'date-fns'
|
||||||
|
|
||||||
export type TimeAgoOptions = {
|
export type DateDiffFormat = 'long' | 'short'
|
||||||
lingui: I18nContext['_']
|
|
||||||
format?: 'long' | 'short'
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useGetTimeAgo() {
|
type DateDiff = {
|
||||||
const {_} = useLingui()
|
value: number
|
||||||
return useCallback(
|
unit: 'now' | 'second' | 'minute' | 'hour' | 'day' | 'month'
|
||||||
(
|
earlier: Date
|
||||||
earlier: number | string | Date,
|
later: Date
|
||||||
later: number | string | Date,
|
|
||||||
options?: Omit<TimeAgoOptions, 'lingui'>,
|
|
||||||
) => {
|
|
||||||
return dateDiff(earlier, later, {lingui: _, format: options?.format})
|
|
||||||
},
|
|
||||||
[_],
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NOW = 5
|
const NOW = 5
|
||||||
|
@ -28,59 +19,160 @@ const HOUR = MINUTE * 60
|
||||||
const DAY = HOUR * 24
|
const DAY = HOUR * 24
|
||||||
const MONTH_30 = DAY * 30
|
const MONTH_30 = DAY * 30
|
||||||
|
|
||||||
|
export function useGetTimeAgo() {
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
return useCallback(
|
||||||
|
(
|
||||||
|
earlier: number | string | Date,
|
||||||
|
later: number | string | Date,
|
||||||
|
options?: {format: DateDiffFormat},
|
||||||
|
) => {
|
||||||
|
const diff = dateDiff(earlier, later)
|
||||||
|
return formatDateDiff({diff, i18n, format: options?.format})
|
||||||
|
},
|
||||||
|
[i18n],
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the difference between `earlier` and `later` dates, formatted as a
|
* Returns the difference between `earlier` and `later` dates, based on
|
||||||
* natural language string.
|
* 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,
|
||||||
|
): 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' as DateDiff['unit'],
|
||||||
|
}
|
||||||
|
} else if (diffSeconds < MINUTE) {
|
||||||
|
diff = {
|
||||||
|
value: diffSeconds,
|
||||||
|
unit: 'second' as DateDiff['unit'],
|
||||||
|
}
|
||||||
|
} else if (diffSeconds < HOUR) {
|
||||||
|
const value = Math.floor(diffSeconds / MINUTE)
|
||||||
|
diff = {
|
||||||
|
value,
|
||||||
|
unit: 'minute' as DateDiff['unit'],
|
||||||
|
}
|
||||||
|
} else if (diffSeconds < DAY) {
|
||||||
|
const value = Math.floor(diffSeconds / HOUR)
|
||||||
|
diff = {
|
||||||
|
value,
|
||||||
|
unit: 'hour' as DateDiff['unit'],
|
||||||
|
}
|
||||||
|
} else if (diffSeconds < MONTH_30) {
|
||||||
|
const value = Math.floor(diffSeconds / DAY)
|
||||||
|
diff = {
|
||||||
|
value,
|
||||||
|
unit: 'day' as DateDiff['unit'],
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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.
|
* - All month are considered exactly 30 days.
|
||||||
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
|
* - Dates assume `earlier` <= `later`, and will otherwise return 'now'.
|
||||||
* - Differences >= 360 days are returned as the "M/D/YYYY" string
|
* - Differences >= 360 days are returned as the "M/D/YYYY" string
|
||||||
* - All values round down
|
* - All values round down
|
||||||
*/
|
*/
|
||||||
export function dateDiff(
|
export function formatDateDiff({
|
||||||
earlier: number | string | Date,
|
diff,
|
||||||
later: number | string | Date,
|
format = 'short',
|
||||||
options: TimeAgoOptions,
|
i18n,
|
||||||
): string {
|
}: {
|
||||||
const _ = options.lingui
|
diff: DateDiff
|
||||||
const format = options?.format || 'short'
|
format?: DateDiffFormat
|
||||||
|
i18n: I18n
|
||||||
|
}): string {
|
||||||
const long = format === 'long'
|
const long = format === 'long'
|
||||||
const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier))
|
|
||||||
|
|
||||||
if (diffSeconds < NOW) {
|
switch (diff.unit) {
|
||||||
return _(msg`now`)
|
case 'now': {
|
||||||
} else if (diffSeconds < MINUTE) {
|
return i18n._(msg`now`)
|
||||||
return `${diffSeconds}${
|
}
|
||||||
long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's'
|
case 'second': {
|
||||||
}`
|
return long
|
||||||
} else if (diffSeconds < HOUR) {
|
? i18n._(plural(diff.value, {one: '# second', other: '# seconds'}))
|
||||||
const diff = Math.floor(diffSeconds / MINUTE)
|
: i18n._(
|
||||||
return `${diff}${
|
defineMessage({
|
||||||
long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm'
|
message: `${diff.value}s`,
|
||||||
}`
|
comment: `How many seconds have passed, displayed in a narrow form`,
|
||||||
} else if (diffSeconds < DAY) {
|
}),
|
||||||
const diff = Math.floor(diffSeconds / HOUR)
|
)
|
||||||
return `${diff}${
|
}
|
||||||
long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h'
|
case 'minute': {
|
||||||
}`
|
return long
|
||||||
} else if (diffSeconds < MONTH_30) {
|
? i18n._(plural(diff.value, {one: '# minute', other: '# minutes'}))
|
||||||
const diff = Math.floor(diffSeconds / DAY)
|
: i18n._(
|
||||||
return `${diff}${
|
defineMessage({
|
||||||
long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd'
|
message: `${diff.value}m`,
|
||||||
}`
|
comment: `How many minutes have passed, displayed in a narrow form`,
|
||||||
} else {
|
}),
|
||||||
const diff = Math.floor(diffSeconds / MONTH_30)
|
)
|
||||||
if (diff < 12) {
|
}
|
||||||
return `${diff}${
|
case 'hour': {
|
||||||
long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo'
|
return long
|
||||||
}`
|
? i18n._(plural(diff.value, {one: '# hour', other: '# hours'}))
|
||||||
} else {
|
: i18n._(
|
||||||
const str = new Date(earlier).toLocaleDateString()
|
defineMessage({
|
||||||
|
message: `${diff.value}h`,
|
||||||
if (long) {
|
comment: `How many hours have passed, displayed in a narrow form`,
|
||||||
return _(msg`on ${str}`)
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
export function niceDate(date: number | string | Date) {
|
import {I18n} from '@lingui/core'
|
||||||
|
|
||||||
|
export function niceDate(i18n: I18n, date: number | string | Date) {
|
||||||
const d = new Date(date)
|
const d = new Date(date)
|
||||||
return `${d.toLocaleDateString('en-us', {
|
|
||||||
year: 'numeric',
|
return i18n.date(d, {
|
||||||
month: 'short',
|
dateStyle: 'long',
|
||||||
day: 'numeric',
|
timeStyle: 'short',
|
||||||
})} at ${d.toLocaleTimeString(undefined, {
|
})
|
||||||
hour: 'numeric',
|
|
||||||
minute: '2-digit',
|
|
||||||
})}`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAge(birthDate: Date): number {
|
export function getAge(birthDate: Date): number {
|
||||||
|
|
|
@ -17,9 +17,9 @@ export function ProfileHeaderMetrics({
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
profile: Shadow<AppBskyActorDefs.ProfileViewDetailed>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const following = formatCount(profile.followsCount || 0)
|
const following = formatCount(i18n, profile.followsCount || 0)
|
||||||
const followers = formatCount(profile.followersCount || 0)
|
const followers = formatCount(i18n, profile.followersCount || 0)
|
||||||
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
const pluralizedFollowers = plural(profile.followersCount || 0, {
|
||||||
one: 'follower',
|
one: 'follower',
|
||||||
other: 'followers',
|
other: 'followers',
|
||||||
|
@ -54,7 +54,7 @@ export function ProfileHeaderMetrics({
|
||||||
</Text>
|
</Text>
|
||||||
</InlineLinkText>
|
</InlineLinkText>
|
||||||
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
|
<Text style={[a.font_bold, t.atoms.text, a.text_md]}>
|
||||||
{formatCount(profile.postsCount || 0)}{' '}
|
{formatCount(i18n, profile.postsCount || 0)}{' '}
|
||||||
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
|
<Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}>
|
||||||
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
|
{plural(profile.postsCount || 0, {one: 'post', other: 'posts'})}
|
||||||
</Text>
|
</Text>
|
||||||
|
|
|
@ -113,7 +113,7 @@ function LandingScreenLoaded({
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
}) {
|
}) {
|
||||||
const {creator, listItemsSample, feeds} = starterPack
|
const {creator, listItemsSample, feeds} = starterPack
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const activeStarterPack = useActiveStarterPack()
|
const activeStarterPack = useActiveStarterPack()
|
||||||
const setActiveStarterPack = useSetActiveStarterPack()
|
const setActiveStarterPack = useSetActiveStarterPack()
|
||||||
|
@ -225,7 +225,9 @@ function LandingScreenLoaded({
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
]}
|
]}
|
||||||
numberOfLines={1}>
|
numberOfLines={1}>
|
||||||
<Trans>{formatCount(JOINED_THIS_WEEK)} joined this week</Trans>
|
<Trans>
|
||||||
|
{formatCount(i18n, JOINED_THIS_WEEK)} joined this week
|
||||||
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -84,7 +84,7 @@ let FeedItem = ({
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
|
const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false)
|
||||||
const itemHref = useMemo(() => {
|
const itemHref = useMemo(() => {
|
||||||
|
@ -225,11 +225,11 @@ let FeedItem = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const formattedCount =
|
const formattedCount =
|
||||||
authors.length > 1 ? formatCount(authors.length - 1) : ''
|
authors.length > 1 ? formatCount(i18n, authors.length - 1) : ''
|
||||||
const firstAuthorName = sanitizeDisplayName(
|
const firstAuthorName = sanitizeDisplayName(
|
||||||
authors[0].profile.displayName || authors[0].profile.handle,
|
authors[0].profile.displayName || authors[0].profile.handle,
|
||||||
)
|
)
|
||||||
const niceTimestamp = niceDate(item.notification.indexedAt)
|
const niceTimestamp = niceDate(i18n, item.notification.indexedAt)
|
||||||
const a11yLabelUsers =
|
const a11yLabelUsers =
|
||||||
authors.length > 1
|
authors.length > 1
|
||||||
? _(msg` and `) +
|
? _(msg` and `) +
|
||||||
|
|
|
@ -181,7 +181,7 @@ let PostThreadItemLoaded = ({
|
||||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const [limitLines, setLimitLines] = React.useState(
|
const [limitLines, setLimitLines] = React.useState(
|
||||||
|
@ -388,7 +388,7 @@ let PostThreadItemLoaded = ({
|
||||||
type="lg"
|
type="lg"
|
||||||
style={pal.textLight}>
|
style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{formatCount(post.repostCount)}
|
{formatCount(i18n, post.repostCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={post.repostCount}
|
value={post.repostCount}
|
||||||
|
@ -410,7 +410,7 @@ let PostThreadItemLoaded = ({
|
||||||
type="lg"
|
type="lg"
|
||||||
style={pal.textLight}>
|
style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{formatCount(post.quoteCount)}
|
{formatCount(i18n, post.quoteCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={post.quoteCount}
|
value={post.quoteCount}
|
||||||
|
@ -430,7 +430,7 @@ let PostThreadItemLoaded = ({
|
||||||
type="lg"
|
type="lg"
|
||||||
style={pal.textLight}>
|
style={pal.textLight}>
|
||||||
<Text type="xl-bold" style={pal.text}>
|
<Text type="xl-bold" style={pal.text}>
|
||||||
{formatCount(post.likeCount)}
|
{formatCount(i18n, post.likeCount)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural value={post.likeCount} one="like" other="likes" />
|
<Plural value={post.likeCount} one="like" other="likes" />
|
||||||
</Text>
|
</Text>
|
||||||
|
@ -705,7 +705,7 @@ function ExpandedPostDetails({
|
||||||
translatorUrl: string
|
translatorUrl: string
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const openLink = useOpenLink()
|
const openLink = useOpenLink()
|
||||||
const isRootPost = !('reply' in post.record)
|
const isRootPost = !('reply' in post.record)
|
||||||
|
|
||||||
|
@ -723,7 +723,9 @@ function ExpandedPostDetails({
|
||||||
s.mt2,
|
s.mt2,
|
||||||
s.mb10,
|
s.mb10,
|
||||||
]}>
|
]}>
|
||||||
<Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text>
|
<Text style={[a.text_sm, pal.textLight]}>
|
||||||
|
{niceDate(i18n, post.indexedAt)}
|
||||||
|
</Text>
|
||||||
{isRootPost && (
|
{isRootPost && (
|
||||||
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
<WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, {memo, useCallback} from 'react'
|
import React, {memo, useCallback} from 'react'
|
||||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
||||||
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
|
import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {precacheProfile} from '#/state/queries/profile'
|
import {precacheProfile} from '#/state/queries/profile'
|
||||||
|
@ -35,6 +36,8 @@ interface PostMetaOpts {
|
||||||
}
|
}
|
||||||
|
|
||||||
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||||
|
const {i18n} = useLingui()
|
||||||
|
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const displayName = opts.author.displayName || opts.author.handle
|
const displayName = opts.author.displayName || opts.author.handle
|
||||||
const handle = opts.author.handle
|
const handle = opts.author.handle
|
||||||
|
@ -101,8 +104,8 @@ let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
|
||||||
type="md"
|
type="md"
|
||||||
style={pal.textLight}
|
style={pal.textLight}
|
||||||
text={timeElapsed}
|
text={timeElapsed}
|
||||||
accessibilityLabel={niceDate(opts.timestamp)}
|
accessibilityLabel={niceDate(i18n, opts.timestamp)}
|
||||||
title={niceDate(opts.timestamp)}
|
title={niceDate(i18n, opts.timestamp)}
|
||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
href={opts.postHref}
|
href={opts.postHref}
|
||||||
onBeforePress={onBeforePressPost}
|
onBeforePress={onBeforePressPost}
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
import {I18n} from '@lingui/core'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
||||||
import {useTickEveryMinute} from '#/state/shell'
|
import {useTickEveryMinute} from '#/state/shell'
|
||||||
|
@ -10,19 +12,21 @@ export function TimeElapsed({
|
||||||
}: {
|
}: {
|
||||||
timestamp: string
|
timestamp: string
|
||||||
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
|
children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
|
||||||
timeToString?: (timeElapsed: string) => string
|
timeToString?: (i18n: I18n, timeElapsed: string) => string
|
||||||
}) {
|
}) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
const ago = useGetTimeAgo()
|
const ago = useGetTimeAgo()
|
||||||
const format = timeToString ?? ago
|
|
||||||
const tick = useTickEveryMinute()
|
const tick = useTickEveryMinute()
|
||||||
const [timeElapsed, setTimeAgo] = React.useState(() =>
|
const [timeElapsed, setTimeAgo] = React.useState(() =>
|
||||||
format(timestamp, tick),
|
timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick),
|
||||||
)
|
)
|
||||||
|
|
||||||
const [prevTick, setPrevTick] = React.useState(tick)
|
const [prevTick, setPrevTick] = React.useState(tick)
|
||||||
if (prevTick !== tick) {
|
if (prevTick !== tick) {
|
||||||
setPrevTick(tick)
|
setPrevTick(tick)
|
||||||
setTimeAgo(format(timestamp, tick))
|
setTimeAgo(
|
||||||
|
timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return children({timeElapsed})
|
return children({timeElapsed})
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import React, {useState, useCallback} from 'react'
|
import React, {useCallback, useState} from 'react'
|
||||||
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'
|
||||||
|
import DatePicker from 'react-native-date-picker'
|
||||||
import {
|
import {
|
||||||
FontAwesomeIcon,
|
FontAwesomeIcon,
|
||||||
FontAwesomeIconStyle,
|
FontAwesomeIconStyle,
|
||||||
} from '@fortawesome/react-native-fontawesome'
|
} from '@fortawesome/react-native-fontawesome'
|
||||||
import {isIOS, isAndroid} from 'platform/detection'
|
import {useLingui} from '@lingui/react'
|
||||||
import {Button, ButtonType} from './Button'
|
|
||||||
import {Text} from '../text/Text'
|
import {usePalette} from 'lib/hooks/usePalette'
|
||||||
import {TypographyVariant} from 'lib/ThemeContext'
|
import {TypographyVariant} from 'lib/ThemeContext'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {usePalette} from 'lib/hooks/usePalette'
|
import {isAndroid, isIOS} from 'platform/detection'
|
||||||
import {getLocales} from 'expo-localization'
|
import {Text} from '../text/Text'
|
||||||
import DatePicker from 'react-native-date-picker'
|
import {Button, ButtonType} from './Button'
|
||||||
|
|
||||||
const LOCALE = getLocales()[0]
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
testID?: string
|
testID?: string
|
||||||
|
@ -30,16 +29,11 @@ interface Props {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function DateInput(props: Props) {
|
export function DateInput(props: Props) {
|
||||||
|
const {i18n} = useLingui()
|
||||||
const [show, setShow] = useState(false)
|
const [show, setShow] = useState(false)
|
||||||
const theme = useTheme()
|
const theme = useTheme()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
|
|
||||||
const formatter = React.useMemo(() => {
|
|
||||||
return new Intl.DateTimeFormat(LOCALE.languageTag, {
|
|
||||||
timeZone: props.handleAsUTC ? 'UTC' : undefined,
|
|
||||||
})
|
|
||||||
}, [props.handleAsUTC])
|
|
||||||
|
|
||||||
const onChangeInternal = useCallback(
|
const onChangeInternal = useCallback(
|
||||||
(date: Date) => {
|
(date: Date) => {
|
||||||
setShow(false)
|
setShow(false)
|
||||||
|
@ -74,7 +68,9 @@ export function DateInput(props: Props) {
|
||||||
<Text
|
<Text
|
||||||
type={props.buttonLabelType}
|
type={props.buttonLabelType}
|
||||||
style={[pal.text, props.buttonLabelStyle]}>
|
style={[pal.text, props.buttonLabelStyle]}>
|
||||||
{formatter.format(props.value)}
|
{i18n.date(props.value, {
|
||||||
|
timeZone: props.handleAsUTC ? 'UTC' : undefined,
|
||||||
|
})}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -1,19 +1,12 @@
|
||||||
export const formatCount = (num: number) =>
|
import type {I18n} from '@lingui/core'
|
||||||
Intl.NumberFormat('en-US', {
|
|
||||||
|
export const formatCount = (i18n: I18n, num: number) => {
|
||||||
|
return i18n.number(num, {
|
||||||
notation: 'compact',
|
notation: 'compact',
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
// `1,953` shouldn't be rounded up to 2k, it should be truncated.
|
||||||
// @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet
|
// @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode
|
||||||
roundingMode: 'trunc',
|
roundingMode: 'trunc',
|
||||||
}).format(num)
|
})
|
||||||
|
|
||||||
export function formatCountShortOnly(num: number): string {
|
|
||||||
if (num >= 1000000) {
|
|
||||||
return (num / 1000000).toFixed(1) + 'M'
|
|
||||||
}
|
|
||||||
if (num >= 1000) {
|
|
||||||
return (num / 1000).toFixed(1) + 'K'
|
|
||||||
}
|
|
||||||
return String(num)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,7 @@ let PostCtrls = ({
|
||||||
threadgateRecord?: AppBskyFeedThreadgate.Record
|
threadgateRecord?: AppBskyFeedThreadgate.Record
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
|
const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext)
|
||||||
|
@ -247,7 +247,7 @@ let PostCtrls = ({
|
||||||
big ? a.text_md : {fontSize: 15},
|
big ? a.text_md : {fontSize: 15},
|
||||||
a.user_select_none,
|
a.user_select_none,
|
||||||
]}>
|
]}>
|
||||||
{formatCount(post.replyCount)}
|
{formatCount(i18n, post.replyCount)}
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
@ -300,7 +300,7 @@ let PostCtrls = ({
|
||||||
: defaultCtrlColor,
|
: defaultCtrlColor,
|
||||||
],
|
],
|
||||||
]}>
|
]}>
|
||||||
{formatCount(post.likeCount)}
|
{formatCount(i18n, post.likeCount)}
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
|
|
|
@ -32,7 +32,7 @@ let RepostButton = ({
|
||||||
embeddingDisabled,
|
embeddingDisabled,
|
||||||
}: Props): React.ReactNode => {
|
}: Props): React.ReactNode => {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
const dialogControl = Dialog.useDialogControl()
|
const dialogControl = Dialog.useDialogControl()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
|
@ -79,7 +79,7 @@ let RepostButton = ({
|
||||||
big ? a.text_md : {fontSize: 15},
|
big ? a.text_md : {fontSize: 15},
|
||||||
isReposted && a.font_bold,
|
isReposted && a.font_bold,
|
||||||
]}>
|
]}>
|
||||||
{formatCount(repostCount)}
|
{formatCount(i18n, repostCount)}
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -128,6 +128,7 @@ const RepostInner = ({
|
||||||
repostCount?: number
|
repostCount?: number
|
||||||
big?: boolean
|
big?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
|
const {i18n} = useLingui()
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}>
|
<View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}>
|
||||||
<Repost style={color} width={big ? 22 : 18} />
|
<Repost style={color} width={big ? 22 : 18} />
|
||||||
|
@ -140,7 +141,7 @@ const RepostInner = ({
|
||||||
isReposted && [a.font_bold],
|
isReposted && [a.font_bold],
|
||||||
a.user_select_none,
|
a.user_select_none,
|
||||||
]}>
|
]}>
|
||||||
{formatCount(repostCount)}
|
{formatCount(i18n, repostCount)}
|
||||||
</Text>
|
</Text>
|
||||||
) : undefined}
|
) : undefined}
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -18,7 +18,6 @@ import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {useModalControls} from '#/state/modals'
|
import {useModalControls} from '#/state/modals'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
|
||||||
import {
|
import {
|
||||||
useAppPasswordDeleteMutation,
|
useAppPasswordDeleteMutation,
|
||||||
useAppPasswordsQuery,
|
useAppPasswordsQuery,
|
||||||
|
@ -218,9 +217,8 @@ function AppPassword({
|
||||||
privileged?: boolean
|
privileged?: boolean
|
||||||
}) {
|
}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const {contentLanguages} = useLanguagePrefs()
|
|
||||||
const deleteMutation = useAppPasswordDeleteMutation()
|
const deleteMutation = useAppPasswordDeleteMutation()
|
||||||
|
|
||||||
const onDelete = React.useCallback(async () => {
|
const onDelete = React.useCallback(async () => {
|
||||||
|
@ -232,9 +230,6 @@ function AppPassword({
|
||||||
control.open()
|
control.open()
|
||||||
}, [control])
|
}, [control])
|
||||||
|
|
||||||
const primaryLocale =
|
|
||||||
contentLanguages.length > 0 ? contentLanguages[0] : 'en-US'
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity
|
<TouchableOpacity
|
||||||
testID={testID}
|
testID={testID}
|
||||||
|
@ -250,14 +245,14 @@ function AppPassword({
|
||||||
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
|
<Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}>
|
||||||
<Trans>
|
<Trans>
|
||||||
Created{' '}
|
Created{' '}
|
||||||
{Intl.DateTimeFormat(primaryLocale, {
|
{i18n.date(createdAt, {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'numeric',
|
month: 'numeric',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
}).format(new Date(createdAt))}
|
})}
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
{privileged && (
|
{privileged && (
|
||||||
|
|
|
@ -30,7 +30,7 @@ import {colors, s} from 'lib/styles'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
import {NavSignupCard} from '#/view/shell/NavSignupCard'
|
||||||
import {formatCountShortOnly} from 'view/com/util/numeric/format'
|
import {formatCount} from 'view/com/util/numeric/format'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {UserAvatar} from 'view/com/util/UserAvatar'
|
import {UserAvatar} from 'view/com/util/UserAvatar'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
|
@ -68,7 +68,7 @@ let DrawerProfileCard = ({
|
||||||
account: SessionAccount
|
account: SessionAccount
|
||||||
onPressProfile: () => void
|
onPressProfile: () => void
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const {_} = useLingui()
|
const {_, i18n} = useLingui()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const {data: profile} = useProfileQuery({did: account.did})
|
const {data: profile} = useProfileQuery({did: account.did})
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ let DrawerProfileCard = ({
|
||||||
<Text type="xl" style={pal.textLight}>
|
<Text type="xl" style={pal.textLight}>
|
||||||
<Trans>
|
<Trans>
|
||||||
<Text type="xl-medium" style={pal.text}>
|
<Text type="xl-medium" style={pal.text}>
|
||||||
{formatCountShortOnly(profile?.followersCount ?? 0)}
|
{formatCount(i18n, profile?.followersCount ?? 0)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={profile?.followersCount || 0}
|
value={profile?.followersCount || 0}
|
||||||
|
@ -123,7 +123,7 @@ let DrawerProfileCard = ({
|
||||||
<Text type="xl" style={pal.textLight}>
|
<Text type="xl" style={pal.textLight}>
|
||||||
<Trans>
|
<Trans>
|
||||||
<Text type="xl-medium" style={pal.text}>
|
<Text type="xl-medium" style={pal.text}>
|
||||||
{formatCountShortOnly(profile?.followsCount ?? 0)}
|
{formatCount(i18n, profile?.followsCount ?? 0)}
|
||||||
</Text>{' '}
|
</Text>{' '}
|
||||||
<Plural
|
<Plural
|
||||||
value={profile?.followsCount || 0}
|
value={profile?.followsCount || 0}
|
||||||
|
|
Loading…
Reference in New Issue