Bluesky app fork with some witchin' additions 💫 witchsky.app
bluesky fork client
117
fork

Configure Feed

Select the types of activity you want to include in your feed.

Update CLAUDE.md with additional context and instructions (#10385)

authored by

DS Boyce and committed by
GitHub
b97cd413 8bfce096

+165 -81
+165 -81
CLAUDE.md
··· 1 - # CLAUDE.md - Bluesky Social App Development Guide 1 + # CLAUDE.md – Bluesky Social App Development Guide 2 2 3 3 This document provides guidance for working effectively in the Bluesky Social app codebase. 4 4 ··· 7 7 Bluesky Social is a cross-platform social media application built with React Native and Expo. It runs on iOS, Android, and Web, connecting to the AT Protocol (atproto) decentralized social network. 8 8 9 9 **Tech Stack:** 10 + 11 + - React 19.1 10 12 - React Native 0.81 with Expo 54 11 - - TypeScript 12 - - React Navigation for routing 13 + - TypeScript 6 14 + - React Navigation 7 for routing 13 15 - TanStack Query (React Query) for data fetching 14 - - Lingui for internationalization 16 + - Lingui 5 for internationalization 15 17 - Custom design system called ALF (Application Layout Framework) 18 + 19 + Prefer using the latest features available for each of these libraries (exact versions are found in `package.json`). For example, prefer `@lingui/react/macro` over `@lingui/react`. Suggest refactoring legacy or deprecated uses. 16 20 17 21 ## Essential Commands 18 22 ··· 162 166 163 167 ### Documentation and Tests Within Features 164 168 169 + Comment code when necessary to explain the “why” behind something; avoid 170 + comments that simply describe the code. Avoid Unicode characters in comments, 171 + e.g., use `-` not `—`. 172 + 165 173 For larger features or components, it's helpful to include a README.md file 166 174 within the directory that explains the purpose of the feature, how it works, and 167 175 any important implementation details. The `/Component/index.tsx` pattern lends ··· 182 190 183 191 ### Basic Usage 184 192 193 + Generally, order atoms by: 194 + 195 + - Flexbox configuration, e.g., `a.flex_row` 196 + - Spacing, e.g., `a.px_md` 197 + - Text styles, e.g., `a.font_bold` 198 + - Themes, e.g., `t.atoms.text`, 199 + - Raw styles, e.g., `{backgroundColor: t.palette.primary_500}` 200 + 185 201 ```tsx 186 202 import {atoms as a, useTheme} from '#/alf' 187 203 ··· 190 206 191 207 return ( 192 208 <View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]}> 193 - <Text style={[a.text_md, a.font_bold, t.atoms.text]}> 194 - Hello 195 - </Text> 209 + <Text style={[a.text_md, a.font_bold, t.atoms.text]}>Hello</Text> 196 210 </View> 197 211 ) 198 212 } ··· 200 214 201 215 ### Key Concepts 202 216 203 - **Static Atoms** - Theme-independent styles imported from `atoms`: 217 + **Static Atoms** – Theme-independent styles imported from `atoms`: 218 + 204 219 ```tsx 205 220 import {atoms as a} from '#/alf' 206 221 // a.flex_row, a.p_md, a.gap_sm, a.rounded_md, a.text_lg, etc. 207 222 ``` 208 223 209 - **Theme Atoms** - Theme-dependent colors from `useTheme()`: 224 + **Theme Atoms** – Theme-dependent colors from `useTheme()`: 225 + 210 226 ```tsx 211 227 const t = useTheme() 212 228 // t.atoms.bg, t.atoms.text, t.atoms.border_contrast_low, etc. 213 229 // t.palette.primary_500, t.palette.negative_400, etc. 214 230 ``` 215 231 216 - **Platform Utilities** - For platform-specific styles: 232 + **Platform Utilities** – For platform-specific styles: 233 + 217 234 ```tsx 218 235 import {web, native, ios, android, platform} from '#/alf' 219 236 ··· 225 242 ] 226 243 ``` 227 244 228 - **Breakpoints** - Responsive design: 245 + **Breakpoints** – Responsive design: 246 + 229 247 ```tsx 230 248 import {useBreakpoints} from '#/alf' 231 249 ··· 245 263 246 264 ## Component Patterns 247 265 266 + - Prefer fragment shorthand over `Fragment` unless a `key` is needed. 267 + - Prefer functions over arrow functions for component declarations. 268 + - Prefer prop destructuring via parameters over a const within the component. 269 + - Prefer inline types over `Props` types or interfaces. 270 + - Set reasonable defaults for optional props. 271 + 272 + ```tsx 273 + import {Fragment} from 'react' 274 + import {View} from 'react-native' 275 + import {Trans, useLingui} from '@lingui/react/macro' 276 + 277 + import {Text} from '#/components/Typography' 278 + 279 + function MyComponent({foo = []}: {foo?: string[]}) { 280 + const {t: l} = useLingui() 281 + 282 + return ( 283 + <> 284 + <View><Text><Trans>Example</Trans><Text></View> 285 + <View> 286 + {foo.map((foo, index) => ( 287 + <Fragment key={foo}> 288 + <Text>{index}</Text> 289 + <Text>{foo}</Text> 290 + </Fragment> 291 + ))} 292 + </View> 293 + </> 294 + ); 295 + } 296 + ``` 297 + 248 298 ### Dialog Component 249 299 250 300 Dialogs use a bottom sheet on native and a modal on web. Use `useDialogControl()` hook to manage state. ··· 263 313 264 314 <Dialog.Outer control={control}> 265 315 {/* Typically the inner part is in its own component */} 266 - <Dialog.Handle /> {/* Native-only drag handle */} 267 - <Dialog.ScrollableInner label={_(msg`My Dialog`)}> 268 - <Dialog.Header> 269 - <Dialog.HeaderText>Title</Dialog.HeaderText> 270 - </Dialog.Header> 316 + <DialogInner /> 317 + </Dialog.Outer> 318 + </> 319 + ) 320 + } 271 321 272 - <Text>Dialog content here</Text> 273 - 274 - <Button label="Done" onPress={() => control.close()}> 275 - <ButtonText>Done</ButtonText> 276 - </Button> 277 - <Dialog.Close /> {/* Web-only X button in top left */} 278 - </Dialog.ScrollableInner> 279 - </Dialog.Outer> 322 + function DialogInner() { 323 + return ( 324 + <> 325 + <Dialog.Handle /> {/* Native-only drag handle */} 326 + <Dialog.ScrollableInner label={l`My Dialog`}> 327 + <Dialog.Header> 328 + <Dialog.HeaderText>Title</Dialog.HeaderText> 329 + </Dialog.Header> 330 + <Text>Dialog content here</Text> 331 + <Button label="Done" onPress={() => control.close()}> 332 + <ButtonText>Done</ButtonText> 333 + </Button> 334 + <Dialog.Close /> {/* Web-only X button in top left */} 335 + </Dialog.ScrollableInner> 280 336 </> 281 337 ) 282 338 } ··· 345 401 ``` 346 402 347 403 **Button Props:** 404 + 348 405 - `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'` 349 406 - `size`: `'tiny'` | `'small'` | `'large'` 350 407 - `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'` ··· 384 441 385 442 ## Internationalization (i18n) 386 443 387 - All user-facing strings must be wrapped for translation using Lingui. 444 + All user-facing strings must be wrapped for translation using Lingui. Include `comment` and/or `context` props when necessary to avoid ambiguity, e.g., “Post” as a noun vs a verb. 445 + 446 + Prefer using `t` via `import {useLingui} '@lingui/react/macro'` vs `_` via `import {useLingui} from '@lingui/react'`. Alias `t` to `l` to avoid collisions with `const t = useTheme()`. Refactor existing uses of ``_(msg`foo`)`` to use `` l`foo` ``. 447 + 448 + Prefer Unicode punctuation over keyboard punctuation, e.g., `“quote”` over `"quote"`. Prefer en dashes preceded by a non-breaking space over em dashes, e.g., `one – two` over `one—two`. 388 449 389 450 ```tsx 390 - import {msg, plural} from '@lingui/core/macro' 391 - import {Trans} from '@lingui/react/macro' 392 - import {useLingui} from '@lingui/react' 451 + import {plural} from '@lingui/core/macro' 452 + import {Trans, useLingui} from '@lingui/react/macro' 393 453 394 454 function MyComponent() { 395 - const {_} = useLingui() 455 + const {t: l} = useLingui() 396 456 397 - // Simple strings - use msg() with _() function 398 - const title = _(msg`Settings`) 399 - const errorMessage = _(msg`Something went wrong`) 457 + // Simple strings - use the l macro 458 + const title = l`Settings` 459 + const errorMessage = l({ 460 + message: 'Something went wrong', 461 + comment: 'Generic error message for unknown/unhandled errors.', 462 + context: 'Toast', 463 + }) 400 464 401 465 // Strings with variables 402 - const greeting = _(msg`Hello, ${name}!`) 466 + const greeting = l`Hello, ${name}!` 403 467 404 468 // Pluralization 405 - const countLabel = _(plural(count, { 469 + const countLabel = plural(count, { 406 470 one: '# item', 407 471 other: '# items', 408 - })) 472 + }) 409 473 410 474 // JSX content - use Trans component 411 475 return ( 412 476 <Text> 413 - <Trans>Welcome to <Text style={a.font_bold}>Bluesky</Text></Trans> 477 + <Trans> 478 + Welcome to <Text style={a.font_bold}>Bluesky</Text>, {name}! 479 + </Trans> 414 480 </Text> 415 481 ) 416 482 } 417 483 ``` 418 484 485 + Prefer `i18n.date` for date and time formatting. This ensures formatting is re-applied when the language changes at runtime. Refactor existing uses of `Intl.DateTimeFormat` to use `i18n.date`. 486 + 487 + ```tsx 488 + import {useLingui} from '@lingui/react/macro' 489 + 490 + function MyComponent() { 491 + const {i18n} = useLingui() 492 + 493 + const createdAt = new Date() 494 + 495 + return i18n.date(createdAt, { 496 + dateStyle: 'medium', 497 + timeStyle: 'medium', 498 + }) 499 + } 500 + ``` 501 + 419 502 **Commands:** 503 + 420 504 ```bash 421 505 # DO NOT run these commands - extraction and compilation are handled by a nightly CI job 422 506 yarn intl:extract # Extract new strings to locale files ··· 473 557 const queryClient = useQueryClient() 474 558 475 559 return useMutation({ 476 - mutationFn: async (data) => { 560 + mutationFn: async data => { 477 561 // Update logic 478 562 }, 479 563 onSuccess: (_, variables) => { ··· 481 565 queryKey: createProfileQueryKey({did: variables.did}), 482 566 }) 483 567 }, 484 - onError: (error) => { 568 + onError: error => { 485 569 if (isNetworkError(error)) { 486 570 // don't log, but inform user 487 571 } else if (error instanceof AppBskyExampleProcedure.ExampleError) { ··· 490 574 // Log unexpected errors to Sentry 491 575 logger.error('Error updating profile', {safeMessage: error}) 492 576 } 493 - } 577 + }, 494 578 }) 495 579 } 496 580 ··· 505 589 const queryClient = useQueryClient() 506 590 507 591 return (data: Partial<Profile>) => { 508 - queryClient.setQueryData(createProfileQueryKey({did: data.did}), oldData => { 509 - if (!oldData) return oldData 510 - return {...oldData, ...data} 511 - }) 592 + queryClient.setQueryData( 593 + createProfileQueryKey({did: data.did}), 594 + oldData => { 595 + if (!oldData) return oldData 596 + return {...oldData, ...data} 597 + }, 598 + ) 512 599 } 513 600 } 514 601 ``` 515 602 516 603 **Stale Time Constants** (from `src/state/queries/index.ts`): 604 + 517 605 ```tsx 518 - STALE.SECONDS.FIFTEEN // 15 seconds 519 - STALE.MINUTES.ONE // 1 minute 520 - STALE.MINUTES.FIVE // 5 minutes 521 - STALE.HOURS.ONE // 1 hour 522 - STALE.INFINITY // Never stale 606 + STALE.SECONDS.FIFTEEN // 15 seconds 607 + STALE.MINUTES.ONE // 1 minute 608 + STALE.MINUTES.FIVE // 5 minutes 609 + STALE.HOURS.ONE // 1 hour 610 + STALE.INFINITY // Never stale 523 611 ``` 524 612 525 613 **Paginated APIs:** Many atproto APIs return paginated results with a `cursor`. Use `useInfiniteQuery` for these: ··· 565 653 const autoplayDisabled = useAutoplayDisabled() 566 654 const setAutoplayDisabled = useSetAutoplayDisabled() 567 655 568 - return ( 569 - <Toggle 570 - value={autoplayDisabled} 571 - onValueChange={setAutoplayDisabled} 572 - /> 573 - ) 656 + return <Toggle value={autoplayDisabled} onValueChange={setAutoplayDisabled} /> 574 657 } 575 658 ``` 576 659 ··· 604 687 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> 605 688 606 689 export function ProfileScreen({route, navigation}: Props) { 607 - const {name} = route.params // Type-safe params 690 + const {name} = route.params // Type-safe params 608 691 609 - return ( 610 - <Layout.Screen> 611 - {/* Screen content */} 612 - </Layout.Screen> 613 - ) 692 + return <Layout.Screen>{/* Screen content */}</Layout.Screen> 614 693 } 615 694 616 695 // Programmatic navigation ··· 637 716 ``` 638 717 639 718 Example from Dialog: 640 - - `src/components/Dialog/index.tsx` - Native (uses BottomSheet) 641 - - `src/components/Dialog/index.web.tsx` - Web (uses modal with Radix primitives) 719 + 720 + - `src/components/Dialog/index.tsx` – Native (uses BottomSheet) 721 + - `src/components/Dialog/index.web.tsx` – Web (uses modal with Radix primitives) 642 722 643 723 **Important:** The bundler automatically resolves platform-specific files. Just import normally: 644 724 ··· 653 733 ``` 654 734 655 735 Platform detection (for runtime logic, not imports): 736 + 656 737 ```tsx 657 738 import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env' 658 739 ··· 687 768 // WRONG - causes bugs with state updates, navigation, opening other dialogs 688 769 const onConfirm = () => { 689 770 control.close() 690 - navigation.navigate('Home') // May race with dialog animation 771 + navigation.navigate('Home') // May race with dialog animation 691 772 } 692 773 693 774 // WRONG - same problem 694 775 const onConfirm = () => { 695 776 control.close() 696 - otherDialogControl.open() // Will likely fail or cause visual glitches 777 + otherDialogControl.open() // Will likely fail or cause visual glitches 697 778 } 698 779 699 780 // CORRECT - action runs after dialog fully closes ··· 720 801 ``` 721 802 722 803 This applies to: 804 + 723 805 - Navigation (`navigation.navigate()`, `navigation.push()`) 724 806 - Opening other dialogs or menus 725 807 - State updates that affect UI (`setState`, `queryClient.invalidateQueries`) 726 808 - Callbacks passed from parent components 727 809 728 - The Menu component on iOS specifically uses this pattern - see `src/components/Menu/index.tsx:151`. 810 + The Menu component on iOS specifically uses this pattern – see `src/components/Menu/index.tsx:151`. 729 811 730 812 ### Controlled vs Uncontrolled Inputs 731 813 ··· 748 830 ### Platform-Specific Behavior 749 831 750 832 Some components behave differently across platforms: 751 - - `Dialog.Handle` - Only renders on native (drag handle for bottom sheet) 752 - - `Dialog.Close` - Only renders on web (X button) 753 - - `Menu.Divider` - Only renders on web 754 - - `Menu.ContainerItem` - Only works on native 833 + 834 + - `Dialog.Handle` – Only renders on native (drag handle for bottom sheet) 835 + - `Dialog.Close` – Only renders on web (X button) 836 + - `Menu.Divider` – Only renders on web 837 + - `Menu.ContainerItem` – Only works on native 755 838 756 839 Always test on multiple platforms when using these components. 757 840 ··· 772 855 ``` 773 856 774 857 Only use `useMemo`/`useCallback` when you have a specific reason, such as: 858 + 775 859 - The value is immediately used in an effect's dependency array 776 860 - You're passing a callback to a non-React library that needs referential stability 777 861 ··· 779 863 780 864 1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful 781 865 782 - 2. **Translations**: Wrap ALL user-facing strings with `msg()` or `<Trans>` 866 + 2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `<Trans>` 783 867 784 868 3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles 785 869 ··· 793 877 794 878 ## Key Files Reference 795 879 796 - | Purpose | Location | 797 - |---------|----------| 798 - | Theme definitions | `src/alf/themes.ts` | 799 - | Design tokens | `src/alf/tokens.ts` | 800 - | Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | 801 - | Navigation config | `src/Navigation.tsx` | 802 - | Route definitions | `src/routes.ts` | 803 - | Route types | `src/lib/routes/types.ts` | 804 - | Query hooks | `src/state/queries/*.ts` | 805 - | Session state | `src/state/session/index.tsx` | 806 - | i18n setup | `src/locale/i18n.ts` | 880 + | Purpose | Location | 881 + | ----------------- | -------------------------------------------- | 882 + | Theme definitions | `src/alf/themes.ts` | 883 + | Design tokens | `src/alf/tokens.ts` | 884 + | Static atoms | `src/alf/atoms.ts` (extends `@bsky.app/alf`) | 885 + | Navigation config | `src/Navigation.tsx` | 886 + | Route definitions | `src/routes.ts` | 887 + | Route types | `src/lib/routes/types.ts` | 888 + | Query hooks | `src/state/queries/*.ts` | 889 + | Session state | `src/state/session/index.tsx` | 890 + | i18n setup | `src/locale/i18n.ts` |