An ATproto social media client -- with an independent Appview.

108 fixes (#8977)

* Translation comment

* Fix error handling in starter pack generation

* Allow access to DM settings for age restricted users

* Leave post stat unit formatting up to translators

authored by

Eric Bailey and committed by
GitHub
c129108b 0b480bda

+89 -67
+4 -5
src/components/PostControls/RepostButton.tsx
··· 10 10 import * as Dialog from '#/components/Dialog' 11 11 import {CloseQuote_Stroke2_Corner1_Rounded as Quote} from '#/components/icons/Quote' 12 12 import {Repost_Stroke2_Corner3_Rounded as Repost} from '#/components/icons/Repost' 13 - import {formatPostStatCount} from '#/components/PostControls/util' 13 + import {useFormatPostStatCount} from '#/components/PostControls/util' 14 14 import {Text} from '#/components/Typography' 15 15 import { 16 16 PostControlButton, ··· 25 25 onQuote: () => void 26 26 big?: boolean 27 27 embeddingDisabled: boolean 28 - compactCount?: boolean 29 28 } 30 29 31 30 let RepostButton = ({ ··· 35 34 onQuote, 36 35 big, 37 36 embeddingDisabled, 38 - compactCount, 39 37 }: Props): React.ReactNode => { 40 38 const t = useTheme() 41 - const {_, i18n} = useLingui() 39 + const {_} = useLingui() 42 40 const requireAuth = useRequireAuth() 43 41 const dialogControl = Dialog.useDialogControl() 42 + const formatPostStatCount = useFormatPostStatCount() 44 43 45 44 const onPress = () => requireAuth(() => dialogControl.open()) 46 45 ··· 88 87 <PostControlButtonIcon icon={Repost} /> 89 88 {typeof repostCount !== 'undefined' && repostCount > 0 && ( 90 89 <PostControlButtonText testID="repostCount"> 91 - {formatPostStatCount(i18n, repostCount, {compact: compactCount})} 90 + {formatPostStatCount(repostCount)} 92 91 </PostControlButtonText> 93 92 )} 94 93 </PostControlButton>
+4 -7
src/components/PostControls/index.tsx
··· 27 27 import * as Toast from '#/view/com/util/Toast' 28 28 import {atoms as a, flatten, useBreakpoints} from '#/alf' 29 29 import {Reply as Bubble} from '#/components/icons/Reply' 30 - import {formatPostStatCount} from '#/components/PostControls/util' 30 + import {useFormatPostStatCount} from '#/components/PostControls/util' 31 31 import {BookmarkButton} from './BookmarkButton' 32 32 import { 33 33 PostControlButton, ··· 69 69 viaRepost?: {uri: string; cid: string} 70 70 variant?: 'compact' | 'normal' | 'large' 71 71 }): React.ReactNode => { 72 - const {_, i18n} = useLingui() 72 + const {_} = useLingui() 73 73 const {openComposer} = useOpenComposer() 74 74 const {feedDescriptor} = useFeedFeedbackContext() 75 75 const [queueLike, queueUnlike] = usePostLikeMutationQueue( ··· 95 95 ) 96 96 const replyDisabled = post.viewer?.replyDisabled 97 97 const {gtPhone} = useBreakpoints() 98 + const formatPostStatCount = useFormatPostStatCount() 98 99 99 100 const [hasLikeIconBeenToggled, setHasLikeIconBeenToggled] = useState(false) 100 101 ··· 232 233 <PostControlButtonIcon icon={Bubble} /> 233 234 {typeof post.replyCount !== 'undefined' && post.replyCount > 0 && ( 234 235 <PostControlButtonText> 235 - {formatPostStatCount(i18n, post.replyCount, { 236 - compact: variant === 'compact', 237 - })} 236 + {formatPostStatCount(post.replyCount)} 238 237 </PostControlButtonText> 239 238 )} 240 239 </PostControlButton> ··· 247 246 onQuote={onQuote} 248 247 big={big} 249 248 embeddingDisabled={Boolean(post.viewer?.embeddingDisabled)} 250 - compactCount={variant === 'compact'} 251 249 /> 252 250 </View> 253 251 <View style={[a.flex_1, a.align_start]}> ··· 288 286 big={big} 289 287 isLiked={Boolean(post.viewer?.like)} 290 288 hasBeenToggled={hasLikeIconBeenToggled} 291 - compactCount={variant === 'compact'} 292 289 /> 293 290 </PostControlButton> 294 291 </View>
+41 -17
src/components/PostControls/util.ts
··· 1 - import {type I18n} from '@lingui/core' 1 + import {useCallback} from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 2 4 3 5 /** 4 6 * This matches `formatCount` from `view/com/util/numeric/format.ts`, but has 5 7 * additional truncation logic for large numbers. `roundingMode` should always 6 8 * match the original impl, regardless of if we add more formatting here. 7 9 */ 8 - export function formatPostStatCount( 9 - i18n: I18n, 10 - count: number, 11 - { 12 - compact = false, 13 - }: { 14 - compact?: boolean 15 - } = {}, 16 - ): string { 17 - const isOver10k = count >= 10_000 18 - return i18n.number(count, { 19 - notation: 'compact', 20 - maximumFractionDigits: isOver10k || compact ? 0 : 1, 21 - // @ts-expect-error - roundingMode not in the types 22 - roundingMode: 'trunc', 23 - }) 10 + export function useFormatPostStatCount() { 11 + const {i18n} = useLingui() 12 + 13 + return useCallback( 14 + (postStatCount: number) => { 15 + const isOver1k = postStatCount >= 1_000 16 + const isOver10k = postStatCount >= 10_000 17 + const isOver1M = postStatCount >= 1_000_000 18 + const formatted = i18n.number(postStatCount, { 19 + notation: 'compact', 20 + maximumFractionDigits: isOver10k ? 0 : 1, 21 + // @ts-expect-error - roundingMode not in the types 22 + roundingMode: 'trunc', 23 + }) 24 + const count = formatted.replace(/\D+$/g, '') 25 + 26 + if (isOver1M) { 27 + return i18n._( 28 + msg({ 29 + message: `${count}M`, 30 + comment: 31 + 'For post statistics. Indicates a number in the millions. Please use the shortest format appropriate for your language.', 32 + }), 33 + ) 34 + } else if (isOver1k) { 35 + return i18n._( 36 + msg({ 37 + message: `${count}K`, 38 + comment: 39 + 'For post statistics. Indicates a number in the thousands. Please use the shortest format appropriate for your language.', 40 + }), 41 + ) 42 + } else { 43 + return count 44 + } 45 + }, 46 + [i18n], 47 + ) 24 48 }
+1 -1
src/components/StarterPack/ProfileStarterPacks.tsx
··· 214 214 onError: e => { 215 215 logger.error('Failed to generate starter pack', {safeMessage: e}) 216 216 setIsGenerating(false) 217 - if (e.name === 'NOT_ENOUGH_FOLLOWERS') { 217 + if (e.message.includes('NOT_ENOUGH_FOLLOWERS')) { 218 218 followersDialogControl.open() 219 219 } else { 220 220 errorDialogControl.open()
+4 -2
src/components/ageAssurance/AgeRestrictedScreen.tsx
··· 18 18 children, 19 19 screenTitle, 20 20 infoText, 21 + rightHeaderSlot, 21 22 }: { 22 23 children: React.ReactNode 23 24 screenTitle?: string 24 25 infoText?: string 26 + rightHeaderSlot?: React.ReactNode 25 27 }) { 26 28 const {_} = useLingui() 27 29 const copy = useAgeAssuranceCopy() ··· 46 48 <Layout.Screen> 47 49 <Layout.Header.Outer> 48 50 <Layout.Header.BackButton /> 49 - <Layout.Header.Content> 51 + <Layout.Header.Content align="left"> 50 52 <Layout.Header.TitleText> 51 53 {screenTitle ?? <Trans>Unavailable</Trans>} 52 54 </Layout.Header.TitleText> 53 55 </Layout.Header.Content> 54 - <Layout.Header.Slot /> 56 + {rightHeaderSlot ?? <Layout.Header.Slot />} 55 57 </Layout.Header.Outer> 56 58 <Layout.Content> 57 59 <View style={[a.p_lg]}>
+5 -1
src/components/dialogs/nuxs/BookmarksAnnouncement.tsx
··· 117 117 }, 118 118 ]} 119 119 alt={_( 120 - msg`A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads "inventing a saturday that immediately follows monday".`, 120 + msg({ 121 + message: `A screenshot of a post with a new button next to the share button that allows you to save the post to your bookmarks. The post is from @jcsalterego.bsky.social and reads "inventing a saturday that immediately follows monday".`, 122 + comment: 123 + 'Contains a post that originally appeared in English. Consider translating the post text if it makes sense in your language, and noting that the post was translated from English.', 124 + }), 121 125 )} 122 126 /> 123 127 </View>
+4 -10
src/lib/custom-animations/CountWheel.tsx
··· 6 6 useReducedMotion, 7 7 withTiming, 8 8 } from 'react-native-reanimated' 9 - import {i18n} from '@lingui/core' 10 9 11 10 import {decideShouldRoll} from '#/lib/custom-animations/util' 12 11 import {s} from '#/lib/styles' 13 12 import {Text} from '#/view/com/util/text/Text' 14 13 import {atoms as a, useTheme} from '#/alf' 15 - import {formatPostStatCount} from '#/components/PostControls/util' 14 + import {useFormatPostStatCount} from '#/components/PostControls/util' 16 15 17 16 const animationConfig = { 18 17 duration: 400, ··· 92 91 big, 93 92 isLiked, 94 93 hasBeenToggled, 95 - compactCount, 96 94 }: { 97 95 likeCount: number 98 96 big?: boolean 99 97 isLiked: boolean 100 98 hasBeenToggled: boolean 101 - compactCount?: boolean 102 99 }) { 103 100 const t = useTheme() 104 101 const shouldAnimate = !useReducedMotion() && hasBeenToggled ··· 111 108 const [key, setKey] = React.useState(0) 112 109 const [prevCount, setPrevCount] = React.useState(likeCount) 113 110 const prevIsLiked = React.useRef(isLiked) 114 - const formattedCount = formatPostStatCount(i18n, likeCount, { 115 - compact: compactCount, 116 - }) 117 - const formattedPrevCount = formatPostStatCount(i18n, prevCount, { 118 - compact: compactCount, 119 - }) 111 + const formatPostStatCount = useFormatPostStatCount() 112 + const formattedCount = formatPostStatCount(likeCount) 113 + const formattedPrevCount = formatPostStatCount(prevCount) 120 114 121 115 React.useEffect(() => { 122 116 if (isLiked === prevIsLiked.current) {
+4 -4
src/lib/custom-animations/CountWheel.web.tsx
··· 1 1 import React from 'react' 2 2 import {View} from 'react-native' 3 3 import {useReducedMotion} from 'react-native-reanimated' 4 - import {i18n} from '@lingui/core' 5 4 6 5 import {decideShouldRoll} from '#/lib/custom-animations/util' 7 6 import {s} from '#/lib/styles' 8 - import {formatCount} from '#/view/com/util/numeric/format' 9 7 import {Text} from '#/view/com/util/text/Text' 10 8 import {atoms as a, useTheme} from '#/alf' 9 + import {useFormatPostStatCount} from '#/components/PostControls/util' 11 10 12 11 const animationConfig = { 13 12 duration: 400, ··· 55 54 56 55 const [prevCount, setPrevCount] = React.useState(likeCount) 57 56 const prevIsLiked = React.useRef(isLiked) 58 - const formattedCount = formatCount(i18n, likeCount) 59 - const formattedPrevCount = formatCount(i18n, prevCount) 57 + const formatPostStatCount = useFormatPostStatCount() 58 + const formattedCount = formatPostStatCount(likeCount) 59 + const formattedPrevCount = formatPostStatCount(prevCount) 60 60 61 61 React.useEffect(() => { 62 62 if (isLiked === prevIsLiked.current) {
+2 -1
src/lib/notifications/notifications.ts
··· 11 11 import {useAgeAssuranceContext} from '#/state/ageAssurance' 12 12 import {type SessionAccount, useAgent, useSession} from '#/state/session' 13 13 import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 14 + import {IS_DEV} from '#/env' 14 15 15 16 /** 16 17 * @private ··· 129 130 }: { 130 131 isAgeRestricted?: boolean 131 132 } = {}) => { 132 - if (!isNative) return 133 + if (!isNative || IS_DEV) return 133 134 134 135 /** 135 136 * This will also fire the listener added via `addPushTokenListener`. That
+12 -1
src/screens/Messages/ChatList.tsx
··· 74 74 return ( 75 75 <AgeRestrictedScreen 76 76 screenTitle={_(msg`Chats`)} 77 - infoText={aaCopy.chatsInfoText}> 77 + infoText={aaCopy.chatsInfoText} 78 + rightHeaderSlot={ 79 + <Link 80 + to="/messages/settings" 81 + label={_(msg`Chat settings`)} 82 + size="small" 83 + color="secondary"> 84 + <ButtonText> 85 + <Trans>Chat settings</Trans> 86 + </ButtonText> 87 + </Link> 88 + }> 78 89 <MessagesScreenInner {...props} /> 79 90 </AgeRestrictedScreen> 80 91 )
+1 -12
src/screens/Messages/Settings.tsx
··· 12 12 import * as Toast from '#/view/com/util/Toast' 13 13 import {atoms as a} from '#/alf' 14 14 import {Admonition} from '#/components/Admonition' 15 - import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' 16 - import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' 17 15 import {Divider} from '#/components/Divider' 18 16 import * as Toggle from '#/components/forms/Toggle' 19 17 import * as Layout from '#/components/Layout' ··· 25 23 type Props = NativeStackScreenProps<CommonNavigatorParams, 'MessagesSettings'> 26 24 27 25 export function MessagesSettingsScreen(props: Props) { 28 - const {_} = useLingui() 29 - const aaCopy = useAgeAssuranceCopy() 30 - 31 - return ( 32 - <AgeRestrictedScreen 33 - screenTitle={_(msg`Chat Settings`)} 34 - infoText={aaCopy.chatsInfoText}> 35 - <MessagesSettingsScreenInner {...props} /> 36 - </AgeRestrictedScreen> 37 - ) 26 + return <MessagesSettingsScreenInner {...props} /> 38 27 } 39 28 40 29 export function MessagesSettingsScreenInner({}: Props) {
+7 -6
src/screens/PostThread/components/ThreadItemAnchor.tsx
··· 52 52 import {type AppModerationCause} from '#/components/Pills' 53 53 import {Embed, PostEmbedViewContext} from '#/components/Post/Embed' 54 54 import {PostControls} from '#/components/PostControls' 55 - import {formatPostStatCount} from '#/components/PostControls/util' 55 + import {useFormatPostStatCount} from '#/components/PostControls/util' 56 56 import {ProfileHoverCard} from '#/components/ProfileHoverCard' 57 57 import * as Prompt from '#/components/Prompt' 58 58 import {RichText} from '#/components/RichText' ··· 176 176 postSource?: PostSource 177 177 }) { 178 178 const t = useTheme() 179 - const {_, i18n} = useLingui() 179 + const {_} = useLingui() 180 180 const {openComposer} = useOpenComposer() 181 181 const {currentAccount, hasSession} = useSession() 182 182 const {gtTablet} = useBreakpoints() 183 183 const feedFeedback = useFeedFeedback(postSource?.feedSourceInfo, hasSession) 184 + const formatPostStatCount = useFormatPostStatCount() 184 185 185 186 const post = postShadow 186 187 const record = item.value.post.record ··· 439 440 testID="repostCount-expanded" 440 441 style={[a.text_md, t.atoms.text_contrast_medium]}> 441 442 <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 442 - {formatPostStatCount(i18n, post.repostCount)} 443 + {formatPostStatCount(post.repostCount)} 443 444 </Text>{' '} 444 445 <Plural 445 446 value={post.repostCount} ··· 457 458 testID="quoteCount-expanded" 458 459 style={[a.text_md, t.atoms.text_contrast_medium]}> 459 460 <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 460 - {formatPostStatCount(i18n, post.quoteCount)} 461 + {formatPostStatCount(post.quoteCount)} 461 462 </Text>{' '} 462 463 <Plural 463 464 value={post.quoteCount} ··· 473 474 testID="likeCount-expanded" 474 475 style={[a.text_md, t.atoms.text_contrast_medium]}> 475 476 <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 476 - {formatPostStatCount(i18n, post.likeCount)} 477 + {formatPostStatCount(post.likeCount)} 477 478 </Text>{' '} 478 479 <Plural value={post.likeCount} one="like" other="likes" /> 479 480 </Text> ··· 485 486 testID="bookmarkCount-expanded" 486 487 style={[a.text_md, t.atoms.text_contrast_medium]}> 487 488 <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 488 - {formatPostStatCount(i18n, post.bookmarkCount)} 489 + {formatPostStatCount(post.bookmarkCount)} 489 490 </Text>{' '} 490 491 <Plural 491 492 value={post.bookmarkCount}