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

chore: sync with upstream (#58)

* Send inferrable interactions to third-party feeds (#9094)

* Fix link crash (#9102)

* fix link crash

* fix link crash

* Log OTA errors properly (#9101)

* Log OTA errors properly

* filter out network errors

* don't send some "activity no longer available" errors (#9100)

* remove root sibling library (#9097)

* Catch errors on geolocation request, reduce Sentry logs (#9098)

* Nightly source-language update

* fix gap on profile (#9081)

* Update admonition component (#9068)

* update admonition component

* fix linting, adominition alighment

* design tweak

* edge cases for admonition, update storybook

* Update src/components/Admonition.tsx

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* fix mobile version

* change button style

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>

* Button tweaks (#9106)

* Tweak button colors

* Semi bold only for tiny buttons

* [Web] Fix thread jumps (#9111)

* [Web] Fix thread jumps

* Comment formatting

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Add StackedButton component (#9086)

* Nightly source-language update

* Fix feedfeedback metrics not distinguishing which feed it's from (#9099)

* fix feedfeedback metrics being sent for all feeds

* remove `discover:` metrics

* add patent pledge link to readme (#9118)

* Language selection and suggestion UX improvements (#9067)

* feat: don't retain accepted language suggestion after finishing or exiting post (#8886)

* feat: don't retain accepted language suggestion after finishing or exiting post

* fix: rebase fixes

* fix: rebase fixes

* chore: lint

* Rename onChange for clarity

* Improve logic in composer

* Handle user override more explicitly

* Drill in onSelectLanguage callback into dialog too

* Fix typo

Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Make text crystal clear

* Handle multiple languages

---------

Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

* Tighten up eslint rule to catch unused `const {_} = useLingui()` (#9122)

* tighten eslint config to catch unused `useLingui`s

* fix now-invalid uses of _

* remove unused function that produces a ton of eslint warnings

* upgrade react compiler plugin

* [Fix Logouts] Remove buggy hackfix (#9108)

* Rip out the network hack in favor of bluesky-social/atproto#4238

* Bump api pkg

* Debug code

* Revert "Debug code"

This reverts commit 38445f31f4d7bc6c360a18080a059040bd6b07d1.

---------

Co-authored-by: Eric Bailey <git@esb.lol>

* Nightly source-language update

* fix: missed alf reversion

---------

Co-authored-by: dan <dan.abramov@gmail.com>
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
Co-authored-by: pfrazee <1270099+pfrazee@users.noreply.github.com>
Co-authored-by: Chenyu <10610892+BinaryFiddler@users.noreply.github.com>
Co-authored-by: Eric Bailey <git@esb.lol>
Co-authored-by: Daniel Holmgren <dtholmgren@gmail.com>
Co-authored-by: Elijah Seed-Arita <elijaharita@gmail.com>
Co-authored-by: surfdude29 <149612116+surfdude29@users.noreply.github.com>

+1 -1
.eslintrc.js
··· 89 89 'no-unused-vars': 'off', 90 90 '@typescript-eslint/no-unused-vars': [ 91 91 'error', 92 - {argsIgnorePattern: '^_', varsIgnorePattern: '^_'}, 92 + {argsIgnorePattern: '^_', varsIgnorePattern: '^_.+'}, 93 93 ], 94 94 '@typescript-eslint/consistent-type-imports': [ 95 95 'warn',
+2
README.md
··· 76 76 77 77 See [./LICENSE](./LICENSE) for the full license. 78 78 79 + Bluesky Social PBC has committed to a software patent non-aggression pledge. For details see [the original announcement](https://bsky.social/about/blog/10-01-2025-patent-pledge). 80 + 79 81 ## P.S. 80 82 81 83 We ❤️ you and all of the ways you support us. Thank you for making Bluesky a great place!
+3 -3
package.json
··· 71 71 "icons:optimize": "svgo -f ./assets/icons" 72 72 }, 73 73 "dependencies": { 74 - "@atproto/api": "^0.16.7", 74 + "@atproto/api": "^0.17.0", 75 75 "@bitdrift/react-native": "^0.6.8", 76 76 "@braintree/sanitize-url": "^6.0.2", 77 77 "@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet", ··· 245 245 "babel-jest": "^29.7.0", 246 246 "babel-plugin-macros": "^3.1.0", 247 247 "babel-plugin-module-resolver": "^5.0.2", 248 - "babel-plugin-react-compiler": "^19.1.0-rc.1", 248 + "babel-plugin-react-compiler": "^19.1.0-rc.3", 249 249 "babel-preset-expo": "~54.0.0", 250 250 "eslint": "^8.19.0", 251 251 "eslint-plugin-bsky-internal": "link:./eslint", ··· 253 253 "eslint-plugin-import": "^2.31.0", 254 254 "eslint-plugin-lingui": "^0.2.0", 255 255 "eslint-plugin-react": "^7.33.2", 256 - "eslint-plugin-react-compiler": "^19.1.0-rc.1", 256 + "eslint-plugin-react-compiler": "^19.1.0-rc.2", 257 257 "eslint-plugin-react-native-a11y": "^3.3.0", 258 258 "eslint-plugin-simple-import-sort": "^12.0.0", 259 259 "file-loader": "6.2.0",
+1 -1
src/components/Button.tsx
··· 731 731 } else if (size === 'small') { 732 732 baseStyles.push(a.text_sm, a.leading_snug, a.font_medium) 733 733 } else if (size === 'tiny') { 734 - baseStyles.push(a.text_xs, a.leading_snug, a.font_semi_bold) 734 + baseStyles.push(a.text_xs, a.leading_snug, a.font_bold) 735 735 } 736 736 737 737 return StyleSheet.flatten(baseStyles)
-1
src/components/PostControls/ShareMenu/RecentChats.tsx
··· 24 24 25 25 export function RecentChats({postUri}: {postUri: string}) { 26 26 const control = useDialogContext() 27 - const {_} = useLingui() 28 27 const {currentAccount} = useSession() 29 28 const {data} = useListConvosQuery({status: 'accepted'}) 30 29 const convos = data?.pages[0]?.convos?.slice(0, 10)
-1
src/components/dialogs/StarterPackDialog.tsx
··· 47 47 targetDid, 48 48 enabled, 49 49 }: StarterPackDialogProps) { 50 - const {_} = useLingui() 51 50 const navigation = useNavigation<NavigationProp>() 52 51 const requireEmailVerification = useRequireEmailVerification() 53 52
+1 -1
src/lib/hooks/useIntentHandler.ts
··· 51 51 } 52 52 53 53 const urlp = new URL(url) 54 - const [_, intent, intentType] = urlp.pathname.split('/') 54 + const [__, intent, intentType] = urlp.pathname.split('/') 55 55 56 56 // On native, our links look like bluesky://intent/SomeIntent, so we have to check the hostname for the 57 57 // intent check. On web, we have to check the first part of the path since we have an actual hostname
+11 -11
src/lib/strings/embed-player.ts
··· 105 105 urlp.hostname === 'm.youtube.com' || 106 106 urlp.hostname === 'music.youtube.com' 107 107 ) { 108 - const [_, page, shortOrLiveVideoId] = urlp.pathname.split('/') 108 + const [__, page, shortOrLiveVideoId] = urlp.pathname.split('/') 109 109 110 110 const isShorts = page === 'shorts' 111 111 const isLive = page === 'live' ··· 137 137 window.location.hostname 138 138 : 'localhost' 139 139 140 - const [_, channelOrVideo, clipOrId, id] = urlp.pathname.split('/') 140 + const [__, channelOrVideo, clipOrId, id] = urlp.pathname.split('/') 141 141 142 142 if (channelOrVideo === 'videos') { 143 143 return { ··· 162 162 163 163 // spotify 164 164 if (urlp.hostname === 'open.spotify.com') { 165 - const [_, typeOrLocale, idOrType, id] = urlp.pathname.split('/') 165 + const [__, typeOrLocale, idOrType, id] = urlp.pathname.split('/') 166 166 167 167 if (idOrType) { 168 168 if (typeOrLocale === 'playlist' || idOrType === 'playlist') { ··· 210 210 urlp.hostname === 'soundcloud.com' || 211 211 urlp.hostname === 'www.soundcloud.com' 212 212 ) { 213 - const [_, user, trackOrSets, set] = urlp.pathname.split('/') 213 + const [__, user, trackOrSets, set] = urlp.pathname.split('/') 214 214 215 215 if (user && trackOrSets) { 216 216 if (trackOrSets === 'sets' && set) { ··· 270 270 } 271 271 272 272 if (urlp.hostname === 'vimeo.com' || urlp.hostname === 'www.vimeo.com') { 273 - const [_, videoId] = urlp.pathname.split('/') 273 + const [__, videoId] = urlp.pathname.split('/') 274 274 if (videoId) { 275 275 return { 276 276 type: 'vimeo_video', ··· 281 281 } 282 282 283 283 if (urlp.hostname === 'giphy.com' || urlp.hostname === 'www.giphy.com') { 284 - const [_, gifs, nameAndId] = urlp.pathname.split('/') 284 + const [__, gifs, nameAndId] = urlp.pathname.split('/') 285 285 286 286 /* 287 287 * nameAndId is a string that consists of the name (dash separated) and the id of the gif (the last part of the name) ··· 309 309 // These can include (presumably) a tracking id in the path name, so we have to check for that as well 310 310 if (giphyRegex.test(urlp.hostname)) { 311 311 // We can link directly to the gif, if its a proper link 312 - const [_, media, trackingOrId, idOrFilename, filename] = 312 + const [__, media, trackingOrId, idOrFilename, filename] = 313 313 urlp.pathname.split('/') 314 314 315 315 if (media === 'media') { ··· 338 338 // Finally, we should see if it is a link to i.giphy.com. These links don't necessarily end in .gif but can also 339 339 // be .webp 340 340 if (urlp.hostname === 'i.giphy.com' || urlp.hostname === 'www.i.giphy.com') { 341 - const [_, mediaOrFilename, filename] = urlp.pathname.split('/') 341 + const [__, mediaOrFilename, filename] = urlp.pathname.split('/') 342 342 343 343 if (mediaOrFilename === 'media' && filename) { 344 344 const gifId = filename.split('.')[0] ··· 389 389 const path_components = urlp.pathname.slice(1, i + 1).split('/') 390 390 if (path_components.length === 4) { 391 391 // discard username - it's not relevant 392 - const [photos, _, albums, id] = path_components 392 + const [photos, __, albums, id] = path_components 393 393 if (photos === 'photos' && albums === 'albums') { 394 394 // this at least has the shape of a valid photo-album URL! 395 395 return { ··· 417 417 // link shortened flickr path 418 418 if (urlp.hostname === 'flic.kr') { 419 419 const b58alph = '123456789abcdefghijkmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' 420 - let [_, type, idBase58Enc] = urlp.pathname.split('/') 420 + let [__, type, idBase58Enc] = urlp.pathname.split('/') 421 421 let id = 0n 422 422 for (const char of idBase58Enc) { 423 423 const nextIdx = b58alph.indexOf(char) ··· 528 528 return {success: false} 529 529 } 530 530 531 - let [_, id, filename] = urlp.pathname.split('/') 531 + let [__, id, filename] = urlp.pathname.split('/') 532 532 533 533 if (!id || !filename) { 534 534 return {success: false}
-17
src/lib/strings/helpers.ts
··· 62 62 }, [splitter, maxCount, text]) 63 63 } 64 64 65 - // https://stackoverflow.com/a/52171480 66 - export function toHashCode(str: string, seed = 0): number { 67 - let h1 = 0xdeadbeef ^ seed, 68 - h2 = 0x41c6ce57 ^ seed 69 - for (let i = 0, ch; i < str.length; i++) { 70 - ch = str.charCodeAt(i) 71 - h1 = Math.imul(h1 ^ ch, 2654435761) 72 - h2 = Math.imul(h2 ^ ch, 1597334677) 73 - } 74 - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) 75 - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909) 76 - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) 77 - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909) 78 - 79 - return 4294967296 * (2097151 & h2) + (h1 >>> 0) 80 - } 81 - 82 65 export function countLines(str: string | undefined): number { 83 66 if (!str) return 0 84 67 return str.match(/\n/g)?.length ?? 0
+1 -1
src/lib/strings/starter-pack.ts
··· 46 46 } else { 47 47 const url = new URL(uri) 48 48 const parts = url.pathname.split('/') 49 - const [_, path, name, rkey] = parts 49 + const [__, path, name, rkey] = parts 50 50 51 51 if (parts.length !== 4) return null 52 52 if (path !== 'starter-pack' && path !== 'start') return null
+10 -5
src/logger/metrics.ts
··· 175 175 'feed:suggestion:press': { 176 176 feedUrl: string 177 177 } 178 - 'discover:showMore': { 178 + 'feed:showMore': { 179 + feed: string 179 180 feedContext: string 180 181 } 181 - 'discover:showLess': { 182 + 'feed:showLess': { 183 + feed: string 182 184 feedContext: string 183 185 } 184 - 'discover:clickthrough': { 186 + 'feed:clickthrough': { 187 + feed: string 185 188 count: number 186 189 } 187 - 'discover:engaged': { 190 + 'feed:engaged': { 191 + feed: string 188 192 count: number 189 193 } 190 - 'discover:seen': { 194 + 'feed:seen': { 195 + feed: string 191 196 count: number 192 197 } 193 198
-2
src/screens/Onboarding/StepFinished.tsx
··· 69 69 import * as bsky from '#/types/bsky' 70 70 71 71 export function StepFinished() { 72 - const {_} = useLingui() 73 72 const {state, dispatch} = useContext(Context) 74 73 const onboardDispatch = useOnboardingDispatch() 75 74 const [saving, setSaving] = useState(false) ··· 495 494 496 495 function Dot({active}: {active: boolean}) { 497 496 const t = useTheme() 498 - const {_} = useLingui() 499 497 500 498 return ( 501 499 <View
-1
src/screens/Search/Shell.tsx
··· 428 428 const {hasSession} = useSession() 429 429 const {gtTablet} = useBreakpoints() 430 430 const [activeTab, setActiveTab] = useState(0) 431 - const {_} = useLingui() 432 431 433 432 const onPageSelected = useCallback( 434 433 (index: number) => {
-1
src/screens/Search/modules/ExploreTrendingVideos.tsx
··· 31 31 } 32 32 33 33 export function ExploreTrendingVideos() { 34 - const {_} = useLingui() 35 34 const gutters = useGutters([0, 'base']) 36 35 const {data, isLoading, error} = usePostFeedQuery(FEED_DESC, FEED_PARAMS) 37 36
+26 -12
src/state/feed-feedback.tsx
··· 12 12 13 13 import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants' 14 14 import {isNetworkError} from '#/lib/hooks/useCleanError' 15 - import {logEvent} from '#/lib/statsig/statsig' 16 15 import {Logger} from '#/logger' 17 16 import { 18 17 type FeedSourceFeedInfo, ··· 90 89 const aggregatedStats = useRef<AggregatedStats | null>(null) 91 90 const throttledFlushAggregatedStats = useMemo( 92 91 () => 93 - throttle(() => flushToStatsig(aggregatedStats.current), 45e3, { 94 - leading: true, // The outer call is already throttled somewhat. 95 - trailing: true, 96 - }), 97 - [], 92 + throttle( 93 + () => 94 + flushToStatsig( 95 + aggregatedStats.current, 96 + feed?.feedDescriptor ?? 'unknown', 97 + ), 98 + 45e3, 99 + { 100 + leading: true, // The outer call is already throttled somewhat. 101 + trailing: true, 102 + }, 103 + ), 104 + [feed?.feedDescriptor], 98 105 ) 99 106 100 107 const sendToFeedNoDelay = useCallback(() => { ··· 135 142 sendOrAggregateInteractionsForStats( 136 143 aggregatedStats.current, 137 144 interactionsToSend, 145 + feed?.feedDescriptor ?? 'unknown', 138 146 ) 139 147 throttledFlushAggregatedStats() 140 148 logger.debug('flushed') ··· 271 279 function sendOrAggregateInteractionsForStats( 272 280 stats: AggregatedStats, 273 281 interactions: AppBskyFeedDefs.Interaction[], 282 + feed: string, 274 283 ) { 275 284 for (let interaction of interactions) { 276 285 switch (interaction.event) { 277 286 // Pressing "Show more" / "Show less" is relatively uncommon so we won't aggregate them. 278 287 // This lets us send the feed context together with them. 279 288 case 'app.bsky.feed.defs#requestLess': { 280 - logEvent('discover:showLess', { 289 + logger.metric('feed:showLess', { 290 + feed, 281 291 feedContext: interaction.feedContext ?? '', 282 292 }) 283 293 break 284 294 } 285 295 case 'app.bsky.feed.defs#requestMore': { 286 - logEvent('discover:showMore', { 296 + logger.metric('feed:showMore', { 297 + feed, 287 298 feedContext: interaction.feedContext ?? '', 288 299 }) 289 300 break ··· 313 324 } 314 325 } 315 326 316 - function flushToStatsig(stats: AggregatedStats | null) { 327 + function flushToStatsig(stats: AggregatedStats | null, feedDescriptor: string) { 317 328 if (stats === null) { 318 329 return 319 330 } 320 331 321 332 if (stats.clickthroughCount > 0) { 322 - logEvent('discover:clickthrough', { 333 + logger.metric('feed:clickthrough', { 323 334 count: stats.clickthroughCount, 335 + feed: feedDescriptor, 324 336 }) 325 337 stats.clickthroughCount = 0 326 338 } 327 339 328 340 if (stats.engagedCount > 0) { 329 - logEvent('discover:engaged', { 341 + logger.metric('feed:engaged', { 330 342 count: stats.engagedCount, 343 + feed: feedDescriptor, 331 344 }) 332 345 stats.engagedCount = 0 333 346 } 334 347 335 348 if (stats.seenCount > 0) { 336 - logEvent('discover:seen', { 349 + logger.metric('feed:seen', { 337 350 count: stats.seenCount, 351 + feed: feedDescriptor, 338 352 }) 339 353 stats.seenCount = 0 340 354 }
+1 -1
src/state/persisted/schema.ts
··· 71 71 contentLanguages: z.array(z.string()), 72 72 /** 73 73 * The language(s) the user is currently posting in, configured within the 74 - * composer. Multiple languages are psearate by commas. 74 + * composer. Multiple languages are separated by commas. 75 75 * 76 76 * BCP-47 2-letter language code without region. 77 77 */
+4
src/state/preferences/languages.tsx
··· 156 156 return postLanguage.split(',').filter(Boolean) 157 157 } 158 158 159 + export function fromPostLanguages(languages: string[]): string { 160 + return languages.filter(Boolean).join(',') 161 + } 162 + 159 163 export function hasPostLanguage(postLanguage: string, code2: string): boolean { 160 164 return toPostLanguages(postLanguage).includes(code2) 161 165 }
+5 -5
src/state/queries/post-feed.ts
··· 492 492 } 493 493 } 494 494 } else if (feedDesc.startsWith('author')) { 495 - const [_, actor, filter] = feedDesc.split('|') 495 + const [__, actor, filter] = feedDesc.split('|') 496 496 return new AuthorFeedAPI({agent, feedParams: {actor, filter}}) 497 497 } else if (feedDesc.startsWith('likes')) { 498 - const [_, actor] = feedDesc.split('|') 498 + const [__, actor] = feedDesc.split('|') 499 499 return new LikesFeedAPI({agent, feedParams: {actor}}) 500 500 } else if (feedDesc.startsWith('feedgen')) { 501 - const [_, feed] = feedDesc.split('|') 501 + const [__, feed] = feedDesc.split('|') 502 502 return new CustomFeedAPI({ 503 503 agent, 504 504 feedParams: {feed}, 505 505 userInterests, 506 506 }) 507 507 } else if (feedDesc.startsWith('list')) { 508 - const [_, list] = feedDesc.split('|') 508 + const [__, list] = feedDesc.split('|') 509 509 return new ListFeedAPI({agent, feedParams: {list}}) 510 510 } else if (feedDesc.startsWith('posts')) { 511 - const [_, uriList] = feedDesc.split('|') 511 + const [__, uriList] = feedDesc.split('|') 512 512 return new PostListFeedAPI({agent, feedParams: {uris: uriList.split(',')}}) 513 513 } else if (feedDesc === 'demo') { 514 514 return new DemoFeedAPI({agent})
+1 -1
src/state/queries/trending/useGetSuggestedUsersQuery.ts
··· 69 69 queryClient.getQueriesData<AppBskyUnspeccedGetSuggestedUsers.OutputSchema>({ 70 70 queryKey: [getSuggestedUsersQueryKeyRoot], 71 71 }) 72 - for (const [_, response] of responses) { 72 + for (const [_key, response] of responses) { 73 73 if (!response) { 74 74 continue 75 75 }
-8
src/state/session/agent.ts
··· 321 321 322 322 // Now the agent is ready. 323 323 const account = agentToSessionAccountOrThrow(this) 324 - let lastSession = this.sessionManager.session 325 324 this.persistSessionHandler = event => { 326 - if (this.sessionManager.session) { 327 - lastSession = this.sessionManager.session 328 - } else if (event === 'network-error') { 329 - // Put it back, we'll try again later. 330 - this.sessionManager.session = lastSession 331 - } 332 - 333 325 onSessionChange(this, account.did, event) 334 326 if (event !== 'create' && event !== 'update') { 335 327 addSessionErrorLog(account.did, event)
+55 -6
src/view/com/composer/Composer.tsx
··· 87 87 import {useModalControls} from '#/state/modals' 88 88 import {useRequireAltTextEnabled} from '#/state/preferences' 89 89 import { 90 + fromPostLanguages, 90 91 toPostLanguages, 91 92 useLanguagePrefs, 92 93 useLanguagePrefsApi, ··· 197 198 const [publishingStage, setPublishingStage] = useState('') 198 199 const [error, setError] = useState('') 199 200 201 + /** 202 + * A temporary local reference to a language suggestion that the user has 203 + * accepted. This overrides the global post language preference, but is not 204 + * stored permanently. 205 + */ 206 + const [acceptedLanguageSuggestion, setAcceptedLanguageSuggestion] = useState< 207 + string | null 208 + >(null) 209 + 210 + /** 211 + * The language(s) of the post being replied to. 212 + */ 213 + const [replyToLanguages, setReplyToLanguages] = useState<string[]>( 214 + replyTo?.langs || [], 215 + ) 216 + 217 + /** 218 + * The currently selected languages of the post. Prefer local temporary 219 + * language suggestion over global lang prefs, if available. 220 + */ 221 + const currentLanguages = useMemo( 222 + () => 223 + acceptedLanguageSuggestion 224 + ? [acceptedLanguageSuggestion] 225 + : toPostLanguages(langPrefs.postLanguage), 226 + [acceptedLanguageSuggestion, langPrefs.postLanguage], 227 + ) 228 + 229 + /** 230 + * When the user selects a language from the composer language selector, 231 + * clear any temporary language suggestions they may have selected 232 + * previously, and any we might try to suggest to them. 233 + */ 234 + const onSelectLanguage = () => { 235 + setAcceptedLanguageSuggestion(null) 236 + setReplyToLanguages([]) 237 + } 238 + 200 239 const [composerState, composerDispatch] = useReducer( 201 240 composerReducer, 202 241 { ··· 414 453 thread, 415 454 replyTo: replyTo?.uri, 416 455 onStateChange: setPublishingStage, 417 - langs: toPostLanguages(langPrefs.postLanguage), 456 + langs: currentLanguages, 418 457 }) 419 458 ).uris[0] 420 459 ··· 490 529 isPartOfThread: thread.posts.length > 1, 491 530 hasLink: !!post.embed.link, 492 531 hasQuote: !!post.embed.quote, 493 - langs: langPrefs.postLanguage, 532 + langs: fromPostLanguages(currentLanguages), 494 533 logContext: 'Composer', 495 534 }) 496 535 index++ ··· 557 596 thread, 558 597 canPost, 559 598 isPublishing, 560 - langPrefs.postLanguage, 599 + currentLanguages, 561 600 onClose, 562 601 onPost, 563 602 onPostSuccess, ··· 654 693 <> 655 694 <SuggestedLanguage 656 695 text={activePost.richtext.text} 657 - // NOTE(@elijaharita): currently just choosing the first language if any exists 658 - replyToLanguage={replyTo?.langs?.[0]} 696 + replyToLanguages={replyToLanguages} 697 + currentLanguages={currentLanguages} 698 + onAcceptSuggestedLanguage={setAcceptedLanguageSuggestion} 659 699 /> 660 700 <ComposerPills 661 701 isReply={!!replyTo} ··· 678 718 type: 'add_post', 679 719 }) 680 720 }} 721 + currentLanguages={currentLanguages} 722 + onSelectLanguage={onSelectLanguage} 681 723 /> 682 724 </> 683 725 ) ··· 1314 1356 onEmojiButtonPress, 1315 1357 onSelectVideo, 1316 1358 onAddPost, 1359 + currentLanguages, 1360 + onSelectLanguage, 1317 1361 }: { 1318 1362 post: PostDraft 1319 1363 dispatch: (action: PostAction) => void ··· 1322 1366 onError: (error: string) => void 1323 1367 onSelectVideo: (postId: string, asset: ImagePickerAsset) => void 1324 1368 onAddPost: () => void 1369 + currentLanguages: string[] 1370 + onSelectLanguage?: (language: string) => void 1325 1371 }) { 1326 1372 const t = useTheme() 1327 1373 const {_} = useLingui() ··· 1475 1521 <PlusIcon size="lg" /> 1476 1522 </Button> 1477 1523 )} 1478 - <PostLanguageSelect /> 1524 + <PostLanguageSelect 1525 + currentLanguages={currentLanguages} 1526 + onSelectLanguage={onSelectLanguage} 1527 + /> 1479 1528 <CharProgress 1480 1529 count={post.shortenedGraphemeLength} 1481 1530 style={{width: 65}}
-1
src/view/com/composer/photos/EditImageDialog.web.tsx
··· 116 116 }) { 117 117 const t = useTheme() 118 118 const [isDragging, setIsDragging] = useState(false) 119 - const {_} = useLingui() 120 119 const control = Dialog.useDialogContext() 121 120 122 121 const source = image.source
+35 -9
src/view/com/composer/select-language/PostLanguageSelect.tsx
··· 17 17 import {Text} from '#/components/Typography' 18 18 import {PostLanguageSelectDialog} from './PostLanguageSelectDialog' 19 19 20 - export function PostLanguageSelect() { 20 + export function PostLanguageSelect({ 21 + currentLanguages: currentLanguagesProp, 22 + onSelectLanguage, 23 + }: { 24 + currentLanguages?: string[] 25 + onSelectLanguage?: (language: string) => void 26 + }) { 21 27 const {_} = useLingui() 22 28 const langPrefs = useLanguagePrefs() 23 29 const setLangPrefs = useLanguagePrefsApi() ··· 26 32 const dedupedHistory = Array.from( 27 33 new Set([...langPrefs.postLanguageHistory, langPrefs.postLanguage]), 28 34 ) 35 + 36 + const currentLanguages = 37 + currentLanguagesProp ?? toPostLanguages(langPrefs.postLanguage) 29 38 30 39 if ( 31 40 dedupedHistory.length === 1 && ··· 34 43 return ( 35 44 <> 36 45 <LanguageBtn onPress={languageDialogControl.open} /> 37 - <PostLanguageSelectDialog control={languageDialogControl} /> 46 + <PostLanguageSelectDialog 47 + control={languageDialogControl} 48 + currentLanguages={currentLanguages} 49 + /> 38 50 </> 39 51 ) 40 52 } ··· 43 55 <> 44 56 <Menu.Root> 45 57 <Menu.Trigger label={_(msg`Select post language`)}> 46 - {({props}) => <LanguageBtn {...props} />} 58 + {({props}) => ( 59 + <LanguageBtn currentLanguages={currentLanguages} {...props} /> 60 + )} 47 61 </Menu.Trigger> 48 62 <Menu.Outer> 49 63 <Menu.Group> ··· 56 70 <Menu.Item 57 71 key={historyItem} 58 72 label={_(msg`Select ${langName}`)} 59 - onPress={() => setLangPrefs.setPostLanguage(historyItem)}> 73 + onPress={() => { 74 + setLangPrefs.setPostLanguage(historyItem) 75 + onSelectLanguage?.(historyItem) 76 + }}> 60 77 <Menu.ItemText>{langName}</Menu.ItemText> 61 78 <Menu.ItemRadio 62 - selected={historyItem === langPrefs.postLanguage} 79 + selected={currentLanguages.includes(historyItem)} 63 80 /> 64 81 </Menu.Item> 65 82 ) ··· 77 94 </Menu.Outer> 78 95 </Menu.Root> 79 96 80 - <PostLanguageSelectDialog control={languageDialogControl} /> 97 + <PostLanguageSelectDialog 98 + control={languageDialogControl} 99 + currentLanguages={currentLanguages} 100 + onSelectLanguage={onSelectLanguage} 101 + /> 81 102 </> 82 103 ) 83 104 } 84 105 85 - function LanguageBtn(props: Omit<ButtonProps, 'label' | 'children'>) { 106 + function LanguageBtn( 107 + props: Omit<ButtonProps, 'label' | 'children'> & { 108 + currentLanguages?: string[] 109 + }, 110 + ) { 86 111 const {_} = useLingui() 87 112 const langPrefs = useLanguagePrefs() 88 113 const t = useTheme() 89 114 90 115 const postLanguagesPref = toPostLanguages(langPrefs.postLanguage) 116 + const currentLanguages = props.currentLanguages ?? postLanguagesPref 91 117 92 118 return ( 93 119 <Button ··· 106 132 {({pressed, hovered}) => { 107 133 const color = 108 134 pressed || hovered ? t.palette.primary_300 : t.palette.primary_500 109 - if (postLanguagesPref.length > 0) { 135 + if (currentLanguages.length > 0) { 110 136 return ( 111 137 <Text 112 138 style={[ ··· 117 143 {maxWidth: 100}, 118 144 ]} 119 145 numberOfLines={1}> 120 - {postLanguagesPref 146 + {currentLanguages 121 147 .map(lang => codeToLanguageName(lang, langPrefs.appLanguage)) 122 148 .join(', ')} 123 149 </Text>
+25 -3
src/view/com/composer/select-language/PostLanguageSelectDialog.tsx
··· 8 8 import {type Language, LANGUAGES, LANGUAGES_MAP_CODE2} from '#/locale/languages' 9 9 import {isNative, isWeb} from '#/platform/detection' 10 10 import { 11 + toPostLanguages, 11 12 useLanguagePrefs, 12 13 useLanguagePrefsApi, 13 14 } from '#/state/preferences/languages' ··· 23 24 24 25 export function PostLanguageSelectDialog({ 25 26 control, 27 + /** 28 + * Optionally can be passed to show different values than what is saved in 29 + * langPrefs. 30 + */ 31 + currentLanguages, 32 + onSelectLanguage, 26 33 }: { 27 34 control: Dialog.DialogControlProps 35 + currentLanguages?: string[] 36 + onSelectLanguage?: (language: string) => void 28 37 }) { 29 38 const {height} = useWindowDimensions() 30 39 const insets = useSafeAreaInsets() ··· 40 49 nativeOptions={{minHeight: height - insets.top}}> 41 50 <Dialog.Handle /> 42 51 <ErrorBoundary renderError={renderErrorBoundary}> 43 - <DialogInner /> 52 + <DialogInner 53 + currentLanguages={currentLanguages} 54 + onSelectLanguage={onSelectLanguage} 55 + /> 44 56 </ErrorBoundary> 45 57 </Dialog.Outer> 46 58 ) 47 59 } 48 60 49 - export function DialogInner() { 61 + export function DialogInner({ 62 + currentLanguages, 63 + onSelectLanguage, 64 + }: { 65 + currentLanguages?: string[] 66 + onSelectLanguage?: (language: string) => void 67 + }) { 50 68 const control = Dialog.useDialogContext() 51 69 const [headerHeight, setHeaderHeight] = useState(0) 52 70 ··· 63 81 }, []) 64 82 65 83 const langPrefs = useLanguagePrefs() 84 + const postLanguagesPref = 85 + currentLanguages ?? toPostLanguages(langPrefs.postLanguage) 86 + 66 87 const [checkedLanguagesCode2, setCheckedLanguagesCode2] = useState<string[]>( 67 - langPrefs.postLanguage.split(',') || [langPrefs.primaryLanguage], 88 + postLanguagesPref || [langPrefs.primaryLanguage], 68 89 ) 69 90 const [search, setSearch] = useState('') 70 91 ··· 79 100 langsString = langPrefs.primaryLanguage 80 101 } 81 102 setLangPrefs.setPostLanguage(langsString) 103 + onSelectLanguage?.(langsString) 82 104 }) 83 105 } 84 106
+121 -44
src/view/com/composer/select-language/SuggestedLanguage.tsx
··· 1 1 import {useEffect, useState} from 'react' 2 - import {View} from 'react-native' 2 + import {Text as RNText, View} from 'react-native' 3 3 import {parseLanguage} from '@atproto/api' 4 4 import {msg, Trans} from '@lingui/macro' 5 5 import {useLingui} from '@lingui/react' 6 6 import lande from 'lande' 7 7 8 8 import {code3ToCode2Strict, codeToLanguageName} from '#/locale/helpers' 9 - import { 10 - toPostLanguages, 11 - useLanguagePrefs, 12 - useLanguagePrefsApi, 13 - } from '#/state/preferences/languages' 9 + import {useLanguagePrefs} from '#/state/preferences/languages' 14 10 import {atoms as a, useTheme} from '#/alf' 15 11 import {Button, ButtonText} from '#/components/Button' 16 12 import {Earth_Stroke2_Corner2_Rounded as EarthIcon} from '#/components/icons/Globe' ··· 22 18 23 19 export function SuggestedLanguage({ 24 20 text, 25 - replyToLanguage: replyToLanguageProp, 21 + replyToLanguages: replyToLanguagesProp, 22 + currentLanguages, 23 + onAcceptSuggestedLanguage, 26 24 }: { 27 25 text: string 28 - replyToLanguage?: string 26 + /** 27 + * All languages associated with the post being replied to. 28 + */ 29 + replyToLanguages: string[] 30 + /** 31 + * All languages currently selected for the post being composed. 32 + */ 33 + currentLanguages: string[] 34 + /** 35 + * Called when the user accepts a suggested language. We only pass a single 36 + * language here. If the post being replied to has multiple languages, we 37 + * only suggest the first one. 38 + */ 39 + onAcceptSuggestedLanguage: (language: string | null) => void 29 40 }) { 30 - const replyToLanguage = cleanUpLanguage(replyToLanguageProp) 41 + const langPrefs = useLanguagePrefs() 42 + const replyToLanguages = replyToLanguagesProp 43 + .map(lang => cleanUpLanguage(lang)) 44 + .filter(Boolean) as string[] 45 + const [hasInteracted, setHasInteracted] = useState(false) 31 46 const [suggestedLanguage, setSuggestedLanguage] = useState< 32 47 string | undefined 33 - >(text.length === 0 ? replyToLanguage : undefined) 34 - const langPrefs = useLanguagePrefs() 35 - const setLangPrefs = useLanguagePrefsApi() 36 - const t = useTheme() 37 - const {_} = useLingui() 48 + >(undefined) 38 49 39 50 useEffect(() => { 40 - // For replies, suggest the language of the post being replied to if no text 41 - // has been typed yet 42 - if (replyToLanguage && text.length === 0) { 43 - setSuggestedLanguage(replyToLanguage) 44 - return 51 + if (text.length > 0 && !hasInteracted) { 52 + setHasInteracted(true) 45 53 } 54 + }, [text, hasInteracted]) 46 55 56 + useEffect(() => { 47 57 const textTrimmed = text.trim() 48 58 49 59 // Don't run the language model on small posts, the results are likely ··· 58 68 }) 59 69 60 70 return () => cancelIdle(idle) 61 - }, [text, replyToLanguage]) 71 + }, [text]) 72 + 73 + /* 74 + * We've detected a language, and the user hasn't already selected it. 75 + */ 76 + const hasLanguageSuggestion = 77 + suggestedLanguage && !currentLanguages.includes(suggestedLanguage) 78 + /* 79 + * We have not detected a different language, and the user is not already 80 + * using or has not already selected one of the languages of the post they 81 + * are replying to. 82 + */ 83 + const hasSuggestedReplyLanguage = 84 + !hasInteracted && 85 + !suggestedLanguage && 86 + replyToLanguages.length && 87 + !replyToLanguages.some(l => currentLanguages.includes(l)) 62 88 63 - if ( 64 - suggestedLanguage && 65 - !toPostLanguages(langPrefs.postLanguage).includes(suggestedLanguage) 66 - ) { 89 + if (hasLanguageSuggestion) { 67 90 const suggestedLanguageName = codeToLanguageName( 68 91 suggestedLanguage, 69 92 langPrefs.appLanguage, 70 93 ) 71 94 72 95 return ( 96 + <LanguageSuggestionButton 97 + label={ 98 + <RNText> 99 + <Trans> 100 + Are you writing in{' '} 101 + <Text style={[a.font_bold]}>{suggestedLanguageName}</Text>? 102 + </Trans> 103 + </RNText> 104 + } 105 + value={suggestedLanguage} 106 + onAccept={onAcceptSuggestedLanguage} 107 + /> 108 + ) 109 + } else if (hasSuggestedReplyLanguage) { 110 + const suggestedLanguageName = codeToLanguageName( 111 + replyToLanguages[0], 112 + langPrefs.appLanguage, 113 + ) 114 + 115 + return ( 116 + <LanguageSuggestionButton 117 + label={ 118 + <RNText> 119 + <Trans> 120 + The post you're replying to was marked as being written in{' '} 121 + {suggestedLanguageName} by its author. Would you like to reply in{' '} 122 + <Text style={[a.font_bold]}>{suggestedLanguageName}</Text>? 123 + </Trans> 124 + </RNText> 125 + } 126 + value={replyToLanguages[0]} 127 + onAccept={onAcceptSuggestedLanguage} 128 + /> 129 + ) 130 + } else { 131 + return null 132 + } 133 + } 134 + 135 + function LanguageSuggestionButton({ 136 + label, 137 + value, 138 + onAccept, 139 + }: { 140 + label: React.ReactNode 141 + value: string 142 + onAccept: (language: string | null) => void 143 + }) { 144 + const t = useTheme() 145 + const {_} = useLingui() 146 + 147 + return ( 148 + <View style={[a.px_lg, a.py_sm]}> 73 149 <View 74 150 style={[ 75 - t.atoms.border_contrast_low, 76 - a.gap_sm, 151 + a.gap_md, 77 152 a.border, 78 153 a.flex_row, 79 154 a.align_center, 80 155 a.rounded_sm, 81 - a.px_lg, 82 - a.py_md, 83 - a.mx_md, 84 - a.my_sm, 156 + a.p_md, 157 + a.pl_lg, 85 158 t.atoms.bg, 159 + t.atoms.border_contrast_low, 86 160 ]}> 87 161 <EarthIcon /> 88 - <Text style={[a.flex_1]}> 89 - <Trans> 90 - Are you writing in{' '} 91 - <Text style={[a.font_bold]}>{suggestedLanguageName}</Text>? 92 - </Trans> 93 - </Text> 162 + <View style={[a.flex_1]}> 163 + <Text 164 + style={[ 165 + a.flex_1, 166 + a.leading_snug, 167 + { 168 + maxWidth: 400, 169 + }, 170 + ]}> 171 + {label} 172 + </Text> 173 + </View> 94 174 95 175 <Button 96 - color="secondary" 97 176 size="small" 98 - variant="solid" 99 - onPress={() => setLangPrefs.setPostLanguage(suggestedLanguage)} 100 - label={_(msg`Change post language to ${suggestedLanguageName}`)}> 177 + color="secondary" 178 + onPress={() => onAccept(value)} 179 + label={_(msg`Accept this language suggestion`)}> 101 180 <ButtonText> 102 181 <Trans>Yes</Trans> 103 182 </ButtonText> 104 183 </Button> 105 184 </View> 106 - ) 107 - } else { 108 - return null 109 - } 185 + </View> 186 + ) 110 187 } 111 188 112 189 /**
+1 -1
src/view/com/composer/text-input/web/TagDecorator.ts
··· 30 30 31 31 let match 32 32 while ((match = regex.exec(textContent))) { 33 - const [matchedString, _, tag] = match 33 + const [matchedString, __, tag] = match 34 34 35 35 if (!tag || tag.replace(TRAILING_PUNCTUATION_REGEX, '').length > 64) 36 36 continue
+1 -1
src/view/com/posts/PostFeedErrorMessage.tsx
··· 128 128 })[knownError], 129 129 [_l, knownError], 130 130 ) 131 - const [_, uri] = feedDesc.split('|') 131 + const [__, uri] = feedDesc.split('|') 132 132 const [ownerDid] = safeParseFeedgenUri(uri) 133 133 const removePromptControl = Prompt.usePromptControl() 134 134 const {mutateAsync: removeFeed} = useRemoveFeedMutation()
-2
src/view/screens/ModerationMutedAccounts.tsx
··· 2 2 import {type StyleProp, View, type ViewStyle} from 'react-native' 3 3 import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' 4 4 import {Trans} from '@lingui/macro' 5 - import {useLingui} from '@lingui/react' 6 5 import {useFocusEffect} from '@react-navigation/native' 7 6 import {type NativeStackScreenProps} from '@react-navigation/native-stack' 8 7 ··· 27 26 export function ModerationMutedAccounts({}: Props) { 28 27 const t = useTheme() 29 28 const moderationOpts = useModerationOpts() 30 - const {_} = useLingui() 31 29 const setMinimalShellMode = useSetMinimalShellMode() 32 30 33 31 const [isPTRing, setIsPTRing] = useState(false)
-2
src/view/screens/ProfileFeedLikedBy.tsx
··· 1 1 import {useCallback} from 'react' 2 2 import {Trans} from '@lingui/macro' 3 - import {useLingui} from '@lingui/react' 4 3 import {useFocusEffect} from '@react-navigation/native' 5 4 6 5 import { ··· 17 16 const setMinimalShellMode = useSetMinimalShellMode() 18 17 const {name, rkey} = route.params 19 18 const uri = makeRecordUri(name, 'app.bsky.feed.generator', rkey) 20 - const {_} = useLingui() 21 19 22 20 useFocusEffect( 23 21 useCallback(() => {
+24
src/view/screens/Storybook/Buttons.tsx
··· 43 43 </StackedButton> 44 44 </View> 45 45 46 + <View style={[a.flex_row, a.gap_md, a.align_start, {maxWidth: 350}]}> 47 + <StackedButton 48 + label="stacked" 49 + icon={Globe} 50 + color="secondary" 51 + style={[a.flex_1]}> 52 + Bop it 53 + </StackedButton> 54 + <StackedButton 55 + label="stacked" 56 + icon={Globe} 57 + color="negative_subtle" 58 + style={[a.flex_1]}> 59 + Twist it 60 + </StackedButton> 61 + <StackedButton 62 + label="stacked" 63 + icon={Globe} 64 + color="primary" 65 + style={[a.flex_1]}> 66 + Pull it 67 + </StackedButton> 68 + </View> 69 + 46 70 {[ 47 71 'primary', 48 72 'secondary',
+48 -12
yarn.lock
··· 77 77 tlds "^1.234.0" 78 78 zod "^3.23.8" 79 79 80 + "@atproto/api@^0.17.0": 81 + version "0.17.0" 82 + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.17.0.tgz#1fe87ef703f8020dbe00bb5e5cc18622b8b91f4a" 83 + integrity sha512-FNS9SW7/3kslAnJH7F4fO9/jPjXzC0NMD6u9NjJ/h4EnaIEpWHZQPkmD9Q2hvAwD6+Uo2boYZEPKkOa55Lr5Dg== 84 + dependencies: 85 + "@atproto/common-web" "^0.4.3" 86 + "@atproto/lexicon" "^0.5.1" 87 + "@atproto/syntax" "^0.4.1" 88 + "@atproto/xrpc" "^0.7.5" 89 + await-lock "^2.2.2" 90 + multiformats "^9.9.0" 91 + tlds "^1.234.0" 92 + zod "^3.23.8" 93 + 80 94 "@atproto/aws@^0.2.28": 81 95 version "0.2.28" 82 96 resolved "https://registry.yarnpkg.com/@atproto/aws/-/aws-0.2.28.tgz#17bd88a6276e323ebb094a3f01bd94b1173a29a4" ··· 170 184 uint8arrays "3.0.0" 171 185 zod "^3.23.8" 172 186 187 + "@atproto/common-web@^0.4.3": 188 + version "0.4.3" 189 + resolved "https://registry.yarnpkg.com/@atproto/common-web/-/common-web-0.4.3.tgz#b4480220b5682db09da45f4ef906eb7619c838b5" 190 + integrity sha512-nRDINmSe4VycJzPo6fP/hEltBcULFxt9Kw7fQk6405FyAWZiTluYHlXOnU7GkQfeUK44OENG1qFTBcmCJ7e8pg== 191 + dependencies: 192 + graphemer "^1.4.0" 193 + multiformats "^9.9.0" 194 + uint8arrays "3.0.0" 195 + zod "^3.23.8" 196 + 173 197 "@atproto/common@0.1.0": 174 198 version "0.1.0" 175 199 resolved "https://registry.yarnpkg.com/@atproto/common/-/common-0.1.0.tgz#4216a8fef5b985ab62ac21252a0f8ca0f4a0f210" ··· 298 322 integrity sha512-3aAzEAy9EAPs3CxznzMhEcqDd7m3vz1eze/ya9/ThbB7yleqJIhz5GY2q76tCCwHPhn5qDDMhlA9kKV6fG23gA== 299 323 dependencies: 300 324 "@atproto/common-web" "^0.4.2" 325 + "@atproto/syntax" "^0.4.1" 326 + iso-datestring-validator "^2.2.2" 327 + multiformats "^9.9.0" 328 + zod "^3.23.8" 329 + 330 + "@atproto/lexicon@^0.5.1": 331 + version "0.5.1" 332 + resolved "https://registry.yarnpkg.com/@atproto/lexicon/-/lexicon-0.5.1.tgz#e9b7d5c70dc5a38518a8069cd80fea77ab526947" 333 + integrity sha512-y8AEtYmfgVl4fqFxqXAeGvhesiGkxiy3CWoJIfsFDDdTlZUC8DFnZrYhcqkIop3OlCkkljvpSJi1hbeC1tbi8A== 334 + dependencies: 335 + "@atproto/common-web" "^0.4.3" 301 336 "@atproto/syntax" "^0.4.1" 302 337 iso-datestring-validator "^2.2.2" 303 338 multiformats "^9.9.0" ··· 514 549 integrity sha512-sDi68+QE1XHegTaNAndlX41Gp827pouSzSs8CyAwhrqZdsJUxE3P7TMtrA0z+zAjvxVyvzscRc0TsN/fGUGrhw== 515 550 dependencies: 516 551 "@atproto/lexicon" "^0.5.0" 552 + zod "^3.23.8" 553 + 554 + "@atproto/xrpc@^0.7.5": 555 + version "0.7.5" 556 + resolved "https://registry.yarnpkg.com/@atproto/xrpc/-/xrpc-0.7.5.tgz#40cef1a657b5f28af8ebec9e3dac5872e58e88ea" 557 + integrity sha512-MUYNn5d2hv8yVegRL0ccHvTHAVj5JSnW07bkbiaz96UH45lvYNRVwt44z+yYVnb0/mvBzyD3/ZQ55TRGt7fHkA== 558 + dependencies: 559 + "@atproto/lexicon" "^0.5.1" 517 560 zod "^3.23.8" 518 561 519 562 "@aws-crypto/crc32@3.0.0": ··· 8596 8639 dependencies: 8597 8640 "@babel/helper-define-polyfill-provider" "^0.6.3" 8598 8641 8599 - babel-plugin-react-compiler@^19.1.0-rc.1: 8600 - version "19.1.0-rc.1" 8601 - resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.1.tgz#99d131be61017e40abbaedd98321069bf8b7e54a" 8602 - integrity sha512-M4fpG+Hfq5gWzsJeeMErdRokzg0fdJ8IAk+JDhfB/WLT+U3WwJWR8edphypJrk447/JEvYu6DBFwsTn10bMW4Q== 8603 - dependencies: 8604 - "@babel/types" "^7.26.0" 8605 - 8606 - babel-plugin-react-compiler@^19.1.0-rc.2: 8642 + babel-plugin-react-compiler@^19.1.0-rc.2, babel-plugin-react-compiler@^19.1.0-rc.3: 8607 8643 version "19.1.0-rc.3" 8608 8644 resolved "https://registry.yarnpkg.com/babel-plugin-react-compiler/-/babel-plugin-react-compiler-19.1.0-rc.3.tgz#45e5a282a2460b3701971e5eb8310a90a7919022" 8609 8645 integrity sha512-mjRn69WuTz4adL0bXGx8Rsyk1086zFJeKmes6aK0xPuK3aaXmDJdLHqwKKMrpm6KAI1MCoUK72d2VeqQbu8YIA== ··· 10817 10853 dependencies: 10818 10854 "@typescript-eslint/utils" "^5.61.0" 10819 10855 10820 - eslint-plugin-react-compiler@^19.1.0-rc.1: 10821 - version "19.1.0-rc.1" 10822 - resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.1.0-rc.1.tgz#e974ba9541c9a4464d77723e0505b5742bc22e56" 10823 - integrity sha512-3umw5eqZXapBl7aQGmvcjheKhUbsElb9jTETxRZg371e1LG4EPs/zCHt2JzP+wNcdaZWzjU/R730zPUJblY2zw== 10856 + eslint-plugin-react-compiler@^19.1.0-rc.2: 10857 + version "19.1.0-rc.2" 10858 + resolved "https://registry.yarnpkg.com/eslint-plugin-react-compiler/-/eslint-plugin-react-compiler-19.1.0-rc.2.tgz#83343e7422e00fa61e729af8e8468f0ddec37925" 10859 + integrity sha512-oKalwDGcD+RX9mf3NEO4zOoUMeLvjSvcbbEOpquzmzqEEM2MQdp7/FY/Hx9NzmUwFzH1W9SKTz5fihfMldpEYw== 10824 10860 dependencies: 10825 10861 "@babel/core" "^7.24.4" 10826 10862 "@babel/parser" "^7.24.4"