Bluesky app fork with some witchin' additions 💫

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>

authored by

Eric Bailey
Mary
and committed by
GitHub
8651f31e d5a76183

+364 -175
+3 -3
src/components/ProfileHoverCard/index.web.tsx
··· 377 hide: () => void 378 }) { 379 const t = useTheme() 380 - const {_} = useLingui() 381 const {currentAccount} = useSession() 382 const moderation = React.useMemo( 383 () => moderateProfile(profile, moderationOpts), ··· 393 profile.viewer?.blocking || 394 profile.viewer?.blockedBy || 395 profile.viewer?.blockingByList 396 - const following = formatCount(profile.followsCount || 0) 397 - const followers = formatCount(profile.followersCount || 0) 398 const pluralizedFollowers = plural(profile.followersCount || 0, { 399 one: 'follower', 400 other: 'followers',
··· 377 hide: () => void 378 }) { 379 const t = useTheme() 380 + const {_, i18n} = useLingui() 381 const {currentAccount} = useSession() 382 const moderation = React.useMemo( 383 () => moderateProfile(profile, moderationOpts), ··· 393 profile.viewer?.blocking || 394 profile.viewer?.blockedBy || 395 profile.viewer?.blockingByList 396 + const following = formatCount(i18n, profile.followsCount || 0) 397 + const followers = formatCount(i18n, profile.followersCount || 0) 398 const pluralizedFollowers = plural(profile.followersCount || 0, { 399 one: 'follower', 400 other: 'followers',
+3 -3
src/components/dialogs/Embed.tsx
··· 43 timestamp, 44 }: Omit<EmbedDialogProps, 'control'>) { 45 const t = useTheme() 46 - const {_} = useLingui() 47 const ref = useRef<TextInput>(null) 48 const [copied, setCopied] = useState(false) 49 ··· 86 )} (<a href="${escapeHtml(profileHref)}">@${escapeHtml( 87 postAuthor.handle, 88 )}</a>) <a href="${escapeHtml(href)}">${escapeHtml( 89 - niceDate(timestamp), 90 )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` 91 - }, [postUri, postCid, record, timestamp, postAuthor]) 92 93 return ( 94 <Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
··· 43 timestamp, 44 }: Omit<EmbedDialogProps, 'control'>) { 45 const t = useTheme() 46 + const {_, i18n} = useLingui() 47 const ref = useRef<TextInput>(null) 48 const [copied, setCopied] = useState(false) 49 ··· 86 )} (<a href="${escapeHtml(profileHref)}">@${escapeHtml( 87 postAuthor.handle, 88 )}</a>) <a href="${escapeHtml(href)}">${escapeHtml( 89 + niceDate(i18n, timestamp), 90 )}</a></blockquote><script async src="${EMBED_SCRIPT}" charset="utf-8"></script>` 91 + }, [i18n, postUri, postCid, record, timestamp, postAuthor]) 92 93 return ( 94 <Dialog.Inner label="Embed post" style={[a.gap_md, {maxWidth: 500}]}>
+6 -5
src/components/dms/MessageItem.tsx
··· 11 ChatBskyConvoDefs, 12 RichText as RichTextAPI, 13 } from '@atproto/api' 14 import {msg} from '@lingui/macro' 15 import {useLingui} from '@lingui/react' 16 ··· 153 ) 154 155 const relativeTimestamp = useCallback( 156 - (timestamp: string) => { 157 const date = new Date(timestamp) 158 const now = new Date() 159 160 - const time = new Intl.DateTimeFormat(undefined, { 161 hour: 'numeric', 162 minute: 'numeric', 163 - }).format(date) 164 165 const diff = now.getTime() - date.getTime() 166 ··· 182 return _(msg`Yesterday, ${time}`) 183 } 184 185 - return new Intl.DateTimeFormat(undefined, { 186 hour: 'numeric', 187 minute: 'numeric', 188 day: 'numeric', 189 month: 'numeric', 190 year: 'numeric', 191 - }).format(date) 192 }, 193 [_], 194 )
··· 11 ChatBskyConvoDefs, 12 RichText as RichTextAPI, 13 } from '@atproto/api' 14 + import {I18n} from '@lingui/core' 15 import {msg} from '@lingui/macro' 16 import {useLingui} from '@lingui/react' 17 ··· 154 ) 155 156 const relativeTimestamp = useCallback( 157 + (i18n: I18n, timestamp: string) => { 158 const date = new Date(timestamp) 159 const now = new Date() 160 161 + const time = i18n.date(date, { 162 hour: 'numeric', 163 minute: 'numeric', 164 + }) 165 166 const diff = now.getTime() - date.getTime() 167 ··· 183 return _(msg`Yesterday, ${time}`) 184 } 185 186 + return i18n.date(date, { 187 hour: 'numeric', 188 minute: 'numeric', 189 day: 'numeric', 190 month: 'numeric', 191 year: 'numeric', 192 + }) 193 }, 194 [_], 195 )
+3 -2
src/components/forms/DateField/index.shared.tsx
··· 1 import React from 'react' 2 import {Pressable, View} from 'react-native' 3 4 import {android, atoms as a, useTheme, web} from '#/alf' 5 import * as TextField from '#/components/forms/TextField' 6 import {useInteractionState} from '#/components/hooks/useInteractionState' 7 import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' 8 import {Text} from '#/components/Typography' 9 - import {localizeDate} from './utils' 10 11 // looks like a TextField.Input, but is just a button. It'll do something different on each platform on press 12 // iOS: open a dialog with an inline date picker ··· 25 isInvalid?: boolean 26 accessibilityHint?: string 27 }) { 28 const t = useTheme() 29 30 const { ··· 91 t.atoms.text, 92 {lineHeight: a.text_md.fontSize * 1.1875}, 93 ]}> 94 - {localizeDate(value)} 95 </Text> 96 </Pressable> 97 </View>
··· 1 import React from 'react' 2 import {Pressable, View} from 'react-native' 3 + import {useLingui} from '@lingui/react' 4 5 import {android, atoms as a, useTheme, web} from '#/alf' 6 import * as TextField from '#/components/forms/TextField' 7 import {useInteractionState} from '#/components/hooks/useInteractionState' 8 import {CalendarDays_Stroke2_Corner0_Rounded as CalendarDays} from '#/components/icons/CalendarDays' 9 import {Text} from '#/components/Typography' 10 11 // looks like a TextField.Input, but is just a button. It'll do something different on each platform on press 12 // iOS: open a dialog with an inline date picker ··· 25 isInvalid?: boolean 26 accessibilityHint?: string 27 }) { 28 + const {i18n} = useLingui() 29 const t = useTheme() 30 31 const { ··· 92 t.atoms.text, 93 {lineHeight: a.text_md.fontSize * 1.1875}, 94 ]}> 95 + {i18n.date(value, {timeZone: 'UTC'})} 96 </Text> 97 </Pressable> 98 </View>
-11
src/components/forms/DateField/utils.ts
··· 1 - import {getLocales} from 'expo-localization' 2 - 3 - const LOCALE = getLocales()[0] 4 - 5 // we need the date in the form yyyy-MM-dd to pass to the input 6 export function toSimpleDateString(date: Date | string): string { 7 const _date = typeof date === 'string' ? new Date(date) : date 8 return _date.toISOString().split('T')[0] 9 } 10 - 11 - export function localizeDate(date: Date | string): string { 12 - const _date = typeof date === 'string' ? new Date(date) : date 13 - return new Intl.DateTimeFormat(LOCALE.languageTag, { 14 - timeZone: 'UTC', 15 - }).format(_date) 16 - }
··· 1 // we need the date in the form yyyy-MM-dd to pass to the input 2 export function toSimpleDateString(date: Date | string): string { 3 const _date = typeof date === 'string' ? new Date(date) : date 4 return _date.toISOString().split('T')[0] 5 }
+136 -25
src/lib/hooks/__tests__/useTimeAgo.test.ts
··· 1 import {describe, expect, it} from '@jest/globals' 2 - import {MessageDescriptor} from '@lingui/core' 3 import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns' 4 5 import {dateDiff} from '../useTimeAgo' 6 - 7 - const lingui: any = (obj: MessageDescriptor) => obj.message 8 9 const base = new Date('2024-06-17T00:00:00Z') 10 11 describe('dateDiff', () => { 12 it(`works with numbers`, () => { 13 - expect(dateDiff(subDays(base, 3), Number(base), {lingui})).toEqual('3d') 14 }) 15 it(`works with strings`, () => { 16 - expect(dateDiff(subDays(base, 3), base.toString(), {lingui})).toEqual('3d') 17 }) 18 it(`works with dates`, () => { 19 - expect(dateDiff(subDays(base, 3), base, {lingui})).toEqual('3d') 20 }) 21 22 it(`equal values return now`, () => { 23 - expect(dateDiff(base, base, {lingui})).toEqual('now') 24 }) 25 it(`future dates return now`, () => { 26 - expect(dateDiff(addDays(base, 3), base, {lingui})).toEqual('now') 27 }) 28 29 it(`values < 5 seconds ago return now`, () => { 30 const then = subSeconds(base, 4) 31 - expect(dateDiff(then, base, {lingui})).toEqual('now') 32 }) 33 it(`values >= 5 seconds ago return seconds`, () => { 34 const then = subSeconds(base, 5) 35 - expect(dateDiff(then, base, {lingui})).toEqual('5s') 36 }) 37 38 it(`values < 1 min return seconds`, () => { 39 const then = subSeconds(base, 59) 40 - expect(dateDiff(then, base, {lingui})).toEqual('59s') 41 }) 42 it(`values >= 1 min return minutes`, () => { 43 const then = subSeconds(base, 60) 44 - expect(dateDiff(then, base, {lingui})).toEqual('1m') 45 }) 46 it(`minutes round down`, () => { 47 const then = subSeconds(base, 119) 48 - expect(dateDiff(then, base, {lingui})).toEqual('1m') 49 }) 50 51 it(`values < 1 hour return minutes`, () => { 52 const then = subMinutes(base, 59) 53 - expect(dateDiff(then, base, {lingui})).toEqual('59m') 54 }) 55 it(`values >= 1 hour return hours`, () => { 56 const then = subMinutes(base, 60) 57 - expect(dateDiff(then, base, {lingui})).toEqual('1h') 58 }) 59 it(`hours round down`, () => { 60 const then = subMinutes(base, 119) 61 - expect(dateDiff(then, base, {lingui})).toEqual('1h') 62 }) 63 64 it(`values < 1 day return hours`, () => { 65 const then = subHours(base, 23) 66 - expect(dateDiff(then, base, {lingui})).toEqual('23h') 67 }) 68 it(`values >= 1 day return days`, () => { 69 const then = subHours(base, 24) 70 - expect(dateDiff(then, base, {lingui})).toEqual('1d') 71 }) 72 it(`days round down`, () => { 73 const then = subHours(base, 47) 74 - expect(dateDiff(then, base, {lingui})).toEqual('1d') 75 }) 76 77 it(`values < 30 days return days`, () => { 78 const then = subDays(base, 29) 79 - expect(dateDiff(then, base, {lingui})).toEqual('29d') 80 }) 81 it(`values >= 30 days return months`, () => { 82 const then = subDays(base, 30) 83 - expect(dateDiff(then, base, {lingui})).toEqual('1mo') 84 }) 85 it(`months round down`, () => { 86 const then = subDays(base, 59) 87 - expect(dateDiff(then, base, {lingui})).toEqual('1mo') 88 }) 89 it(`values are rounded by increments of 30`, () => { 90 const then = subDays(base, 61) 91 - expect(dateDiff(then, base, {lingui})).toEqual('2mo') 92 }) 93 94 it(`values < 360 days return months`, () => { 95 const then = subDays(base, 359) 96 - expect(dateDiff(then, base, {lingui})).toEqual('11mo') 97 }) 98 it(`values >= 360 days return the earlier value`, () => { 99 const then = subDays(base, 360) 100 - expect(dateDiff(then, base, {lingui})).toEqual(then.toLocaleDateString()) 101 }) 102 })
··· 1 import {describe, expect, it} from '@jest/globals' 2 import {addDays, subDays, subHours, subMinutes, subSeconds} from 'date-fns' 3 4 import {dateDiff} from '../useTimeAgo' 5 6 const base = new Date('2024-06-17T00:00:00Z') 7 8 describe('dateDiff', () => { 9 it(`works with numbers`, () => { 10 + const earlier = subDays(base, 3) 11 + expect(dateDiff(earlier, Number(base))).toEqual({ 12 + value: 3, 13 + unit: 'day', 14 + earlier, 15 + later: base, 16 + }) 17 }) 18 it(`works with strings`, () => { 19 + const earlier = subDays(base, 3) 20 + expect(dateDiff(earlier, base.toString())).toEqual({ 21 + value: 3, 22 + unit: 'day', 23 + earlier, 24 + later: base, 25 + }) 26 }) 27 it(`works with dates`, () => { 28 + const earlier = subDays(base, 3) 29 + expect(dateDiff(earlier, base)).toEqual({ 30 + value: 3, 31 + unit: 'day', 32 + earlier, 33 + later: base, 34 + }) 35 }) 36 37 it(`equal values return now`, () => { 38 + expect(dateDiff(base, base)).toEqual({ 39 + value: 0, 40 + unit: 'now', 41 + earlier: base, 42 + later: base, 43 + }) 44 }) 45 it(`future dates return now`, () => { 46 + const earlier = addDays(base, 3) 47 + expect(dateDiff(earlier, base)).toEqual({ 48 + value: 0, 49 + unit: 'now', 50 + earlier, 51 + later: base, 52 + }) 53 }) 54 55 it(`values < 5 seconds ago return now`, () => { 56 const then = subSeconds(base, 4) 57 + expect(dateDiff(then, base)).toEqual({ 58 + value: 0, 59 + unit: 'now', 60 + earlier: then, 61 + later: base, 62 + }) 63 }) 64 it(`values >= 5 seconds ago return seconds`, () => { 65 const then = subSeconds(base, 5) 66 + expect(dateDiff(then, base)).toEqual({ 67 + value: 5, 68 + unit: 'second', 69 + earlier: then, 70 + later: base, 71 + }) 72 }) 73 74 it(`values < 1 min return seconds`, () => { 75 const then = subSeconds(base, 59) 76 + expect(dateDiff(then, base)).toEqual({ 77 + value: 59, 78 + unit: 'second', 79 + earlier: then, 80 + later: base, 81 + }) 82 }) 83 it(`values >= 1 min return minutes`, () => { 84 const then = subSeconds(base, 60) 85 + expect(dateDiff(then, base)).toEqual({ 86 + value: 1, 87 + unit: 'minute', 88 + earlier: then, 89 + later: base, 90 + }) 91 }) 92 it(`minutes round down`, () => { 93 const then = subSeconds(base, 119) 94 + expect(dateDiff(then, base)).toEqual({ 95 + value: 1, 96 + unit: 'minute', 97 + earlier: then, 98 + later: base, 99 + }) 100 }) 101 102 it(`values < 1 hour return minutes`, () => { 103 const then = subMinutes(base, 59) 104 + expect(dateDiff(then, base)).toEqual({ 105 + value: 59, 106 + unit: 'minute', 107 + earlier: then, 108 + later: base, 109 + }) 110 }) 111 it(`values >= 1 hour return hours`, () => { 112 const then = subMinutes(base, 60) 113 + expect(dateDiff(then, base)).toEqual({ 114 + value: 1, 115 + unit: 'hour', 116 + earlier: then, 117 + later: base, 118 + }) 119 }) 120 it(`hours round down`, () => { 121 const then = subMinutes(base, 119) 122 + expect(dateDiff(then, base)).toEqual({ 123 + value: 1, 124 + unit: 'hour', 125 + earlier: then, 126 + later: base, 127 + }) 128 }) 129 130 it(`values < 1 day return hours`, () => { 131 const then = subHours(base, 23) 132 + expect(dateDiff(then, base)).toEqual({ 133 + value: 23, 134 + unit: 'hour', 135 + earlier: then, 136 + later: base, 137 + }) 138 }) 139 it(`values >= 1 day return days`, () => { 140 const then = subHours(base, 24) 141 + expect(dateDiff(then, base)).toEqual({ 142 + value: 1, 143 + unit: 'day', 144 + earlier: then, 145 + later: base, 146 + }) 147 }) 148 it(`days round down`, () => { 149 const then = subHours(base, 47) 150 + expect(dateDiff(then, base)).toEqual({ 151 + value: 1, 152 + unit: 'day', 153 + earlier: then, 154 + later: base, 155 + }) 156 }) 157 158 it(`values < 30 days return days`, () => { 159 const then = subDays(base, 29) 160 + expect(dateDiff(then, base)).toEqual({ 161 + value: 29, 162 + unit: 'day', 163 + earlier: then, 164 + later: base, 165 + }) 166 }) 167 it(`values >= 30 days return months`, () => { 168 const then = subDays(base, 30) 169 + expect(dateDiff(then, base)).toEqual({ 170 + value: 1, 171 + unit: 'month', 172 + earlier: then, 173 + later: base, 174 + }) 175 }) 176 it(`months round down`, () => { 177 const then = subDays(base, 59) 178 + expect(dateDiff(then, base)).toEqual({ 179 + value: 1, 180 + unit: 'month', 181 + earlier: then, 182 + later: base, 183 + }) 184 }) 185 it(`values are rounded by increments of 30`, () => { 186 const then = subDays(base, 61) 187 + expect(dateDiff(then, base)).toEqual({ 188 + value: 2, 189 + unit: 'month', 190 + earlier: then, 191 + later: base, 192 + }) 193 }) 194 195 it(`values < 360 days return months`, () => { 196 const then = subDays(base, 359) 197 + expect(dateDiff(then, base)).toEqual({ 198 + value: 11, 199 + unit: 'month', 200 + earlier: then, 201 + later: base, 202 + }) 203 }) 204 it(`values >= 360 days return the earlier value`, () => { 205 const then = subDays(base, 360) 206 + expect(dateDiff(then, base)).toEqual({ 207 + value: 12, 208 + unit: 'month', 209 + earlier: then, 210 + later: base, 211 + }) 212 }) 213 })
+142 -50
src/lib/hooks/useTimeAgo.ts
··· 1 import {useCallback} from 'react' 2 - import {msg, plural} from '@lingui/macro' 3 - import {I18nContext, useLingui} from '@lingui/react' 4 import {differenceInSeconds} from 'date-fns' 5 6 - export type TimeAgoOptions = { 7 - lingui: I18nContext['_'] 8 - format?: 'long' | 'short' 9 } 10 11 export function useGetTimeAgo() { 12 - const {_} = useLingui() 13 return useCallback( 14 ( 15 earlier: number | string | Date, 16 later: number | string | Date, 17 - options?: Omit<TimeAgoOptions, 'lingui'>, 18 ) => { 19 - return dateDiff(earlier, later, {lingui: _, format: options?.format}) 20 }, 21 - [_], 22 ) 23 } 24 - 25 - const NOW = 5 26 - const MINUTE = 60 27 - const HOUR = MINUTE * 60 28 - const DAY = HOUR * 24 29 - const MONTH_30 = DAY * 30 30 31 /** 32 - * Returns the difference between `earlier` and `later` dates, formatted as a 33 - * natural language string. 34 * 35 * - All month are considered exactly 30 days. 36 * - Dates assume `earlier` <= `later`, and will otherwise return 'now'. 37 - * - Differences >= 360 days are returned as the "M/D/YYYY" string 38 * - All values round down 39 */ 40 export function dateDiff( 41 earlier: number | string | Date, 42 later: number | string | Date, 43 - options: TimeAgoOptions, 44 - ): string { 45 - const _ = options.lingui 46 - const format = options?.format || 'short' 47 - const long = format === 'long' 48 - const diffSeconds = differenceInSeconds(new Date(later), new Date(earlier)) 49 50 if (diffSeconds < NOW) { 51 - return _(msg`now`) 52 } else if (diffSeconds < MINUTE) { 53 - return `${diffSeconds}${ 54 - long ? ` ${plural(diffSeconds, {one: 'second', other: 'seconds'})}` : 's' 55 - }` 56 } else if (diffSeconds < HOUR) { 57 - const diff = Math.floor(diffSeconds / MINUTE) 58 - return `${diff}${ 59 - long ? ` ${plural(diff, {one: 'minute', other: 'minutes'})}` : 'm' 60 - }` 61 } else if (diffSeconds < DAY) { 62 - const diff = Math.floor(diffSeconds / HOUR) 63 - return `${diff}${ 64 - long ? ` ${plural(diff, {one: 'hour', other: 'hours'})}` : 'h' 65 - }` 66 } else if (diffSeconds < MONTH_30) { 67 - const diff = Math.floor(diffSeconds / DAY) 68 - return `${diff}${ 69 - long ? ` ${plural(diff, {one: 'day', other: 'days'})}` : 'd' 70 - }` 71 } else { 72 - const diff = Math.floor(diffSeconds / MONTH_30) 73 - if (diff < 12) { 74 - return `${diff}${ 75 - long ? ` ${plural(diff, {one: 'month', other: 'months'})}` : 'mo' 76 - }` 77 - } else { 78 - const str = new Date(earlier).toLocaleDateString() 79 80 - if (long) { 81 - return _(msg`on ${str}`) 82 } 83 - return str 84 } 85 } 86 }
··· 1 import {useCallback} from 'react' 2 + import {I18n} from '@lingui/core' 3 + import {defineMessage, msg, plural} from '@lingui/macro' 4 + import {useLingui} from '@lingui/react' 5 import {differenceInSeconds} from 'date-fns' 6 7 + export type DateDiffFormat = 'long' | 'short' 8 + 9 + type DateDiff = { 10 + value: number 11 + unit: 'now' | 'second' | 'minute' | 'hour' | 'day' | 'month' 12 + earlier: Date 13 + later: Date 14 } 15 16 + const NOW = 5 17 + const MINUTE = 60 18 + const HOUR = MINUTE * 60 19 + const DAY = HOUR * 24 20 + const MONTH_30 = DAY * 30 21 + 22 export function useGetTimeAgo() { 23 + const {i18n} = useLingui() 24 return useCallback( 25 ( 26 earlier: number | string | Date, 27 later: number | string | Date, 28 + options?: {format: DateDiffFormat}, 29 ) => { 30 + const diff = dateDiff(earlier, later) 31 + return formatDateDiff({diff, i18n, format: options?.format}) 32 }, 33 + [i18n], 34 ) 35 } 36 37 /** 38 + * Returns the difference between `earlier` and `later` dates, based on 39 + * opinionated rules. 40 * 41 * - All month are considered exactly 30 days. 42 * - Dates assume `earlier` <= `later`, and will otherwise return 'now'. 43 * - All values round down 44 */ 45 export function dateDiff( 46 earlier: number | string | Date, 47 later: number | string | Date, 48 + ): DateDiff { 49 + let diff = { 50 + value: 0, 51 + unit: 'now' as DateDiff['unit'], 52 + } 53 + const e = new Date(earlier) 54 + const l = new Date(later) 55 + const diffSeconds = differenceInSeconds(l, e) 56 57 if (diffSeconds < NOW) { 58 + diff = { 59 + value: 0, 60 + unit: 'now' as DateDiff['unit'], 61 + } 62 } else if (diffSeconds < MINUTE) { 63 + diff = { 64 + value: diffSeconds, 65 + unit: 'second' as DateDiff['unit'], 66 + } 67 } else if (diffSeconds < HOUR) { 68 + const value = Math.floor(diffSeconds / MINUTE) 69 + diff = { 70 + value, 71 + unit: 'minute' as DateDiff['unit'], 72 + } 73 } else if (diffSeconds < DAY) { 74 + const value = Math.floor(diffSeconds / HOUR) 75 + diff = { 76 + value, 77 + unit: 'hour' as DateDiff['unit'], 78 + } 79 } else if (diffSeconds < MONTH_30) { 80 + const value = Math.floor(diffSeconds / DAY) 81 + diff = { 82 + value, 83 + unit: 'day' as DateDiff['unit'], 84 + } 85 } else { 86 + const value = Math.floor(diffSeconds / MONTH_30) 87 + diff = { 88 + value, 89 + unit: 'month' as DateDiff['unit'], 90 + } 91 + } 92 93 + return { 94 + ...diff, 95 + earlier: e, 96 + later: l, 97 + } 98 + } 99 + 100 + /** 101 + * Accepts a `DateDiff` and teturns the difference between `earlier` and 102 + * `later` dates, formatted as a natural language string. 103 + * 104 + * - All month are considered exactly 30 days. 105 + * - Dates assume `earlier` <= `later`, and will otherwise return 'now'. 106 + * - Differences >= 360 days are returned as the "M/D/YYYY" string 107 + * - All values round down 108 + */ 109 + export function formatDateDiff({ 110 + diff, 111 + format = 'short', 112 + i18n, 113 + }: { 114 + diff: DateDiff 115 + format?: DateDiffFormat 116 + i18n: I18n 117 + }): string { 118 + const long = format === 'long' 119 + 120 + switch (diff.unit) { 121 + case 'now': { 122 + return i18n._(msg`now`) 123 + } 124 + case 'second': { 125 + return long 126 + ? i18n._(plural(diff.value, {one: '# second', other: '# seconds'})) 127 + : i18n._( 128 + defineMessage({ 129 + message: `${diff.value}s`, 130 + comment: `How many seconds have passed, displayed in a narrow form`, 131 + }), 132 + ) 133 + } 134 + case 'minute': { 135 + return long 136 + ? i18n._(plural(diff.value, {one: '# minute', other: '# minutes'})) 137 + : i18n._( 138 + defineMessage({ 139 + message: `${diff.value}m`, 140 + comment: `How many minutes have passed, displayed in a narrow form`, 141 + }), 142 + ) 143 + } 144 + case 'hour': { 145 + return long 146 + ? i18n._(plural(diff.value, {one: '# hour', other: '# hours'})) 147 + : i18n._( 148 + defineMessage({ 149 + message: `${diff.value}h`, 150 + comment: `How many hours have passed, displayed in a narrow form`, 151 + }), 152 + ) 153 + } 154 + case 'day': { 155 + return long 156 + ? i18n._(plural(diff.value, {one: '# day', other: '# days'})) 157 + : i18n._( 158 + defineMessage({ 159 + message: `${diff.value}d`, 160 + comment: `How many days have passed, displayed in a narrow form`, 161 + }), 162 + ) 163 + } 164 + case 'month': { 165 + if (diff.value < 12) { 166 + return long 167 + ? i18n._(plural(diff.value, {one: '# month', other: '# months'})) 168 + : i18n._( 169 + defineMessage({ 170 + message: `${diff.value}mo`, 171 + comment: `How many months have passed, displayed in a narrow form`, 172 + }), 173 + ) 174 } 175 + return i18n.date(new Date(diff.earlier)) 176 } 177 } 178 }
+8 -9
src/lib/strings/time.ts
··· 1 - export function niceDate(date: number | string | Date) { 2 const d = new Date(date) 3 - return `${d.toLocaleDateString('en-us', { 4 - year: 'numeric', 5 - month: 'short', 6 - day: 'numeric', 7 - })} at ${d.toLocaleTimeString(undefined, { 8 - hour: 'numeric', 9 - minute: '2-digit', 10 - })}` 11 } 12 13 export function getAge(birthDate: Date): number {
··· 1 + import {I18n} from '@lingui/core' 2 + 3 + export function niceDate(i18n: I18n, date: number | string | Date) { 4 const d = new Date(date) 5 + 6 + return i18n.date(d, { 7 + dateStyle: 'long', 8 + timeStyle: 'short', 9 + }) 10 } 11 12 export function getAge(birthDate: Date): number {
+4 -4
src/screens/Profile/Header/Metrics.tsx
··· 17 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 18 }) { 19 const t = useTheme() 20 - const {_} = useLingui() 21 - const following = formatCount(profile.followsCount || 0) 22 - const followers = formatCount(profile.followersCount || 0) 23 const pluralizedFollowers = plural(profile.followersCount || 0, { 24 one: 'follower', 25 other: 'followers', ··· 54 </Text> 55 </InlineLinkText> 56 <Text style={[a.font_bold, t.atoms.text, a.text_md]}> 57 - {formatCount(profile.postsCount || 0)}{' '} 58 <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 59 {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 60 </Text>
··· 17 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 18 }) { 19 const t = useTheme() 20 + const {_, i18n} = useLingui() 21 + const following = formatCount(i18n, profile.followsCount || 0) 22 + const followers = formatCount(i18n, profile.followersCount || 0) 23 const pluralizedFollowers = plural(profile.followersCount || 0, { 24 one: 'follower', 25 other: 'followers', ··· 54 </Text> 55 </InlineLinkText> 56 <Text style={[a.font_bold, t.atoms.text, a.text_md]}> 57 + {formatCount(i18n, profile.postsCount || 0)}{' '} 58 <Text style={[t.atoms.text_contrast_medium, a.font_normal, a.text_md]}> 59 {plural(profile.postsCount || 0, {one: 'post', other: 'posts'})} 60 </Text>
+4 -2
src/screens/StarterPack/StarterPackLandingScreen.tsx
··· 113 moderationOpts: ModerationOpts 114 }) { 115 const {creator, listItemsSample, feeds} = starterPack 116 - const {_} = useLingui() 117 const t = useTheme() 118 const activeStarterPack = useActiveStarterPack() 119 const setActiveStarterPack = useSetActiveStarterPack() ··· 225 t.atoms.text_contrast_medium, 226 ]} 227 numberOfLines={1}> 228 - <Trans>{formatCount(JOINED_THIS_WEEK)} joined this week</Trans> 229 </Text> 230 </View> 231 </View>
··· 113 moderationOpts: ModerationOpts 114 }) { 115 const {creator, listItemsSample, feeds} = starterPack 116 + const {_, i18n} = useLingui() 117 const t = useTheme() 118 const activeStarterPack = useActiveStarterPack() 119 const setActiveStarterPack = useSetActiveStarterPack() ··· 225 t.atoms.text_contrast_medium, 226 ]} 227 numberOfLines={1}> 228 + <Trans> 229 + {formatCount(i18n, JOINED_THIS_WEEK)} joined this week 230 + </Trans> 231 </Text> 232 </View> 233 </View>
+3 -3
src/view/com/notifications/FeedItem.tsx
··· 84 }): React.ReactNode => { 85 const queryClient = useQueryClient() 86 const pal = usePalette('default') 87 - const {_} = useLingui() 88 const t = useTheme() 89 const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false) 90 const itemHref = useMemo(() => { ··· 225 } 226 227 const formattedCount = 228 - authors.length > 1 ? formatCount(authors.length - 1) : '' 229 const firstAuthorName = sanitizeDisplayName( 230 authors[0].profile.displayName || authors[0].profile.handle, 231 ) 232 - const niceTimestamp = niceDate(item.notification.indexedAt) 233 const a11yLabelUsers = 234 authors.length > 1 235 ? _(msg` and `) +
··· 84 }): React.ReactNode => { 85 const queryClient = useQueryClient() 86 const pal = usePalette('default') 87 + const {_, i18n} = useLingui() 88 const t = useTheme() 89 const [isAuthorsExpanded, setAuthorsExpanded] = useState<boolean>(false) 90 const itemHref = useMemo(() => { ··· 225 } 226 227 const formattedCount = 228 + authors.length > 1 ? formatCount(i18n, authors.length - 1) : '' 229 const firstAuthorName = sanitizeDisplayName( 230 authors[0].profile.displayName || authors[0].profile.handle, 231 ) 232 + const niceTimestamp = niceDate(i18n, item.notification.indexedAt) 233 const a11yLabelUsers = 234 authors.length > 1 235 ? _(msg` and `) +
+8 -6
src/view/com/post-thread/PostThreadItem.tsx
··· 181 threadgateRecord?: AppBskyFeedThreadgate.Record 182 }): React.ReactNode => { 183 const pal = usePalette('default') 184 - const {_} = useLingui() 185 const langPrefs = useLanguagePrefs() 186 const {openComposer} = useComposerControls() 187 const [limitLines, setLimitLines] = React.useState( ··· 388 type="lg" 389 style={pal.textLight}> 390 <Text type="xl-bold" style={pal.text}> 391 - {formatCount(post.repostCount)} 392 </Text>{' '} 393 <Plural 394 value={post.repostCount} ··· 410 type="lg" 411 style={pal.textLight}> 412 <Text type="xl-bold" style={pal.text}> 413 - {formatCount(post.quoteCount)} 414 </Text>{' '} 415 <Plural 416 value={post.quoteCount} ··· 430 type="lg" 431 style={pal.textLight}> 432 <Text type="xl-bold" style={pal.text}> 433 - {formatCount(post.likeCount)} 434 </Text>{' '} 435 <Plural value={post.likeCount} one="like" other="likes" /> 436 </Text> ··· 705 translatorUrl: string 706 }) { 707 const pal = usePalette('default') 708 - const {_} = useLingui() 709 const openLink = useOpenLink() 710 const isRootPost = !('reply' in post.record) 711 ··· 723 s.mt2, 724 s.mb10, 725 ]}> 726 - <Text style={[a.text_sm, pal.textLight]}>{niceDate(post.indexedAt)}</Text> 727 {isRootPost && ( 728 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 729 )}
··· 181 threadgateRecord?: AppBskyFeedThreadgate.Record 182 }): React.ReactNode => { 183 const pal = usePalette('default') 184 + const {_, i18n} = useLingui() 185 const langPrefs = useLanguagePrefs() 186 const {openComposer} = useComposerControls() 187 const [limitLines, setLimitLines] = React.useState( ··· 388 type="lg" 389 style={pal.textLight}> 390 <Text type="xl-bold" style={pal.text}> 391 + {formatCount(i18n, post.repostCount)} 392 </Text>{' '} 393 <Plural 394 value={post.repostCount} ··· 410 type="lg" 411 style={pal.textLight}> 412 <Text type="xl-bold" style={pal.text}> 413 + {formatCount(i18n, post.quoteCount)} 414 </Text>{' '} 415 <Plural 416 value={post.quoteCount} ··· 430 type="lg" 431 style={pal.textLight}> 432 <Text type="xl-bold" style={pal.text}> 433 + {formatCount(i18n, post.likeCount)} 434 </Text>{' '} 435 <Plural value={post.likeCount} one="like" other="likes" /> 436 </Text> ··· 705 translatorUrl: string 706 }) { 707 const pal = usePalette('default') 708 + const {_, i18n} = useLingui() 709 const openLink = useOpenLink() 710 const isRootPost = !('reply' in post.record) 711 ··· 723 s.mt2, 724 s.mb10, 725 ]}> 726 + <Text style={[a.text_sm, pal.textLight]}> 727 + {niceDate(i18n, post.indexedAt)} 728 + </Text> 729 {isRootPost && ( 730 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} /> 731 )}
+5 -2
src/view/com/util/PostMeta.tsx
··· 1 import React, {memo, useCallback} from 'react' 2 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' 4 import {useQueryClient} from '@tanstack/react-query' 5 6 import {precacheProfile} from '#/state/queries/profile' ··· 35 } 36 37 let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 38 const pal = usePalette('default') 39 const displayName = opts.author.displayName || opts.author.handle 40 const handle = opts.author.handle ··· 101 type="md" 102 style={pal.textLight} 103 text={timeElapsed} 104 - accessibilityLabel={niceDate(opts.timestamp)} 105 - title={niceDate(opts.timestamp)} 106 accessibilityHint="" 107 href={opts.postHref} 108 onBeforePress={onBeforePressPost}
··· 1 import React, {memo, useCallback} from 'react' 2 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 import {AppBskyActorDefs, ModerationDecision, ModerationUI} from '@atproto/api' 4 + import {useLingui} from '@lingui/react' 5 import {useQueryClient} from '@tanstack/react-query' 6 7 import {precacheProfile} from '#/state/queries/profile' ··· 36 } 37 38 let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 39 + const {i18n} = useLingui() 40 + 41 const pal = usePalette('default') 42 const displayName = opts.author.displayName || opts.author.handle 43 const handle = opts.author.handle ··· 104 type="md" 105 style={pal.textLight} 106 text={timeElapsed} 107 + accessibilityLabel={niceDate(i18n, opts.timestamp)} 108 + title={niceDate(i18n, opts.timestamp)} 109 accessibilityHint="" 110 href={opts.postHref} 111 onBeforePress={onBeforePressPost}
+8 -4
src/view/com/util/TimeElapsed.tsx
··· 1 import React from 'react' 2 3 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 4 import {useTickEveryMinute} from '#/state/shell' ··· 10 }: { 11 timestamp: string 12 children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element 13 - timeToString?: (timeElapsed: string) => string 14 }) { 15 const ago = useGetTimeAgo() 16 - const format = timeToString ?? ago 17 const tick = useTickEveryMinute() 18 const [timeElapsed, setTimeAgo] = React.useState(() => 19 - format(timestamp, tick), 20 ) 21 22 const [prevTick, setPrevTick] = React.useState(tick) 23 if (prevTick !== tick) { 24 setPrevTick(tick) 25 - setTimeAgo(format(timestamp, tick)) 26 } 27 28 return children({timeElapsed})
··· 1 import React from 'react' 2 + import {I18n} from '@lingui/core' 3 + import {useLingui} from '@lingui/react' 4 5 import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo' 6 import {useTickEveryMinute} from '#/state/shell' ··· 12 }: { 13 timestamp: string 14 children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element 15 + timeToString?: (i18n: I18n, timeElapsed: string) => string 16 }) { 17 + const {i18n} = useLingui() 18 const ago = useGetTimeAgo() 19 const tick = useTickEveryMinute() 20 const [timeElapsed, setTimeAgo] = React.useState(() => 21 + timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick), 22 ) 23 24 const [prevTick, setPrevTick] = React.useState(tick) 25 if (prevTick !== tick) { 26 setPrevTick(tick) 27 + setTimeAgo( 28 + timeToString ? timeToString(i18n, timestamp) : ago(timestamp, tick), 29 + ) 30 } 31 32 return children({timeElapsed})
+12 -16
src/view/com/util/forms/DateInput.tsx
··· 1 - import React, {useState, useCallback} from 'react' 2 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 import { 4 FontAwesomeIcon, 5 FontAwesomeIconStyle, 6 } from '@fortawesome/react-native-fontawesome' 7 - import {isIOS, isAndroid} from 'platform/detection' 8 - import {Button, ButtonType} from './Button' 9 - import {Text} from '../text/Text' 10 import {TypographyVariant} from 'lib/ThemeContext' 11 import {useTheme} from 'lib/ThemeContext' 12 - import {usePalette} from 'lib/hooks/usePalette' 13 - import {getLocales} from 'expo-localization' 14 - import DatePicker from 'react-native-date-picker' 15 - 16 - const LOCALE = getLocales()[0] 17 18 interface Props { 19 testID?: string ··· 30 } 31 32 export function DateInput(props: Props) { 33 const [show, setShow] = useState(false) 34 const theme = useTheme() 35 const pal = usePalette('default') 36 - 37 - const formatter = React.useMemo(() => { 38 - return new Intl.DateTimeFormat(LOCALE.languageTag, { 39 - timeZone: props.handleAsUTC ? 'UTC' : undefined, 40 - }) 41 - }, [props.handleAsUTC]) 42 43 const onChangeInternal = useCallback( 44 (date: Date) => { ··· 74 <Text 75 type={props.buttonLabelType} 76 style={[pal.text, props.buttonLabelStyle]}> 77 - {formatter.format(props.value)} 78 </Text> 79 </View> 80 </Button>
··· 1 + import React, {useCallback, useState} from 'react' 2 import {StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native' 3 + import DatePicker from 'react-native-date-picker' 4 import { 5 FontAwesomeIcon, 6 FontAwesomeIconStyle, 7 } from '@fortawesome/react-native-fontawesome' 8 + import {useLingui} from '@lingui/react' 9 + 10 + import {usePalette} from 'lib/hooks/usePalette' 11 import {TypographyVariant} from 'lib/ThemeContext' 12 import {useTheme} from 'lib/ThemeContext' 13 + import {isAndroid, isIOS} from 'platform/detection' 14 + import {Text} from '../text/Text' 15 + import {Button, ButtonType} from './Button' 16 17 interface Props { 18 testID?: string ··· 29 } 30 31 export function DateInput(props: Props) { 32 + const {i18n} = useLingui() 33 const [show, setShow] = useState(false) 34 const theme = useTheme() 35 const pal = usePalette('default') 36 37 const onChangeInternal = useCallback( 38 (date: Date) => { ··· 68 <Text 69 type={props.buttonLabelType} 70 style={[pal.text, props.buttonLabelStyle]}> 71 + {i18n.date(props.value, { 72 + timeZone: props.handleAsUTC ? 'UTC' : undefined, 73 + })} 74 </Text> 75 </View> 76 </Button>
+5 -12
src/view/com/util/numeric/format.ts
··· 1 - export const formatCount = (num: number) => 2 - Intl.NumberFormat('en-US', { 3 notation: 'compact', 4 maximumFractionDigits: 1, 5 // `1,953` shouldn't be rounded up to 2k, it should be truncated. 6 // @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet 7 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode 8 roundingMode: 'trunc', 9 - }).format(num) 10 - 11 - export function formatCountShortOnly(num: number): string { 12 - if (num >= 1000000) { 13 - return (num / 1000000).toFixed(1) + 'M' 14 - } 15 - if (num >= 1000) { 16 - return (num / 1000).toFixed(1) + 'K' 17 - } 18 - return String(num) 19 }
··· 1 + import type {I18n} from '@lingui/core' 2 + 3 + export const formatCount = (i18n: I18n, num: number) => { 4 + return i18n.number(num, { 5 notation: 'compact', 6 maximumFractionDigits: 1, 7 // `1,953` shouldn't be rounded up to 2k, it should be truncated. 8 // @ts-expect-error: `roundingMode` doesn't seem to be in the typings yet 9 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#roundingmode 10 roundingMode: 'trunc', 11 + }) 12 }
+3 -3
src/view/com/util/post-ctrls/PostCtrls.tsx
··· 75 threadgateRecord?: AppBskyFeedThreadgate.Record 76 }): React.ReactNode => { 77 const t = useTheme() 78 - const {_} = useLingui() 79 const {openComposer} = useComposerControls() 80 const {currentAccount} = useSession() 81 const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) ··· 247 big ? a.text_md : {fontSize: 15}, 248 a.user_select_none, 249 ]}> 250 - {formatCount(post.replyCount)} 251 </Text> 252 ) : undefined} 253 </Pressable> ··· 300 : defaultCtrlColor, 301 ], 302 ]}> 303 - {formatCount(post.likeCount)} 304 </Text> 305 ) : undefined} 306 </Pressable>
··· 75 threadgateRecord?: AppBskyFeedThreadgate.Record 76 }): React.ReactNode => { 77 const t = useTheme() 78 + const {_, i18n} = useLingui() 79 const {openComposer} = useComposerControls() 80 const {currentAccount} = useSession() 81 const [queueLike, queueUnlike] = usePostLikeMutationQueue(post, logContext) ··· 247 big ? a.text_md : {fontSize: 15}, 248 a.user_select_none, 249 ]}> 250 + {formatCount(i18n, post.replyCount)} 251 </Text> 252 ) : undefined} 253 </Pressable> ··· 300 : defaultCtrlColor, 301 ], 302 ]}> 303 + {formatCount(i18n, post.likeCount)} 304 </Text> 305 ) : undefined} 306 </Pressable>
+2 -2
src/view/com/util/post-ctrls/RepostButton.tsx
··· 32 embeddingDisabled, 33 }: Props): React.ReactNode => { 34 const t = useTheme() 35 - const {_} = useLingui() 36 const requireAuth = useRequireAuth() 37 const dialogControl = Dialog.useDialogControl() 38 const playHaptic = useHaptics() ··· 79 big ? a.text_md : {fontSize: 15}, 80 isReposted && a.font_bold, 81 ]}> 82 - {formatCount(repostCount)} 83 </Text> 84 ) : undefined} 85 </Button>
··· 32 embeddingDisabled, 33 }: Props): React.ReactNode => { 34 const t = useTheme() 35 + const {_, i18n} = useLingui() 36 const requireAuth = useRequireAuth() 37 const dialogControl = Dialog.useDialogControl() 38 const playHaptic = useHaptics() ··· 79 big ? a.text_md : {fontSize: 15}, 80 isReposted && a.font_bold, 81 ]}> 82 + {formatCount(i18n, repostCount)} 83 </Text> 84 ) : undefined} 85 </Button>
+2 -1
src/view/com/util/post-ctrls/RepostButton.web.tsx
··· 128 repostCount?: number 129 big?: boolean 130 }) => { 131 return ( 132 <View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}> 133 <Repost style={color} width={big ? 22 : 18} /> ··· 140 isReposted && [a.font_bold], 141 a.user_select_none, 142 ]}> 143 - {formatCount(repostCount)} 144 </Text> 145 ) : undefined} 146 </View>
··· 128 repostCount?: number 129 big?: boolean 130 }) => { 131 + const {i18n} = useLingui() 132 return ( 133 <View style={[a.flex_row, a.align_center, a.gap_xs, {padding: 5}]}> 134 <Repost style={color} width={big ? 22 : 18} /> ··· 141 isReposted && [a.font_bold], 142 a.user_select_none, 143 ]}> 144 + {formatCount(i18n, repostCount)} 145 </Text> 146 ) : undefined} 147 </View>
+3 -8
src/view/screens/AppPasswords.tsx
··· 18 import {CommonNavigatorParams} from '#/lib/routes/types' 19 import {cleanError} from '#/lib/strings/errors' 20 import {useModalControls} from '#/state/modals' 21 - import {useLanguagePrefs} from '#/state/preferences' 22 import { 23 useAppPasswordDeleteMutation, 24 useAppPasswordsQuery, ··· 218 privileged?: boolean 219 }) { 220 const pal = usePalette('default') 221 - const {_} = useLingui() 222 const control = useDialogControl() 223 - const {contentLanguages} = useLanguagePrefs() 224 const deleteMutation = useAppPasswordDeleteMutation() 225 226 const onDelete = React.useCallback(async () => { ··· 232 control.open() 233 }, [control]) 234 235 - const primaryLocale = 236 - contentLanguages.length > 0 ? contentLanguages[0] : 'en-US' 237 - 238 return ( 239 <TouchableOpacity 240 testID={testID} ··· 250 <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}> 251 <Trans> 252 Created{' '} 253 - {Intl.DateTimeFormat(primaryLocale, { 254 year: 'numeric', 255 month: 'numeric', 256 day: 'numeric', 257 hour: '2-digit', 258 minute: '2-digit', 259 second: '2-digit', 260 - }).format(new Date(createdAt))} 261 </Trans> 262 </Text> 263 {privileged && (
··· 18 import {CommonNavigatorParams} from '#/lib/routes/types' 19 import {cleanError} from '#/lib/strings/errors' 20 import {useModalControls} from '#/state/modals' 21 import { 22 useAppPasswordDeleteMutation, 23 useAppPasswordsQuery, ··· 217 privileged?: boolean 218 }) { 219 const pal = usePalette('default') 220 + const {_, i18n} = useLingui() 221 const control = useDialogControl() 222 const deleteMutation = useAppPasswordDeleteMutation() 223 224 const onDelete = React.useCallback(async () => { ··· 230 control.open() 231 }, [control]) 232 233 return ( 234 <TouchableOpacity 235 testID={testID} ··· 245 <Text type="md" style={[pal.text, styles.pr10]} numberOfLines={1}> 246 <Trans> 247 Created{' '} 248 + {i18n.date(createdAt, { 249 year: 'numeric', 250 month: 'numeric', 251 day: 'numeric', 252 hour: '2-digit', 253 minute: '2-digit', 254 second: '2-digit', 255 + })} 256 </Trans> 257 </Text> 258 {privileged && (
+4 -4
src/view/shell/Drawer.tsx
··· 30 import {useTheme} from 'lib/ThemeContext' 31 import {isWeb} from 'platform/detection' 32 import {NavSignupCard} from '#/view/shell/NavSignupCard' 33 - import {formatCountShortOnly} from 'view/com/util/numeric/format' 34 import {Text} from 'view/com/util/text/Text' 35 import {UserAvatar} from 'view/com/util/UserAvatar' 36 import {atoms as a} from '#/alf' ··· 68 account: SessionAccount 69 onPressProfile: () => void 70 }): React.ReactNode => { 71 - const {_} = useLingui() 72 const pal = usePalette('default') 73 const {data: profile} = useProfileQuery({did: account.did}) 74 ··· 108 <Text type="xl" style={pal.textLight}> 109 <Trans> 110 <Text type="xl-medium" style={pal.text}> 111 - {formatCountShortOnly(profile?.followersCount ?? 0)} 112 </Text>{' '} 113 <Plural 114 value={profile?.followersCount || 0} ··· 123 <Text type="xl" style={pal.textLight}> 124 <Trans> 125 <Text type="xl-medium" style={pal.text}> 126 - {formatCountShortOnly(profile?.followsCount ?? 0)} 127 </Text>{' '} 128 <Plural 129 value={profile?.followsCount || 0}
··· 30 import {useTheme} from 'lib/ThemeContext' 31 import {isWeb} from 'platform/detection' 32 import {NavSignupCard} from '#/view/shell/NavSignupCard' 33 + import {formatCount} from 'view/com/util/numeric/format' 34 import {Text} from 'view/com/util/text/Text' 35 import {UserAvatar} from 'view/com/util/UserAvatar' 36 import {atoms as a} from '#/alf' ··· 68 account: SessionAccount 69 onPressProfile: () => void 70 }): React.ReactNode => { 71 + const {_, i18n} = useLingui() 72 const pal = usePalette('default') 73 const {data: profile} = useProfileQuery({did: account.did}) 74 ··· 108 <Text type="xl" style={pal.textLight}> 109 <Trans> 110 <Text type="xl-medium" style={pal.text}> 111 + {formatCount(i18n, profile?.followersCount ?? 0)} 112 </Text>{' '} 113 <Plural 114 value={profile?.followersCount || 0} ··· 123 <Text type="xl" style={pal.textLight}> 124 <Trans> 125 <Text type="xl-medium" style={pal.text}> 126 + {formatCount(i18n, profile?.followsCount ?? 0)} 127 </Text>{' '} 128 <Plural 129 value={profile?.followsCount || 0}