{"owner":"bluesky-social","repo":"social-app","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["CLAUDE.md"],"files":{"CLAUDE.md":"# CLAUDE.md – Bluesky Social App Development Guide\n\nThis document provides guidance for working effectively in the Bluesky Social app codebase.\n\n## Project Overview\n\nBluesky 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.\n\n**Tech Stack:**\n\n- React 19.1\n- React Native 0.81 with Expo 54\n- TypeScript 7\n- React Navigation 7 for routing\n- TanStack Query (React Query) for data fetching\n- Lingui 5 for internationalization\n- Custom design system called ALF (Application Layout Framework)\n\nPrefer 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.\n\n## Essential Commands\n\n```bash\n# Development\npnpm start              # Start Expo dev server\npnpm web                # Start web version\npnpm android            # Run on Android\npnpm ios                # Run on iOS\n\n# Testing & Quality\n# IMPORTANT: Always use these pnpm scripts, never call the underlying tools directly\npnpm test               # Run Jest tests\npnpm lint               # Run Oxlint\npnpm typecheck          # Run TypeScript type checking\npnpm prettier           # Run Prettier for code formatting\n\n# Internationalization\n# DO NOT run these commands - extraction and compilation are handled by CI\npnpm intl:extract       # Extract translation strings (nightly CI job)\npnpm intl:compile       # Compile translations for runtime (nightly CI job)\n\n# Build\npnpm build-web          # Build web version\npnpm prebuild           # Generate native projects\n```\n\n## Project Structure\n\n```\nsrc/\n├── alf/                    # Design system (ALF) - themes, atoms, tokens\n├── components/             # Shared UI components (Button, Dialog, Menu, etc.)\n├── screens/                # Full-page screen components (newer pattern)\n├── features/               # Macro-features that bridge components/screens\n├── view/\n│   ├── screens/            # Full-page screens (legacy location)\n│   ├── com/                # Reusable view components\n│   └── shell/              # App shell (navigation bars, tabs)\n├── state/\n│   ├── queries/            # TanStack Query hooks\n│   ├── preferences/        # User preferences (React Context)\n│   ├── session/            # Authentication state\n│   └── persisted/          # Persistent storage layer\n├── lib/                    # Utilities, constants, helpers\n├── locale/                 # i18n configuration and language files\n└── Navigation.tsx          # Main navigation configuration\n```\n\n### Project Structure in Depth\n\nWhen building new things, follow these guidelines for where to put code.\n\n#### Components vs Screens vs Features\n\n**Components** are reusable UI elements that are not full screens. Should be\nplatform-agnostic when possible. Examples: Button, Dialog, Menu, TextField. Put\nthese in `/components` if they are shared across screens.\n\n**Screens** are full-page components that represent a route in the app. They\noften contain multiple components and handle layout for a page. New screens\nshould go in `/screens` (not `/view/screens`) to encourage better organization\nand separation from legacy code.\n\nFor complex screens that have specific components or data needs that _are not\nshared by other screens_, we encourage subdirectories within `/screens/<name>`\ne.g. `/screens/ProfileScreen/ProfileScreen.tsx` and\n`/screens/ProfileScreen/components/`.\n\n**Features** are higher-level modules that may include context, data fetching,\ncomponents, and utilities related to a specific feature e.g.\n`/features/liveNow`. They don't neatly fit into components or screens and often\nspan multiple screens. This is an optional pattern for organizing complex\nfeatures.\n\n#### Legacy Directories\n\nFor the most part, avoid writing new files into the `/view` directory and\nsubdirectories. This is the older pattern for organizing screens and components,\nand it has become a bit disorganized over time. New development should go into\n`/screens`, `/components`, and `/features`.\n\n#### State\n\nThe `/state` directory is where we've historically put all our data fetching and\nstate management logic. This is perfectly fine, but for new features, consider\norganizing state logic closer to the components that use it, either within a\nfeature directory or co-located with a screen. The key is to keep related code\ntogether and avoid having \"god files\" with too much unrelated logic.\n\n#### Lib\n\nThe `/lib` directory is for utilities and helpers that don't fit into other\ncategories. This can include things like API clients, formatting functions,\nconstants, and other shared logic.\n\n#### Top Level Directories\n\nAvoid writing new top-level subdirectories within `/src`. We've done this for a\nfew things in the past that, but we have stronger patterns now. Examples:\n`/logger` should probably have been written into `/lib`. And `ageAssurance` is\nbetter classified within `/features`. We will probably migrate these things\neventually.\n\n### File and Directory Naming Conventions\n\nTypically JS style for variables, functions, etc. We use ProudCamelCase for\ncomponents, and camelCase directories and files.\n\nFor \"macro\" cases in `/features`, `/screens`, or `/components`, co-locate related\ncode in a directory with an `index.tsx` main component plus sibling\ncomponents/hooks/utils (e.g. `screens/ProfileScreen/index.tsx` +\n`screens/ProfileScreen/components/`). Keep related code together so it lives where\nsomeone would look for it. Don't overdo it: a component that fits in one file\nshould just be `Component.tsx`, not `Component/index.tsx`.\n\nPlatform-specific files are covered under \"Platform-Specific Code\" below.\n\n### Comments\n\nComment code when necessary to explain the “why” behind something; avoid\ncomments that simply describe the code. Avoid Unicode characters in comments,\ne.g., use `-` not `—`.\n\nAlways use docblock (`/** */`) syntax for comments that document a type, type\nmember, method, function, or variable. These are the comments a reader expects\nto find attached to a named declaration, and the docblock form makes that intent\nclear and surfaces nicely in editor tooltips.\n\n```tsx\ntype DateFieldProps = {\n  /**\n   * An empty string renders the placeholder and opens the picker at today (or\n   * maximumDate, if earlier).\n   */\n  value: string | Date\n}\n\n/**\n * Date-only input. Accepts a string in the format YYYY-MM-DD, or a Date object.\n */\nexport function DateField() {}\n```\n\nMore generally, any multiline comment should use the `/* */` block syntax rather\nthan stacked `//` lines. Reserve `//` for short, single-line comments.\n\n```tsx\n/*\n * The picker requires a valid date, so when value is empty we fall back to\n * maximumDate (if set) or today.\n */\nconst fallbackDate = maximumDate ? toSimpleDateString(maximumDate) : today\n```\n\n### Documentation and Tests Within Features\n\nFor larger features or components, co-locate documentation and tests with the\ncode. A `README.md` in the directory (the `/Component/index.tsx` pattern lends\nitself well to this) can document the whole feature, and feature-specific tests\nbelong alongside it as `Component.test.tsx` or in a `__tests__/` subdirectory.\nBoth are optional.\n\n## Styling System (ALF)\n\nALF is the custom design system. Tailwind-inspired naming with underscores\ninstead of hyphens. Static atoms (`atoms as a`) are theme-independent; theme\natoms/palette come from `useTheme()` (`t.atoms.bg`, `t.palette.primary_500`).\nStyle props take an array of atoms + theme atoms + raw styles.\n\nOrder atoms by: flexbox (`a.flex_row`), spacing (`a.px_md`), text (`a.font_bold`),\nthemes (`t.atoms.text`), then raw styles (`{backgroundColor: t.palette.primary_500}`).\n\n```tsx\nimport {atoms as a, useTheme} from '#/alf'\n\nconst t = useTheme()\n<View style={[a.flex_row, a.gap_md, a.p_lg, t.atoms.bg]} />\n```\n\n### Key Concepts\n\nStatic atoms live in `a.*` (e.g. `a.flex_row`, `a.p_md`, `a.rounded_md`,\n`a.text_lg`). Theme atoms/palette come from `useTheme()` (`t.atoms.bg`,\n`t.atoms.text`, `t.atoms.border_contrast_low`, `t.palette.primary_500`).\n\n**Platform utilities** (`import {web, native, ios, android, platform} from '#/alf'`)\nreturn conditional styles inline in a style array: `web({cursor: 'pointer'})`,\n`native({paddingBottom: 20})`, `platform({ios: {...}, android: {...}, web: {...}})`.\n\n**Breakpoints:** `const {gtPhone, gtMobile, gtTablet} = useBreakpoints()` from `#/alf`.\n\n### Naming Conventions\n\n- Spacing: `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl` (t-shirt sizes)\n- Text: `text_xs`, `text_sm`, `text_md`, `text_lg`, `text_xl`\n- Gaps/Padding: `gap_sm`, `p_md`, `px_lg`, `py_xl`\n- Flex: `flex_row`, `flex_1`, `align_center`, `justify_between`\n- Borders: `border`, `border_t`, `rounded_md`, `rounded_full`\n\n## Component Patterns\n\n- Prefer fragment shorthand over `Fragment` unless a `key` is needed.\n- Prefer functions over arrow functions for component declarations.\n- Prefer prop destructuring via parameters over a const within the component.\n- Prefer inline types over `Props` types or interfaces.\n- Set reasonable defaults for optional props.\n- Prefer the implicit global `React` for types over `type` imports.\n\n```tsx\nimport {Fragment} from 'react'\nimport {View} from 'react-native'\nimport {Trans} from '@lingui/react/macro'\n\nimport {Text} from '#/components/Typography'\n\nfunction MyComponent({\n  items = [],\n  children,\n}: {\n  items?: string[]\n  children: React.ReactNode\n}) {\n  return (\n    <>\n      <View>\n        <Text>\n          <Trans>Example</Trans>\n        </Text>\n      </View>\n      <View>\n        {items.map((item, index) => (\n          <Fragment key={item}>\n            <Text>{index}</Text>\n            <Text>{item}</Text>\n          </Fragment>\n        ))}\n        {children}\n      </View>\n    </>\n  )\n}\n```\n\n### Dialog Component\n\nLives in `#/components/Dialog`. Bottom sheet on native, modal on web. Manage\nstate with `useDialogControl()`. `Dialog.Handle` renders native-only, `Dialog.Close`\nweb-only. CRITICAL: run any post-close action inside the `control.close(() => ...)`\ncallback (see Footguns). Compound-component usage; canonical example in any dialog\nunder `#/components`.\n\n### Menu Component\n\nLives in `#/components/Menu`. Dropdown on web, bottom sheet dialog on native.\n`Menu.Divider` is web-only, `Menu.ContainerItem` native-only. Compound API\n(`Menu.Root` / `Menu.Trigger` / `Menu.Outer` / `Menu.Group` / `Menu.Item`); grep\nexisting usages across the app for a canonical example.\n\n### Button Component\n\n`import {Button, ButtonText, ButtonIcon} from '#/components/Button'`. Props:\n\n- `color`: `'primary'` | `'secondary'` | `'negative'` | `'primary_subtle'` | `'negative_subtle'` | `'secondary_inverted'`\n- `size`: `'tiny'` | `'small'` | `'large'`\n- `shape`: `'default'` (pill) | `'round'` | `'square'` | `'rectangular'`\n- `variant`: `'solid'` | `'outline'` | `'ghost'` (deprecated, prefer `color`)\n\n### TextField\n\nCompound component at `#/components/forms/TextField` (`TextField.LabelText`,\n`TextField.Root`, `TextField.Icon`, `TextField.Input`). Prefer `defaultValue` over\n`value` (see Footguns).\n\n### Typography\n\n`import {Text, H1, H2, P} from '#/components/Typography'`. The `Text` default style\nis `[a.text_sm, a.leading_snug, t.atoms.text]`. Pass the `emoji` prop to any `Text`\nthat may contain emoji - user-generated text (display names etc.) almost always\ndoes, so only omit it for static, emoji-free strings: `<Text emoji>Hello!</Text>`.\n\n## Internationalization (i18n)\n\nAll 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.\n\nPrefer 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` ``.\n\nPrefer 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`.\n\n```tsx\nimport {plural} from '@lingui/core/macro'\nimport {Trans, useLingui} from '@lingui/react/macro'\n\nfunction MyComponent() {\n  const {t: l} = useLingui()\n\n  // Simple strings - use the l macro\n  const title = l`Settings`\n  const errorMessage = l({\n    message: 'Something went wrong',\n    comment: 'Generic error message for unknown/unhandled errors.',\n    context: 'Toast',\n  })\n\n  // Strings with variables\n  const greeting = l`Hello, ${name}!`\n\n  // Pluralization\n  const countLabel = plural(count, {\n    one: '# item',\n    other: '# items',\n  })\n\n  // JSX content - use Trans component\n  return (\n    <Text>\n      <Trans>\n        Welcome to <Text style={a.font_bold}>Bluesky</Text>, {name}!\n      </Trans>\n    </Text>\n  )\n}\n```\n\nPrefer `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`.\n\n```tsx\nimport {useLingui} from '@lingui/react/macro'\n\nfunction MyComponent() {\n  const {i18n} = useLingui()\n\n  const createdAt = new Date()\n\n  return i18n.date(createdAt, {\n    dateStyle: 'medium',\n    timeStyle: 'medium',\n  })\n}\n```\n\n**Commands:**\n\n```bash\n# DO NOT run these commands - extraction and compilation are handled by a nightly CI job\npnpm intl:extract    # Extract new strings to locale files\npnpm intl:compile    # Compile translations for runtime\n```\n\n## State Management\n\n### TanStack Query (Data Fetching)\n\nFollow the established pattern in `src/state/queries/`; `src/state/queries/feed.ts`\nis a good canonical reference (it uses `createQueryKey`, matching key roots,\n`useInfiniteQuery`, and `persistedVersion`).\n\n- Build query keys with `createQueryKey(root, args)` (from `#/state/queries/util`)\n  using an object for `args`. The key root variable should match the hook name.\n- Naming conventions: `use[Name]Query` for queries, `use[Name]Mutation` for\n  mutations, `use[Name]CacheMutation` for helpers that mutate cached data directly.\n- Stale times come from `STALE` in `src/state/queries/index.ts`: `STALE.SECONDS.FIFTEEN`,\n  `STALE.MINUTES.ONE`, `STALE.MINUTES.FIVE`, `STALE.HOURS.ONE`, `STALE.INFINITY`.\n- Paginated atproto APIs (those returning a `cursor`) use `useInfiniteQuery` with\n  `getNextPageParam: page => page.cursor`; flatten results with\n  `data?.pages.flatMap(page => page.items) ?? []`.\n- Persist a query across restarts by passing options:\n  `createQueryKey(root, args, {persistedVersion: n})`. Bumping `n` clears the old\n  persisted data and refetches - do this whenever the data shape changes.\n- Error handling in mutations: don't log network errors (just inform the user),\n  handle typed XRPC errors specifically (e.g. `err instanceof SomeNsid.SomeError`),\n  and send unexpected errors to `logger.error('...', {safeMessage: error})`.\n\n### Preferences (React Context)\n\nBoolean/simple UI preferences are exposed as paired hooks from `#/state/preferences`,\ne.g. `useAutoplayDisabled()` / `useSetAutoplayDisabled()`.\n\n### Session State\n\n`import {useSession, useAgent} from '#/state/session'`. `useSession()` gives\n`hasSession` and `currentAccount`; `useAgent()` gives the atproto agent for API calls.\n\n## Navigation\n\nReact Navigation with type-safe route params. Type a screen with\n`NativeStackScreenProps<CommonNavigatorParams, 'X'>` (`route`/`navigation` come\nfrom props; params via `route.params`). Navigate programmatically with\n`useNavigation()`, or the `navigate` helper from `#/Navigation`. Config lives in\n`src/Navigation.tsx`, routes in `src/routes.ts`, types in `src/lib/routes/types.ts`.\n\n## Platform-Specific Code\n\nUse file extensions for platform-specific implementations. The bundler resolves\nthem automatically - just import the base path normally, never a conditional\n`require()`.\n\n```\nComponent.tsx          # Shared/default\nComponent.web.tsx      # Web-only\nComponent.native.tsx   # iOS + Android\nComponent.ios.tsx      # iOS-only\nComponent.android.tsx  # Android-only\n```\n\nPrefer grouping variants into a `Component/` directory (`index.tsx`,\n`index.web.tsx`, `index.native.tsx`) rather than sibling `Component.web.tsx` files,\nso the shared surface reads as one \"macro\" module (e.g. `src/components/Dialog/index.tsx`\nnative vs `index.web.tsx` web). The app has both patterns; the directory form is\npreferred for new code.\n\n```tsx\n// CORRECT - bundler picks storage.ts or storage.web.ts automatically\nimport * as storage from '#/state/drafts/storage'\n\n// WRONG - don't use require() or conditional imports for platform files\nconst storage = IS_NATIVE\n  ? require('#/state/drafts/storage')\n  : require('#/state/drafts/storage.web')\n```\n\nRuntime platform detection (not for imports): `import {IS_WEB, IS_NATIVE, IS_IOS, IS_ANDROID} from '#/env'`.\n\n## Import Aliases\n\nAlways use the `#/` alias for absolute imports:\n\n```tsx\n// Good\nimport {useSession} from '#/state/session'\nimport {atoms as a, useTheme} from '#/alf'\nimport {Button} from '#/components/Button'\n\n// Avoid\nimport {useSession} from '../../../state/session'\n```\n\n## Footguns\n\nCommon pitfalls to avoid in this codebase:\n\n### Dialog Close Callback (Critical)\n\n**Always use `control.close(() => ...)` when performing actions after closing a dialog.** The callback ensures the action runs after the dialog's close animation completes. Failing to do this causes race conditions with React state updates.\n\n```tsx\n// WRONG - causes bugs with state updates, navigation, opening other dialogs\nconst onConfirm = () => {\n  control.close()\n  navigation.navigate('Home') // May race with dialog animation\n}\n\n// WRONG - same problem\nconst onConfirm = () => {\n  control.close()\n  otherDialogControl.open() // Will likely fail or cause visual glitches\n}\n\n// CORRECT - action runs after dialog fully closes\nconst onConfirm = () => {\n  control.close(() => {\n    navigation.navigate('Home')\n  })\n}\n\n// CORRECT - opening another dialog after close\nconst onConfirm = () => {\n  control.close(() => {\n    otherDialogControl.open()\n  })\n}\n\n// CORRECT - state updates after close\nconst onConfirm = () => {\n  control.close(() => {\n    setSomeState(newValue)\n    onCallback?.()\n  })\n}\n```\n\nThis applies to:\n\n- Navigation (`navigation.navigate()`, `navigation.push()`)\n- Opening other dialogs or menus\n- State updates that affect UI (`setState`, `queryClient.invalidateQueries`)\n- Callbacks passed from parent components\n\nThe Menu component on iOS specifically uses this pattern – see `src/components/Menu/index.tsx:151`.\n\n### Controlled vs Uncontrolled Inputs\n\nPrefer `defaultValue` over `value` for TextInput on the old architecture:\n\n```tsx\n// Preferred - uncontrolled\n<TextField.Input\n  defaultValue={initialEmail}\n  onChangeText={setEmail}\n/>\n\n// Avoid when possible - controlled (can cause performance issues)\n<TextField.Input\n  value={email}\n  onChangeText={setEmail}\n/>\n```\n\n### Platform-Specific Behavior\n\nSome components behave differently across platforms:\n\n- `Dialog.Handle` – Only renders on native (drag handle for bottom sheet)\n- `Dialog.Close` – Only renders on web (X button)\n- `Menu.Divider` – Only renders on web\n- `Menu.ContainerItem` – Only works on native\n\nAlways test on multiple platforms when using these components.\n\n### React Compiler is Enabled\n\nThis codebase uses React Compiler, so **don't proactively add `useMemo` or `useCallback`**. The compiler handles memoization automatically.\n\n```tsx\n// UNNECESSARY - React Compiler handles this\nconst handlePress = useCallback(() => {\n  doSomething()\n}, [doSomething])\n\n// JUST WRITE THIS\nconst handlePress = () => {\n  doSomething()\n}\n```\n\nOnly use `useMemo`/`useCallback` when you have a specific reason, such as:\n\n- The value is immediately used in an effect's dependency array\n- You're passing a callback to a non-React library that needs referential stability\n\n## Best Practices\n\n1. **Accessibility**: Always provide `label` prop for interactive elements, use `accessibilityHint` where helpful\n\n2. **Translations**: Wrap ALL user-facing strings with ` `l` `` or `<Trans>`\n\n3. **Styling**: Combine static atoms with theme atoms, use platform utilities for platform-specific styles\n\n4. **State**: Use TanStack Query for server state, React Context for UI preferences\n\n5. **Components**: Check if a component exists in `#/components/` before creating new ones\n\n6. **Types**: Define explicit types for props, use `NativeStackScreenProps` for screens\n\n7. **Testing**: Components should have `testID` props for E2E testing\n\n## Key Files Reference\n\n| Purpose           | Location                                     |\n| ----------------- | -------------------------------------------- |\n| Theme definitions | `src/alf/themes.ts`                          |\n| Design tokens     | `src/alf/tokens.ts`                          |\n| Static atoms      | `src/alf/atoms.ts` (extends `@bsky.app/alf`) |\n| Navigation config | `src/Navigation.tsx`                         |\n| Route definitions | `src/routes.ts`                              |\n| Route types       | `src/lib/routes/types.ts`                    |\n| Query hooks       | `src/state/queries/*.ts`                     |\n| Session state     | `src/state/session/index.tsx`                |\n| i18n setup        | `src/locale/i18n.ts`                         |\n"}}