Developer Onboarding Guide

bluesky-social/social-app

Generated by Inkwell Forge — automated codebase documentation analysis. Based on analysis of 131 screens. Subject matter expert review is recommended before distribution.

May 5, 2026

1. Welcome & Overview

Scope: This Developer Onboarding Guide covers 131 screens of the social-app React Native application. The screens assessed include, among others: Inbox (/messages/inbox), Shell (/search/shell), Feed (/src/view/com/feeds/feed), Conversation (/messages/conversation), Chat List (/messages/chat-list), Settings (/settings), Account Settings (/settings/account-settings), Profile Header Standard (/profile/header/profile-header-standard), Profile Header Labeler (/profile/header/profile-header-labeler), Explore (/search/explore), Starter Pack (/starter-pack), Hashtag (/hashtag), Topic (/topic), Saved Feeds (/saved-feeds), Login Form (/login/login-form), Signup State (/signup/state), Onboarding Layout (/onboarding/layout), Deactivated (/deactivated), Takendown (/takendown), Signup Queued (/signup-queued), and many more. Components, patterns, and features not included in the assessed documentation are outside the scope of this document. Because this assessment covers a large but not exhaustive portion of the application, some cross-cutting patterns may exist in unassessed screens.

Document disclosure: Generated by Inkwell Forge — automated codebase documentation analysis. Subject matter expert review is recommended before distribution.

What this application does: social-app is the official Bluesky mobile and web client, built on the AT Protocol (atproto) — a decentralized social networking protocol. It allows users to create accounts, compose and browse posts, follow other users, manage moderation preferences, participate in direct messaging, discover content through algorithmic and custom feeds, and configure their experience through a rich settings system. The primary users are Bluesky social network participants on iOS, Android, and web.

Tech stack at a glance:

Technology Role
React Native + Expo (bare workflow) Cross-platform mobile and web UI framework
@atproto/api AT Protocol client SDK — all Bluesky API calls
React Navigation (native stack + tab) Screen navigation and deep linking
TanStack Query (React Query v5) Server state management, caching, infinite queries
Lingui (@lingui/react) Internationalization (i18n)
react-native-reanimated UI-thread animations (parallax, gestures)
react-native-gesture-handler Gesture recognition (swipe actions, drag-and-drop)
expo-image Optimized image rendering
#/alf (internal) Design system — atoms, theme tokens, breakpoints
#/state/session Authentication and session management
#/state/preferences User preferences (persisted client-side)
#/state/queries/* TanStack Query hooks for all data fetching
#/state/shell Global shell state (minimal mode, active convo, etc.)

How this guide is organized: Sections marked [Tutorial] walk you through an experience step by step. Sections marked [How-to] give you procedures for specific tasks. Sections marked [Reference] are lookup tables and factual descriptions. Sections marked [Explanation] provide context and rationale for architectural decisions.

Who to ask:


2. Environment Setup [Tutorial]

This section takes you from a fresh machine to a running local development environment. Follow every step in order.

Step 1: Prerequisites — Core Toolchain

Node.js: Check the project's .nvmrc or package.json engines field for the required version.

# Install nvm if you don't have it, then:
nvm install   # reads .nvmrc automatically
nvm use
node --version

[Not documented — WHO: Team Lead; WHAT: What is the pinned Node.js version (check .nvmrc or package.json engines)?; WHERE: Replace nvm install with nvm install <version> above.]

Package manager: This project uses yarn as its package manager.

(inferred from Expo/React Native conventions and the presence of a yarn.lock — verify against the root package.json and any README in the repository)

npm install -g yarn
yarn --version

Git:

git --version  # must be installed

Step 2: Prerequisites — iOS Toolchain (macOS only)

Xcode: Install from the Mac App Store or developer.apple.com/xcode. Xcode 15+ is required for modern React Native.

xcode-select --version

Xcode Command Line Tools:

xcode-select --install

Ruby + CocoaPods: React Native iOS projects require CocoaPods. The project likely uses a .ruby-version file.

# Install rbenv (recommended)
brew install rbenv
rbenv install   # reads .ruby-version
rbenv global $(cat .ruby-version)

# Install bundler and pods
gem install bundler
bundle install

[Not documented — WHO: iOS Lead; WHAT: Does the project use bundle exec pod install (Bundler) or system pod install? Is there a .ruby-version file?; WHERE: Adjust the Ruby/CocoaPods setup commands above.]


Step 3: Prerequisites — Android Toolchain

Android Studio: Download from developer.android.com/studio.

Android SDK: Open Android Studio → SDK Manager → install the SDK platform version targeted by the app.

[Not documented — WHO: Android Lead; WHAT: What is the target Android SDK version (check android/build.gradle)?; WHERE: Add the specific SDK version to the Android Studio SDK Manager step above.]

JDK: React Native requires JDK 17 for modern builds.

# macOS with Homebrew:
brew install openjdk@17
export JAVA_HOME=$(/usr/libexec/java_home -v 17)

ANDROID_HOME environment variable: Add to your ~/.zshrc or ~/.bashrc:

export ANDROID_HOME=$HOME/Library/Android/sdk
export PATH=$PATH:$ANDROID_HOME/emulator
export PATH=$PATH:$ANDROID_HOME/platform-tools

Then reload your shell:

source ~/.zshrc

Android Emulator: In Android Studio → Device Manager → create a new AVD (e.g., Pixel 7, API 34).


Step 4: Expo / EAS CLI

This project uses Expo's bare workflow. Install the EAS CLI for builds and the Expo CLI for local development:

npm install -g eas-cli
npm install -g expo-cli   # or use npx expo directly
npx expo login            # authenticate with your Expo account

[Not documented — WHO: Team Lead; WHAT: What is the Expo organization name and which account credentials should new developers use?; WHERE: Add the org name to the npx expo login step above.]


Step 5: Clone and Install

git clone [repository-url]
cd [project-directory]
yarn install

iOS only — install CocoaPods:

cd ios && bundle exec pod install && cd ..

Step 6: Environment Configuration

The project uses environment variables for API endpoints, service URLs, and feature flags. These are likely loaded via a .env file and accessed through the #/env module.

cp .env.example .env

Open .env and fill in the required values:

Variable Purpose Where to get it
[placeholder] AT Protocol PDS service URL Ask Team Lead
[placeholder] Bluesky moderation service URL Ask Team Lead
[placeholder] Analytics service key Ask Team Lead
[placeholder] Expo project ID Ask Team Lead

[Not documented — WHO: Team Lead; WHAT: What are all required .env variables and their values for local development?; WHERE: Replace the placeholder rows in the table above with the actual variable names and descriptions.]

How environment variables are loaded: The #/env module exports platform-detection constants (IS_IOS, IS_ANDROID, IS_NATIVE, IS_WEB, IS_INTERNAL, IS_LIQUID_GLASS) and any build-time configuration. These are bundled at build time by Metro, not read at runtime from process.env in most cases.


Step 7: Expo Prebuild (Bare Workflow)

Because this project uses Expo's bare workflow with native code, you may need to run prebuild if the native directories are not committed:

npx expo prebuild --clean

When to re-run prebuild:

[Not documented — WHO: iOS Lead or Android Lead; WHAT: Are the ios/ and android/ directories committed to the repository, or must expo prebuild be run on first clone?; WHERE: Clarify whether Step 7 is required on first setup or only after config changes.]


Step 8: Start the Development Server

# Start Metro bundler
yarn start
# or
npx expo start

# Launch on iOS Simulator
yarn ios
# or
npx expo run:ios

# Launch on Android Emulator
yarn android
# or
npx expo run:android

Step 9: Verify It Works

After launching, you should see the Bluesky splash screen followed by the login/signup flow. If you have a test account, log in and verify the home feed loads.

[Screenshot: first launch — Bluesky splash screen on iOS Simulator]

Confirm Metro is running: In the Metro terminal, you should see:

Metro waiting on exp://...

And after the app loads:

BUNDLE ./index.js

Step 10: Start Background Services

The messaging feature uses a polling-based event bus (useMessagesEventBus). No separate background service needs to be started manually — polling is managed in-process by the app when message screens are active.

[Not documented — WHO: Team Lead; WHAT: Are there any local mock servers, background workers, or additional services that need to be started for full local development (e.g., a local AT Protocol PDS)?; WHERE: Add any required startup commands here.]


3. Mobile Dev Setup [Tutorial]

Simulator & Emulator Selection

iOS Simulators:

# List available simulators
xcrun simctl list devices

# Boot a specific simulator
xcrun simctl boot "iPhone 15 Pro"

[Not documented — WHO: iOS Lead; WHAT: Which iOS Simulator model is the team's preferred target for development?; WHERE: Add the preferred simulator name to the xcrun simctl boot command above.]

Android Emulators:

# List available AVDs
emulator -list-avds

# Launch an AVD
emulator -avd [AVD_NAME]

Real device testing:

# Verify Android device is connected
adb devices

Metro Bundler

# Start Metro
yarn start

# Clear Metro cache (use when you see module resolution errors)
yarn start --reset-cache
# or
npx expo start -c

# Metro runs on port 8081 by default. If there's a conflict:
yarn start --port 8088

In the Metro terminal:


Debugging

Flipper: If Flipper is configured, connect via the Flipper desktop app (fbflipper.com) for network inspection, React DevTools, and Redux/state inspection.

[Not documented — WHO: Team Lead; WHAT: Is Flipper configured for this project? If so, which plugins are used?; WHERE: Add Flipper plugin setup instructions here.]

Reactotron: If Reactotron is configured, start the Reactotron desktop app before launching the app.

[Not documented — WHO: Team Lead; WHAT: Is Reactotron configured for this project?; WHERE: Add Reactotron setup instructions here.]

React Native DevTools (Hermes): Shake the device or press Cmd+D (iOS Simulator) / Cmd+M (Android Emulator) → "Open Debugger". This opens Chrome DevTools for JS inspection.

Expo Dev Tools: Running npx expo start opens a browser-based panel with QR code, logs, and device management.


First-Run Troubleshooting

Issue Likely Cause Fix
pod install fails with Ruby errors Wrong Ruby version Use rbenv to match .ruby-version: rbenv install && rbenv local $(cat .ruby-version)
pod install fails with "Unable to find a specification for..." CocoaPods cache stale pod repo update && bundle exec pod install
Metro error: "Unable to resolve module X" Native package missing after git pull yarn install && cd ios && bundle exec pod install && cd ..
Metro cache error / red screen after upgrade Metro cache stale yarn start --reset-cache
Gradle build failed Android SDK or JDK version mismatch Verify ANDROID_HOME and JAVA_HOME; check android/build.gradle for required versions
expo prebuild generates unexpected native code app.config.ts or plugin config changed npx expo prebuild --clean
App builds but API calls fail .env not configured Copy .env.example to .env and fill in values from Team Lead
iOS build fails with signing error Missing provisioning profile Ask iOS Lead for development certificates

4. Codebase Orientation [Reference]

Directory Structure

src/
├── screens/             — Screen-level components organized by feature
│   ├── StarterPack/     — Starter Pack creation wizard
│   ├── Onboarding/      — Multi-step onboarding flow
│   ├── Settings/        — All settings screens
│   └── Search/          — Search and Explore screens
├── view/
│   └── com/             — Shared view components (legacy location)
│       ├── auth/        — Auth-related views (splash, login shell)
│       ├── feeds/       — Feed components
│       ├── lists/       — List member components
│       ├── posts/       — Post feed and post thread components
│       ├── profile/     — Profile-related components
│       ├── pager/       — Pager/tab components
│       └── util/        — Shared utility components (Error, EmptyState, etc.)
├── state/
│   ├── session/         — Authentication state, AT Protocol agent
│   ├── preferences/     — User preferences (persisted)
│   ├── queries/         — TanStack Query hooks for all data fetching
│   ├── shell/           — Global shell state (minimal mode, active convo)
│   ├── cache/           — Profile shadow cache, optimistic updates
│   └── service-config/  — Server-side feature flags
├── lib/
│   ├── constants.ts     — App-wide constants (DISCOVER_FEED_URI, etc.)
│   ├── routes/          — Navigation type definitions (CommonNavigatorParams)
│   ├── strings/         — String utilities (sanitizeHandle, cleanError, etc.)
│   ├── hooks/           — Shared custom hooks
│   └── media/           — Media picker utilities
├── components/          — Shared UI components (new design system location)
│   ├── Button.tsx
│   ├── Layout.tsx
│   ├── Link.tsx
│   ├── Typography.tsx
│   ├── forms/           — Toggle, TextField, Select components
│   ├── icons/           — SVG icon components
│   └── dialogs/         — Shared dialog components
├── navigation/          — Navigator definitions (inferred from route types)
├── locale/              — Lingui translation catalogs and language lists
├── analytics/           — Analytics abstraction (ax.metric, ax.logger)
├── features/
│   └── liveNow/         — Live streaming feature
├── alf/                 — Internal design system (atoms, theme, breakpoints)
│   ├── atoms.ts         — Atomic style tokens
│   ├── themes/          — Light, dim, dark theme definitions
│   └── tokens.ts        — Spacing, border radius, etc.
└── env.ts               — Platform detection constants (IS_IOS, IS_NATIVE, etc.)

modules/                 — Local Expo native modules
├── expo-bluesky-gif-view/    — Native GIF playback
├── expo-emoji-picker/        — Native emoji picker
├── expo-scroll-forwarder/    — Scroll event forwarding for iOS pager
└── expo-bluesky-swiss-army/  — Shared native utilities (SharedPrefs, etc.)

ios/                     — iOS native project (CocoaPods, Xcode workspace)
android/                 — Android native project (Gradle)
app.config.ts            — Expo app configuration (bundle ID, plugins, version)
metro.config.js          — Metro bundler configuration
babel.config.js          — Babel configuration (Lingui macros, path aliases)

Note: The src/view/com/ directory is a legacy location. New components are being added to src/components/ and src/screens/. When in doubt, ask your team lead which location to use for new work.


Key Files

File Purpose When You'll Touch It
app.config.ts Expo app configuration — bundle ID, app name, plugins, version Adding a new native plugin, changing app metadata
metro.config.js Metro bundler config — path aliases, asset extensions Adding new asset types, debugging module resolution
babel.config.js Babel config — Lingui macros, #/ path alias Adding new Babel plugins, debugging transform issues
src/env.ts Platform detection constants (IS_IOS, IS_NATIVE, IS_WEB, IS_INTERNAL) Rarely; when adding new build-time flags
src/lib/constants.ts App-wide constants (DISCOVER_FEED_URI, MAX_LABELERS, STATUS_PAGE_URL, etc.) Adding new constants, updating feed URIs
src/lib/routes/types.ts CommonNavigatorParams and AllNavigatorParams — the canonical type map of all routes Every time you add a new screen
src/state/session/index.ts Authentication state, AT Protocol agent, createAccount, login, resumeSession Auth flow changes, session management
src/state/queries/post-feed.ts usePostFeedQuery, FEED_RQKEY, FeedDescriptor — the core feed data layer Feed behavior changes, new feed types
src/state/queries/preferences/index.ts usePreferencesQuery, useSetFeedViewPreferencesMutation — user preferences Adding new user preferences
src/state/shell/index.ts Global shell state — minimal mode, active starter pack, logged-out state Shell-level UI changes
src/alf/atoms.ts Atomic style tokens (a.flex_1, a.px_xl, etc.) Adding new design tokens
src/alf/themes/ Light, dim, dark theme color definitions Changing theme colors
src/components/Layout.tsx Layout.Screen, Layout.Header.*, Layout.Content — standard screen shell Every new screen
src/components/Button.tsx Shared Button, ButtonText, ButtonIcon components Button behavior or style changes
src/components/forms/Toggle.tsx Toggle.Group, Toggle.Item, Toggle.Platform — checkbox/radio controls Settings toggle changes
src/navigation/ (inferred) Navigator definitions — root, auth stack, main tabs, nested stacks Adding new screens to a navigator

Naming Conventions

What Convention Example
Screen files PascalCase, descriptive MessagesInboxScreen.tsx, ProfileHeaderStandard.tsx
Component files PascalCase ChatListItem.tsx, WizardProfileCard.tsx
Hook files camelCase, use prefix usePostFeedQuery.ts, useWizardState.ts
Query key factories RQKEY suffix or create*QueryKey FEED_RQKEY(feed), createGetSuggestedFeedsQueryKey()
Route keys in CommonNavigatorParams PascalCase string literal 'MessagesConversation', 'ProfileFollowers'
AT Protocol lexicon namespaces Dot-separated, as defined by AT Protocol app.bsky.feed.post, com.atproto.server.createSession
Constants SCREAMING_SNAKE_CASE DISCOVER_FEED_URI, MAX_LABELERS, MESSAGE_SCREEN_POLL_INTERVAL
Design system atoms a. prefix, snake_case a.flex_1, a.px_xl, a.rounded_full
Theme tokens t.atoms.* or t.palette.* t.atoms.text_contrast_medium, t.palette.primary_500
Platform-specific files .ios.tsx, .android.tsx, .web.tsx StatusBarShadow.web.tsx, CaptchaWebView.web.tsx
Internal path alias #/ prefix #/state/session, #/components/Button, #/alf

5. Architecture & Key Decisions [Explanation]

React Native Framework Choice

What it is: This app uses Expo's bare workflow — Expo manages the native project configuration and provides a curated set of native modules, but the ios/ and android/ directories are fully committed and customizable.

Why it was chosen: The bare workflow gives the team access to custom native modules (the modules/ directory contains several first-party native modules like expo-bluesky-gif-view and expo-emoji-picker) while still benefiting from Expo's tooling (expo prebuild, EAS Build, OTA updates via expo-updates).

How it affects your work: You can use any React Native native module, but adding one requires running npx expo prebuild (or manually editing the native projects) and rebuilding the app. You cannot use Expo Go for development — you must use a development build.

Where to see it: app.config.ts, modules/ directory, ios/ and android/ directories.


What it is: React Navigation with native stack navigators and tab navigators. The canonical type map for all routes is CommonNavigatorParams and AllNavigatorParams in src/lib/routes/types.ts.

Why it was chosen: React Navigation is the standard for React Native apps. The native stack navigator provides native-feeling transitions on iOS and Android.

Navigator hierarchy (inferred from route types and screen documentation):

Root Navigator
├── Logged-out shell (auth flow)
│   ├── Splash
│   ├── Login Form
│   ├── Signup (multi-step wizard)
│   ├── Deactivated
│   ├── Takendown
│   └── Signup Queued
└── Authenticated shell
    ├── Main Tab Navigator
    │   ├── Home Tab (feed stack)
    │   ├── Search Tab (explore/search stack)
    │   ├── Notifications Tab
    │   ├── Messages Tab (chat list stack)
    │   └── Profile Tab
    └── Common Navigator (modal/push screens)
        ├── Profile screens
        ├── Post thread screens
        ├── Settings screens
        ├── Starter Pack screens
        ├── Onboarding flow
        └── ... (all other screens)

(inferred from CommonNavigatorParams, AllNavigatorParams, and screen documentation — verify the exact navigator structure against the navigation configuration files)

The ParamList type as the architectural contract: CommonNavigatorParams and AllNavigatorParams are the single source of truth for what screens exist and what parameters they accept. Every new screen must be registered here. TypeScript will catch navigation calls to unregistered screens at compile time.

How it affects your work: When you add a new screen, you must add it to the appropriate ParamList type before you can navigate to it. When you navigate to a screen, TypeScript enforces that you pass the correct parameters.

Where to see it: src/lib/routes/types.ts, any screen file that uses NativeStackScreenProps<CommonNavigatorParams, 'ScreenName'>.


State Management Strategy

What it is: A layered approach:

Why it was chosen: TanStack Query handles the complexity of caching, deduplication, background refetching, and pagination for server data. Local state machines handle complex multi-step flows. This separation keeps components focused on rendering.

How it affects your work: When you need data from the Bluesky API, look for an existing query hook in src/state/queries/ before writing a new one. When you need to mutate data, use the corresponding mutation hook. Do not fetch() directly in components.

Where to see it: src/state/queries/post-feed.ts (feed queries), src/state/queries/profile.ts (profile queries), src/state/session/ (auth).


Data Fetching Pattern

What it is: TanStack Query infinite queries for paginated lists, regular queries for single resources, and mutations for writes. All queries are defined as custom hooks in src/state/queries/.

Why it was chosen: TanStack Query provides automatic caching, background refetching, deduplication, and optimistic update support without boilerplate.

Key patterns:

How it affects your work: Always use the existing query hooks. If you need a new endpoint, create a new hook in src/state/queries/. Use queryClient.invalidateQueries({ queryKey: RQKEY(...) }) to refresh data after mutations.

Where to see it: src/state/queries/post-feed.ts, src/state/queries/profile.ts, src/state/queries/actor-search.ts.


Authentication and Authorization Approach

What it is: AT Protocol session-based authentication. The agent (an AtpAgent instance from @atproto/api) carries the authenticated session token and is used for all API calls. Sessions are persisted to device storage and resumed on app launch.

Token storage: Session tokens are stored securely via the session state module. The accessJwt is used for API authentication.

Auth state propagation: useSession() provides currentAccount (the active account) and hasSession (boolean). The shell renders either the logged-out flow or the authenticated app based on session state.

Protected-route pattern: Screens that require authentication rely on the navigator's auth guard (not on per-screen checks). The useRequireAuth() hook wraps actions that require authentication — if the user is not logged in, it redirects to the sign-in flow before executing the action.

How it affects your work: Use useSession() to read auth state. Use useAgent() to get the AT Protocol agent for API calls. Use useRequireAuth() to gate user actions behind authentication. Do not store tokens in component state.

Where to see it: src/state/session/, src/lib/hooks/useRequireAuth.ts (inferred), any screen that calls useSession().


Platform Branching Strategy

What it is: Multiple mechanisms for handling iOS/Android/web differences:

  1. Platform.OS conditionals: IS_IOS, IS_ANDROID, IS_NATIVE, IS_WEB constants from src/env.ts (which wrap Platform.OS).
  2. platform() utility from #/alf: Returns different values for native vs. web — used in style definitions.
  3. web() utility from #/alf: Applies styles only on web.
  4. native() utility from #/alf: Applies styles only on native.
  5. Platform-specific files: .ios.tsx, .android.tsx, .web.tsx — Metro resolves these automatically. Used for components with fundamentally different implementations (e.g., CaptchaWebView.web.tsx, StatusBarShadow.web.tsx).
  6. IS_LIQUID_GLASS: A flag for the newer iOS design language variant.

Convention in this codebase: Use IS_NATIVE, IS_IOS, IS_ANDROID, IS_WEB constants (not Platform.OS directly) for consistency. Use .web.tsx file splits for components that are entirely different on web (e.g., web stubs that throw or return null).

How it affects your work: When writing platform-specific code, prefer the IS_* constants. For components that need fundamentally different implementations per platform, create platform-specific files. Check for existing .web.tsx stubs before assuming a component works on web.

Where to see it: src/env.ts, src/view/com/pager/draggable-scroll/DraggableScrollView.tsx (web-only drag scroll), src/signup/step-captcha/CaptchaWebView.web.tsx.


UI Component Strategy

What it is: A two-layer component system:

Design system atoms (#/alf): Atomic style tokens accessed as a.flex_1, a.px_xl, a.rounded_full, etc. These are the building blocks for all layout and typography.

Theme tokens: t.atoms.* (semantic, theme-aware) and t.palette.* (raw color values). Always use t.atoms.* for colors that should adapt to light/dark/dim themes.

How it affects your work: When building new UI, use components from src/components/ and atoms from #/alf. If you see a @deprecated import, replace it with the new equivalent. Do not add new components to src/view/com/util/.

Where to see it: src/components/Button.tsx, src/components/Layout.tsx, src/alf/atoms.ts.


Background and Async Processing Patterns

What it is: The messaging feature uses a polling-based event bus (useMessagesEventBus) rather than WebSockets. Screens that display messages request a poll interval when focused and active.

Feed polling: The home feed polls for new content every 60 seconds via setInterval in PostFeed. The Discover feed always reports hasNew = true when polled (algorithmic content is always fresh).

Optimistic updates: Profile follow/block mutations use a shadow cache (useProfileShadow) to reflect changes immediately. Like counts on labeler profiles are updated optimistically in local state.

Background sync: No service workers or background fetch are used. All data refresh is triggered by user interaction (pull-to-refresh, screen focus) or the polling intervals described above.

Where to see it: src/state/messages/ (inferred), src/view/com/posts/PostFeed.tsx, src/state/cache/profile-shadow.ts.


6. Development Patterns & Conventions [Reference + Explanation]

Pattern: Creating a New Screen

Type: Screen Pattern

When to use: Every time you add a new navigable screen to the app.

The pattern:

// 1. Register the route in src/lib/routes/types.ts
export type CommonNavigatorParams = {
  // ... existing routes ...
  MyNewScreen: { someParam: string; optionalParam?: number }
}

// 2. Create the screen component file
// src/screens/MyFeature/MyNewScreen.tsx
import React, { useCallback } from 'react'
import { View } from 'react-native'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { useFocusEffect } from '@react-navigation/native'
import { Trans } from '@lingui/react/macro'

import { CommonNavigatorParams } from '#/lib/routes/types'
import { useSetMinimalShellMode } from '#/state/shell'
import { Layout } from '#/components/Layout'
import { Text } from '#/components/Typography'
import { atoms as a } from '#/alf'

type Props = NativeStackScreenProps<CommonNavigatorParams, 'MyNewScreen'>

export function MyNewScreen({ route }: Props) {
  const { someParam } = route.params
  const setMinimalShellMode = useSetMinimalShellMode()

  // Ensure full shell is visible when this screen is focused
  useFocusEffect(
    useCallback(() => {
      setMinimalShellMode(false)
    }, [setMinimalShellMode]),
  )

  return (
    <Layout.Screen testID="myNewScreen">
      <Layout.Header.Outer>
        <Layout.Header.BackButton />
        <Layout.Header.Content>
          <Layout.Header.TitleText>
            <Trans>My New Screen</Trans>
          </Layout.Header.TitleText>
        </Layout.Header.Content>
        <Layout.Header.Slot />
      </Layout.Header.Outer>
      <Layout.Content>
        {/* Screen content here */}
        <Text>{someParam}</Text>
      </Layout.Content>
    </Layout.Screen>
  )
}

// 3. Register the screen in the navigator definition
// (in the appropriate navigator file)

Key rules:

Example in the codebase: src/screens/Settings/AccountSettings.tsx, src/post/PostLikedBy.tsx.


Pattern: Adding a New API Call (Query Hook)

Type: Data Fetching Pattern

When to use: When you need to fetch data from the Bluesky AT Protocol API that doesn't already have a hook.

The pattern:

// src/state/queries/my-feature.ts
import { useQuery, useInfiniteQuery, useMutation } from '@tanstack/react-query'
import { useAgent } from '#/state/session'

// Query key factory — used for cache targeting and invalidation
export const RQKEY = (param: string) => ['my-feature', param]

// Regular query
export function useMyFeatureQuery(param: string) {
  const agent = useAgent()
  return useQuery({
    queryKey: RQKEY(param),
    queryFn: async () => {
      const res = await agent.app.bsky.someNamespace.someMethod({ param })
      return res.data
    },
  })
}

// Infinite (paginated) query
export function useMyFeatureInfiniteQuery(param: string) {
  const agent = useAgent()
  return useInfiniteQuery({
    queryKey: RQKEY(param),
    queryFn: async ({ pageParam }) => {
      const res = await agent.app.bsky.someNamespace.someMethod({
        param,
        cursor: pageParam,
        limit: 25,
      })
      return res.data
    },
    initialPageParam: undefined as string | undefined,
    getNextPageParam: lastPage => lastPage.cursor,
  })
}

// Mutation
export function useMyFeatureMutation() {
  const agent = useAgent()
  return useMutation({
    mutationFn: async (input: { id: string }) => {
      await agent.app.bsky.someNamespace.someAction(input)
    },
    onSuccess: () => {
      // Invalidate related queries
    },
  })
}

Key rules:

Example in the codebase: src/state/queries/post-feed.ts, src/state/queries/profile.ts, src/state/queries/actor-search.ts.


Pattern: Navigator Registration

Type: Navigation Pattern

When to use: When adding a new screen to an existing navigator.

The pattern:

// Step 1: Add to the ParamList type in src/lib/routes/types.ts
export type CommonNavigatorParams = {
  // ... existing routes ...
  MyNewScreen: { param: string }
}

// Step 2: Add to the navigator definition (in the appropriate navigator file)
// Example for a native stack navigator:
<Stack.Screen
  name="MyNewScreen"
  component={MyNewScreen}
  options={{ headerShown: false }}
/>

// Step 3: Navigate to it from another screen
navigation.navigate('MyNewScreen', { param: 'value' })

// Step 4: Use typed props in the screen component
type Props = NativeStackScreenProps<CommonNavigatorParams, 'MyNewScreen'>
export function MyNewScreen({ route, navigation }: Props) {
  const { param } = route.params
  // ...
}

Key rules:

Example in the codebase: src/lib/routes/types.ts (ParamList), any navigator definition file.


Pattern: Platform Branching

Type: Platform Pattern

When to use: When behavior or styling must differ between iOS, Android, and web.

The pattern:

import { IS_IOS, IS_ANDROID, IS_NATIVE, IS_WEB } from '#/env'
import { platform, web, native } from '#/alf'

// Style-level branching using alf utilities
const styles = {
  container: [
    a.flex_1,
    web({ maxWidth: 600 }),           // only on web
    native({ paddingBottom: 20 }),    // only on native
    platform({ ios: a.pt_xl, android: a.pt_md }),  // per-platform
  ],
}

// Logic-level branching using constants
if (IS_IOS) {
  // iOS-specific behavior
}

if (IS_NATIVE) {
  // Both iOS and Android
}

// Platform-specific file split (Metro resolves automatically):
// MyComponent.tsx        — default (native)
// MyComponent.web.tsx    — web only
// MyComponent.ios.tsx    — iOS only
// MyComponent.android.tsx — Android only

Key rules:

Example in the codebase: src/view/com/pager/DraggableScrollView.tsx + DraggableScrollView.web.tsx, src/profile/header/StatusBarShadow.tsx + StatusBarShadow.web.tsx.


Pattern: Form Handling and Validation

Type: Form Pattern

When to use: When collecting user input that requires validation before submission.

The pattern:

import { useState } from 'react'
import { useLingui } from '@lingui/react'
import { msg } from '@lingui/core/macro'
import { isOverMaxGraphemeCount } from '#/lib/strings/grapheme'
import { TextField } from '#/components/forms/TextField'
import { Button, ButtonText } from '#/components/Button'

export function MyForm() {
  const { _ } = useLingui()
  const [value, setValue] = useState('')
  const [isSubmitting, setIsSubmitting] = useState(false)

  const isTooLong = isOverMaxGraphemeCount({ text: value, maxCount: 50 })

  const onSubmit = async () => {
    if (!value || isTooLong) return
    setIsSubmitting(true)
    try {
      await someApiCall(value.trimEnd())
    } finally {
      setIsSubmitting(false)
    }
  }

  return (
    <>
      <TextField.Root isInvalid={isTooLong}>
        <TextField.LabelText>{_(msg`My Field`)}</TextField.LabelText>
        <TextField.Input
          value={value}
          onChangeText={setValue}
          label={_(msg`Enter a value`)}
        />
        <TextField.SuffixText
          label={`${value.length} out of 50`}>
          {value.length}/50
        </TextField.SuffixText>
      </TextField.Root>
      <Button
        label={_(msg`Submit`)}
        onPress={onSubmit}
        disabled={!value || isTooLong || isSubmitting}>
        <ButtonText>Submit</ButtonText>
      </Button>
    </>
  )
}

Key rules:

Example in the codebase: src/screens/Settings/AccountSettings.tsx (ChangeHandleDialog), src/login/SetNewPasswordForm.tsx.


Pattern: AsyncStorage Read/Write Lifecycle

Type: Data Fetching Pattern

When to use: When persisting user preferences or settings to device storage.

The pattern:

// Preferences are managed through the #/state/preferences layer.
// Do NOT access AsyncStorage directly in components.

// Reading preferences:
import { useLanguagePrefs } from '#/state/preferences'
const { appLanguage, contentLanguages } = useLanguagePrefs()

// Writing preferences:
import { useLanguagePrefsApi } from '#/state/preferences'
const setLangPrefs = useLanguagePrefsApi()
setLangPrefs.setAppLanguage('en')

// For search history and similar per-account storage:
import { useStorage } from '#/state/storage' // (inferred pattern)
const [history, setHistory] = useStorage(account, [did, 'searchTermHistory'])

Key rules:

Example in the codebase: src/state/preferences/, src/screens/Search/SearchScreenShell.tsx (search history).


Pattern: Error Handling in Screen Components

Type: Screen Pattern

When to use: When a screen needs to display an error state from a failed API call.

The pattern:

import { ListMaybePlaceholder } from '#/view/com/util/List'
import { cleanError } from '#/lib/strings/errors'

export function MyScreen() {
  const { data, isLoading, isError, error, refetch } = useMyQuery()

  if (isLoading || isError || !data) {
    return (
      <ListMaybePlaceholder
        isLoading={isLoading}
        isError={isError}
        onRetry={refetch}
        errorMessage={cleanError(error)}
      />
    )
  }

  return <MyContent data={data} />
}

Key rules:

Example in the codebase: src/screens/Settings/ActivityNotificationSettings.tsx, src/notifications/ActivityList.tsx.


Pattern: Error Handling in Hook/Service Layer

Type: Data Fetching Pattern

When to use: When writing query hooks or mutation hooks that need to handle and report errors.

The pattern:

import { logger } from '#/logger'
import { useAnalytics } from '#/analytics'
import { cleanError } from '#/lib/strings/errors'
import { isNetworkError } from '#/lib/strings/errors'
import Toast from '#/view/com/util/Toast'

export function useMyMutation() {
  const { ax } = useAnalytics()
  return useMutation({
    mutationFn: async (input) => {
      await agent.someMethod(input)
    },
    onError: (e: any) => {
      if (isNetworkError(e)) {
        Toast.show('Check your internet connection', { type: 'error' })
      } else {
        logger.error('Failed to do the thing', { safeMessage: e.message })
        Toast.show(cleanError(e), { type: 'error' })
      }
    },
  })
}

Key rules:

Example in the codebase: src/screens/StarterPack/StarterPackScreen.tsx (onFollowAll), src/profile/header/ProfileHeaderLabeler.tsx.


7. Common Development Tasks [How-to]

How to: Add a New Screen

When: You need to create a new navigable screen in the app.

Steps:

  1. Add the route to the ParamList type in src/lib/routes/types.ts:

    export type CommonNavigatorParams = {
      // ... existing routes ...
      MyNewScreen: { param: string; optionalParam?: number }
    }
  2. Create the screen component file (e.g., src/screens/MyFeature/MyNewScreen.tsx):

    import { NativeStackScreenProps } from '@react-navigation/native-stack'
    import { CommonNavigatorParams } from '#/lib/routes/types'
    // ... (see Screen Pattern in Section 6)
  3. Register the screen in the navigator definition. Find the appropriate navigator file and add a Stack.Screen entry.

  4. Navigate to the screen from another screen:

    navigation.navigate('MyNewScreen', { param: 'value' })
  5. Test on both iOS Simulator and Android Emulator.

Watch out for: Forgetting to add the route to CommonNavigatorParams — you'll get a TypeScript error when trying to navigate. If you see "The action 'NAVIGATE' with payload was not handled by any navigator" at runtime, the screen is not registered in the navigator definition.


How to: Add a New Navigator

When: You need a new tab, stack, or drawer navigator for a new feature area.

Steps:

  1. Create the navigator component file (e.g., src/navigation/MyFeatureNavigator.tsx).
  2. Define the navigator using createNativeStackNavigator() or createBottomTabNavigator().
  3. Add the navigator's screens to the appropriate ParamList type.
  4. Nest the navigator inside the parent navigator.
  5. Update AllNavigatorParams if the new navigator introduces new screen names.

Watch out for: Nested navigators can cause issues with the back button and deep linking. Test back navigation thoroughly on both platforms.


How to: Add a New API Endpoint / Hook

When: You need to call a Bluesky AT Protocol API endpoint that doesn't have an existing hook.

Steps:

  1. Find the AT Protocol lexicon method in @atproto/api (e.g., agent.app.bsky.graph.getList).
  2. Create a new hook file in src/state/queries/ (or add to an existing file if closely related).
  3. Define a RQKEY factory for the query key.
  4. Implement the hook using useQuery, useInfiniteQuery, or useMutation.
  5. Export the hook and use it in your screen component.

Watch out for: Always use useAgent() to get the agent — never import it directly. Always handle the isLoading, isError, and data states in the consuming component.


How to: Add a New Form with Validation

When: You need to collect user input with validation before submitting to the API.

Steps:

  1. Use useState for controlled inputs.
  2. Use isOverMaxGraphemeCount for character limit validation.
  3. Use TextField.Root, TextField.Input, TextField.LabelText from #/components/forms/TextField.
  4. Disable the submit button when isSubmitting || isInvalid.
  5. Call value.trimEnd() before submitting.
  6. Handle errors with cleanError(e) and display via ErrorMessage or Admonition.

Watch out for: Using .length for character limits — this counts UTF-16 code units, not grapheme clusters. Emoji and some Unicode characters will be miscounted. Always use isOverMaxGraphemeCount.


When: You need a new screen to be reachable via a URL (e.g., bsky.app/profile/alice).

Steps:

  1. Add the screen to the appropriate ParamList type.
  2. Add a linking configuration entry in the app's linking config (inferred to be in the navigation setup file).
  3. Test the deep link by opening the URL on a device.

[Not documented — WHO: Team Lead; WHAT: Where is the deep linking configuration defined (likely in the root navigator or app.config.ts)?; WHERE: Add the specific file path and configuration pattern here.]


How to: Run Tests

When: Before submitting a pull request or verifying a change.

Steps:

# Run unit tests
yarn test

# Run tests in watch mode
yarn test --watch

# Run a specific test file
yarn test src/state/queries/__tests__/utils.test.ts

(inferred from standard React Native/Expo project conventions — verify against package.json scripts)

Watch out for: The project uses Jest for unit tests. E2E tests (if configured) would use Detox or Maestro. Check package.json for the exact test scripts.

[Not documented — WHO: Team Lead; WHAT: What test frameworks are configured (Jest, Detox, Maestro)? What is the command to run E2E tests?; WHERE: Add the E2E test commands here.]


How to: Debug a Failing API Call

When: An API call is returning an error or unexpected data.

Steps:

  1. Check the Metro terminal for any JavaScript errors.
  2. Use Flipper's Network Inspector (if configured) to inspect the raw HTTP request and response.
  3. Add a temporary console.log in the query function to log the response:
    const res = await agent.app.bsky.someMethod(params)
    console.log('API response:', JSON.stringify(res.data, null, 2))
  4. Check the AT Protocol lexicon in @atproto/api to verify the expected request/response shape.
  5. Verify the agent has a valid session by checking agent.session is not null.
  6. Check cleanError(e) to see the user-facing error message.

Watch out for: AT Protocol errors often come as string messages in e.message rather than HTTP status codes. Use isNetworkError(e) to distinguish network failures from API errors.


How to: Add a New Native Package

When: You need to use a React Native library that has native code (iOS/Android).

Steps:

  1. Install the package:

    yarn add react-native-some-package
  2. If it's an Expo module, add it to app.config.ts plugins:

    plugins: [
      // ... existing plugins ...
      'react-native-some-package',
    ]
  3. Run Expo prebuild to regenerate native code:

    npx expo prebuild --clean
  4. Install CocoaPods (iOS):

    cd ios && bundle exec pod install && cd ..
  5. Rebuild the app (a full native rebuild is required — r in Metro won't work):

    yarn ios   # or yarn android

Watch out for: After adding a native package, you must do a full native rebuild. Pressing r in Metro only reloads the JavaScript bundle — it does not rebuild native code. If you see "Native module X not found", you forgot to rebuild.


How to: Handle a Platform-Specific Difference

When: A feature behaves differently on iOS vs. Android vs. web.

Steps:

  1. For style differences: Use platform(), web(), or native() from #/alf:

    style={[a.flex_1, web({ maxWidth: 600 }), platform({ ios: a.pt_xl, android: a.pt_md })]}
  2. For logic differences: Use IS_IOS, IS_ANDROID, IS_NATIVE, IS_WEB from #/env:

    if (IS_IOS) { /* iOS-specific behavior */ }
  3. For fundamentally different implementations: Create platform-specific files:

  4. Test on both platforms before submitting.

Watch out for: The .web.tsx file split is resolved by Metro at build time. If you create MyComponent.web.tsx, Metro will use it for web builds and MyComponent.tsx for native builds. Make sure both files export the same interface.


8. Debugging & Troubleshooting [How-to]

Common Error: Metro Module Resolution Failure

Symptoms: Red screen with "Unable to resolve module X from Y" or "Module not found".

Cause: A native package was added but not linked, or node_modules is out of sync after a git pull.

Fix:

  1. yarn install — reinstall all packages.
  2. cd ios && bundle exec pod install && cd .. — reinstall iOS pods.
  3. yarn start --reset-cache — clear Metro's module cache.
  4. If still failing, do a full native rebuild: yarn ios or yarn android.

Common Error: iOS Build Failure (CocoaPods)

Symptoms: Xcode build fails with "No such module" or "Unable to find a specification for...".

Cause: CocoaPods cache is stale, or a new native dependency was added without running pod install.

Fix:

  1. cd ios && bundle exec pod install && cd ..
  2. If that fails: pod repo update && bundle exec pod install
  3. If still failing: cd ios && bundle exec pod install --repo-update && cd ..
  4. Clean Xcode build folder: Xcode → Product → Clean Build Folder, then rebuild.

Common Error: Android Build Failure (Gradle)

Symptoms: Gradle build failed with errors about SDK version, JDK, or NDK.

Cause: ANDROID_HOME or JAVA_HOME not set correctly, or wrong SDK/JDK version.

Fix:

  1. Verify ANDROID_HOME points to your Android SDK: echo $ANDROID_HOME
  2. Verify JAVA_HOME points to JDK 17: echo $JAVA_HOME
  3. Check android/build.gradle for the required compileSdkVersion and targetSdkVersion.
  4. Install the required SDK version via Android Studio → SDK Manager.
  5. Try: cd android && ./gradlew clean && cd .., then rebuild.

Common Error: Navigation Error — Screen Not Handled

Symptoms: "The action 'NAVIGATE' with payload {name: 'MyScreen', params: {...}} was not handled by any navigator."

Cause: The screen is registered in CommonNavigatorParams (TypeScript type) but not in the actual navigator definition (the Stack.Screen component).

Fix:

  1. Find the navigator that should contain your screen.
  2. Add <Stack.Screen name="MyScreen" component={MyScreen} /> to that navigator.
  3. Verify the name prop exactly matches the key in CommonNavigatorParams.

Common Error: Platform-Specific Crash

Symptoms: App crashes on iOS but not Android (or vice versa), or behavior is different between platforms.

Cause: Platform-specific API usage, missing Platform.OS check, or a native module that behaves differently.

Fix:

  1. Check if the code uses any iOS-only or Android-only APIs without a platform guard.
  2. Add IS_IOS / IS_ANDROID guards from #/env.
  3. Check if there's a .ios.tsx or .android.tsx file that should be created.
  4. Test on both platforms before submitting.

Common Error: AsyncStorage Hydration Race Condition

Symptoms: Preferences or settings appear as default values on first render, then "jump" to the correct values after a brief delay.

Cause: The preferences state is loaded asynchronously from storage. Components that read preferences before hydration completes see default values.

Fix:

  1. Check if the preference hook returns undefined during loading (e.g., langPrefs.appLanguage may be undefined before hydration).
  2. Add a loading guard: if (!preferences) return <Loader />
  3. Use ?? defaultValue to provide safe fallbacks.
  4. Do not store preferences in local useState — always read from the preference hooks.

Symptoms: Tapping a deep link opens the app but navigates to the wrong screen or shows an error.

Cause: The linking configuration prefix doesn't match the URL, or the screen is not registered in the linking config.

Fix:

  1. Check the linking configuration (inferred to be in the navigation setup or app.config.ts).
  2. Verify the URL prefix matches the app's scheme.
  3. Verify the screen name in the linking config matches the CommonNavigatorParams key exactly.
  4. Test with npx uri-scheme open "bsky://..." --ios or adb shell am start -a android.intent.action.VIEW -d "bsky://...".

[Not documented — WHO: Team Lead; WHAT: Where is the deep linking configuration defined and what is the URL scheme?; WHERE: Add the specific file path and URL scheme here.]


Common Error: expo prebuild Generates Unexpected Native Code

Symptoms: After running expo prebuild, the ios/ or android/ directories have unexpected changes.

Cause: app.config.ts or a plugin configuration changed, causing prebuild to regenerate native code differently.

Fix:

  1. Review the changes in ios/ and android/ to understand what changed.
  2. If the changes are expected (you added a plugin), commit them.
  3. If the changes are unexpected, check app.config.ts for unintended modifications.
  4. Run npx expo prebuild --clean to start fresh from the current config.

9. Gotchas & Pitfalls [Reference]

# Gotcha Why It Matters What To Do Instead
1 useEffect runs twice in React StrictMode In development, React StrictMode mounts components twice to detect side effects. This can cause double API calls or duplicate state updates. Use cleanup functions in useEffect. Don't rely on effects running exactly once.
2 FlatList re-renders all items when data reference changes Passing a new array reference on every render causes all list items to re-render, even if the data is the same. Memoize the data array with useMemo. Use stable keyExtractor functions.
3 Animated.Value not resetting on screen unmount If you create an Animated.Value in a component and the component unmounts, the value persists if referenced elsewhere. Use useRef for animated values. Clean up animations in useEffect cleanup.
4 BackHandler not cleaned up on Android Registering a BackHandler listener without removing it on unmount causes memory leaks and unexpected back behavior. Always return a cleanup function from useEffect that calls backHandler.remove().
5 navigation.navigate vs. navigation.push navigate reuses an existing screen in the stack if it exists; push always adds a new one. Using navigate when you want a fresh screen can cause stale state. Use navigation.push when you always want a new screen instance. Use navigate for tab-level navigation.
6 AsyncStorage is async but feels synchronous Missing await on AsyncStorage.getItem() returns a Promise, not the value. This causes stale reads. Always await AsyncStorage calls. Better: use the #/state/preferences abstraction which handles this.
7 CocoaPods out of sync after yarn install Adding a native package with yarn add does not automatically run pod install. The iOS build will fail. Always run cd ios && bundle exec pod install && cd .. after adding any native package.
8 Platform.OS === 'web' is true in Expo Web Code that checks Platform.OS !== 'web' to detect native will incorrectly include web. Use IS_NATIVE from #/env (which is false on web) instead of Platform.OS !== 'web'.
9 Hot reload does not reset navigation state After changing navigator structure or screen params, the app may be in an inconsistent navigation state. Kill the app and restart for deep navigation testing. Use yarn start --reset-cache if Metro is confused.
10 @deprecated components in src/view/com/util/ Many components in the legacy location are marked @deprecated. Using them adds technical debt. Use the new equivalents in src/components/. Check the deprecation comment for the replacement.
11 Character limits use grapheme count, not .length .length counts UTF-16 code units. A single emoji can be 2+ code units. Using .length for limits will allow more characters than intended. Use isOverMaxGraphemeCount({ text, maxCount }) from #/lib/strings/grapheme.
12 staleTime: 0 means data refetches on every focus TanStack Query's default staleTime is 0, so data is immediately stale and will refetch on window focus. This can cause unexpected loading states. Set staleTime: Infinity for data that doesn't change (e.g., static assets). Use staleTime: STALE.MINUTES.FIVE for semi-static data.
13 Swipe-to-delete gesture conflicts with scroll on Android The GestureActionView swipe pattern has known issues on Android with nested scroll views. Test swipe gestures on Android specifically. Check for IS_ANDROID guards in gesture components.
14 Portal in SettingsList.Group affects DOM order The Portal pattern teleports ItemIcon and ItemText to a different visual position. Screen readers may traverse them in DOM order, not visual order. Test settings screens with VoiceOver/TalkBack. Add explicit accessibilityLabel props where needed.
15 expo prebuild --clean deletes manual native changes If you've manually edited ios/ or android/ files, --clean will overwrite them. Use Expo config plugins for native modifications instead of manual edits. Document any manual changes.

10. First-Task Checklist [Tutorial]

This tutorial walks you through adding a new informational screen reachable from the Settings section. It exercises the most common development patterns: ParamList registration, navigator update, screen component structure, data fetching, and cross-platform testing.

The Task

Add a new "App Info" screen that displays the app version, build date, and a link to the Bluesky status page. It should be reachable from the main Settings screen.

What You'll Learn


Step 1: Register the Route

Open src/lib/routes/types.ts and add your new screen to CommonNavigatorParams:

export type CommonNavigatorParams = {
  // ... existing routes ...
  AppInfo: undefined  // no params needed
}

Expected result: TypeScript now knows about 'AppInfo' as a valid route name. You can navigate to it (though it will fail at runtime until you complete the next steps).


Step 2: Create the Screen Component

Create src/screens/Settings/AppInfoScreen.tsx:

import React, { useCallback } from 'react'
import { View } from 'react-native'
import { NativeStackScreenProps } from '@react-navigation/native-stack'
import { useFocusEffect } from '@react-navigation/native'
import { Trans } from '@lingui/react/macro'

import { CommonNavigatorParams } from '#/lib/routes/types'
import { useSetMinimalShellMode } from '#/state/shell'
import { Layout } from '#/components/Layout'
import { Text } from '#/components/Typography'
import { atoms as a } from '#/alf'
import { STATUS_PAGE_URL } from '#/lib/constants'
import { InlineLinkText } from '#/components/Link'

type Props = NativeStackScreenProps<CommonNavigatorParams, 'AppInfo'>

export function AppInfoScreen({}: Props) {
  const setMinimalShellMode = useSetMinimalShellMode()

  useFocusEffect(
    useCallback(() => {
      setMinimalShellMode(false)
    }, [setMinimalShellMode]),
  )

  return (
    <Layout.Screen testID="appInfoScreen">
      <Layout.Header.Outer>
        <Layout.Header.BackButton />
        <Layout.Header.Content>
          <Layout.Header.TitleText>
            <Trans>App Info</Trans>
          </Layout.Header.TitleText>
        </Layout.Header.Content>
        <Layout.Header.Slot />
      </Layout.Header.Outer>
      <Layout.Content>
        <View style={[a.px_xl, a.py_lg, a.gap_md]}>
          <Text style={[a.text_md]}>
            <Trans>Status page:</Trans>{' '}
            <InlineLinkText to={STATUS_PAGE_URL} label="Bluesky status page">
              {STATUS_PAGE_URL}
            </InlineLinkText>
          </Text>
        </View>
      </Layout.Content>
    </Layout.Screen>
  )
}

Expected result: The screen component exists and compiles without errors.


Step 3: Add the Screen to the Navigator

Find the Settings navigator definition file. Add your screen:

import { AppInfoScreen } from '#/screens/Settings/AppInfoScreen'

// Inside the navigator:
<Stack.Screen name="AppInfo" component={AppInfoScreen} />

[Not documented — WHO: Team Lead; WHAT: Which file contains the Settings navigator definition?; WHERE: Replace this step with the specific file path.]

Expected result: The navigator now knows how to render AppInfoScreen when 'AppInfo' is navigated to.


In the main Settings screen (likely src/screens/Settings/index.tsx or similar), add a link to your new screen:

import { useNavigation } from '@react-navigation/native'

// Inside the component:
const navigation = useNavigation()

// In the JSX:
<SettingsList.LinkItem
  to={{ screen: 'AppInfo' }}
  label={_(msg`App Info`)}>
  <SettingsList.ItemText>
    <Trans>App Info</Trans>
  </SettingsList.ItemText>
</SettingsList.LinkItem>

[Not documented — WHO: Team Lead; WHAT: Which file is the main Settings screen and where should the new link be added?; WHERE: Replace this step with the specific file path and location within the settings list.]


Step 5: Test on Both Platforms

iOS Simulator:

  1. Run yarn ios (or press i in Metro if already running).
  2. Navigate to Settings.
  3. Tap "App Info".
  4. Verify the screen renders with the correct title and status page link.

Android Emulator:

  1. Run yarn android (or press a in Metro if already running).
  2. Repeat the same navigation steps.
  3. Verify the screen renders identically.

[Screenshot: AppInfo screen on iOS Simulator showing the status page link]


Verification

Your task is complete when:


What to Do Next

Once you've completed this task, try these more complex exercises:

  1. Add a data-fetching hook: Modify AppInfoScreen to fetch the current app version from a query hook instead of a constant.
  2. Add a setting toggle: Add a new boolean preference to the Settings screen using Toggle.Item and usePreferencesQuery.
  3. Add a new tab: Create a new tab in the main tab navigator with its own stack of screens.

11. Glossary

Technologies

Term Definition
AT Protocol (atproto) The open, decentralized social networking protocol developed by Bluesky. Defines lexicons (API schemas), DIDs, handles, and the data model for all Bluesky content. All API calls in this app use AT Protocol.
@atproto/api The official AT Protocol JavaScript/TypeScript client SDK. Provides the AtpAgent class, all lexicon type definitions, and moderateProfile().
XRPC Cross-service Remote Procedure Call — the HTTP-based RPC mechanism used by AT Protocol. Queries (reads) use GET; procedures (writes) use POST.
PDS (Personal Data Server) The server that hosts a user's AT Protocol repository — their posts, follows, profile, and other records.
AppView The Bluesky-specific aggregation layer that indexes AT Protocol data and serves it via the app.bsky.* API namespace.
Expo (bare workflow) A React Native framework that provides native module management, build tooling (EAS), and OTA updates. The bare workflow gives full access to native code while using Expo's tooling.
EAS (Expo Application Services) Expo's cloud build and deployment service. Used for creating production builds and OTA updates.
Metro The JavaScript bundler used by React Native and Expo. Handles module resolution, asset bundling, and hot reloading.
TanStack Query (React Query v5) A server state management library. Handles caching, background refetching, pagination, and optimistic updates for all API data.
Lingui An internationalization (i18n) library. Trans wraps JSX for translation; msg tags string literals; useLingui provides the _() translation function.
react-native-reanimated A React Native animation library that runs animations on the UI thread via worklets, enabling 60/120fps scroll-driven animations without JS thread involvement.
react-native-gesture-handler A React Native library for declarative gesture recognition (pan, tap, swipe). Used for swipe-to-delete and drag-and-drop in list items.
expo-image An Expo-provided image component with superior caching, memory management, and transition support compared to React Native's built-in Image.
CocoaPods The dependency manager for iOS native libraries. Run bundle exec pod install after adding native packages.
Gradle The build system for Android. Used to compile and link native Android code.
Hermes The JavaScript engine used by React Native on both iOS and Android. Provides faster startup and lower memory usage than JavaScriptCore.
Flipper A debugging platform for React Native apps. Provides network inspection, React DevTools, and plugin support.

Patterns & Conventions

Term Definition
CommonNavigatorParams The TypeScript type map defining all screen names and their route parameter types in the common navigation stack. Every new screen must be registered here.
AllNavigatorParams A superset of CommonNavigatorParams that includes all navigators (including authenticated-only screens).
RQKEY / Query Key Factory A function that generates a TanStack Query cache key array for a specific query. Used for targeted cache invalidation. Example: FEED_RQKEY(feed).
FeedDescriptor A pipe-delimited string identifying a feed: `"type
truncateAndInvalidate A utility that truncates a feed's TanStack Query cache to the first page and marks it stale, triggering a refetch. Used for pull-to-refresh and "load latest".
Profile Shadow (useProfileShadow) A client-side cache overlay that applies optimistic updates (follow/block state) to profile data before server confirmation.
atoms (a) The design system's atomic style tokens from #/alf. Used as style array entries: a.flex_1, a.px_xl, a.rounded_full. Analogous to Tailwind utility classes.
useTheme / t A hook from #/alf returning the current theme object. t.atoms.* provides semantic, theme-aware style tokens; t.palette.* provides raw color values.
platform() / web() / native() Utilities from #/alf for platform-conditional styles. web(style) applies only on web; native(style) applies only on native; platform({ios, android}) applies per-platform.
IS_NATIVE / IS_IOS / IS_ANDROID / IS_WEB Build-time boolean constants from #/env for platform detection. Prefer these over Platform.OS directly.
IS_INTERNAL A build-time boolean that is true only in internal (non-public) builds. Gates developer tooling and debug features.
Minimal Shell Mode An app UI state where the navigation chrome (tab bar, header) is hidden for immersive content viewing. Controlled via useSetMinimalShellMode.
cleanError(e) A utility from #/lib/strings/errors that extracts a user-safe string from an error object, stripping stack traces and sensitive details.
isNetworkError(e) A utility that returns true if an error is a network connectivity failure (vs. a server-returned error).
isOverMaxGraphemeCount A utility for character limit validation that counts Unicode grapheme clusters (not raw characters). Required for correct emoji handling.
requireAuth A hook-provided function that gates an action behind authentication. If the user is not logged in, it redirects to sign-in before executing the callback.
GestureActionView A component that wraps list items with swipe gesture support, revealing action buttons (mark as read, delete) at configurable pixel thresholds.
SettingsList.* A set of layout primitives (Container, Item, LinkItem, PressableItem, ItemIcon, ItemText, Divider, BadgeText) for building consistent settings screens.
Layout.* Standard screen shell components: Layout.Screen, Layout.Header.Outer, Layout.Header.BackButton, Layout.Header.Content, Layout.Header.TitleText, Layout.Content.
Portal (in SettingsList.Group) A React context-based teleportation mechanism that renders ItemIcon and ItemText in a different visual position than where they appear in JSX.
useImperativeHandle / SectionRef A pattern used in profile section components to expose a scrollToTop() method to parent components via a forwarded ref.
Worklet (Reanimated) A JavaScript function annotated to run on the UI thread via react-native-reanimated. Enables 60/120fps animations without JS bridge overhead.
SharedValue (Reanimated) A value that lives on the UI thread and can be read/written from worklets without crossing the JS bridge. Used for scroll-driven animations.
runOnJS / runOnUI Reanimated utilities for crossing the JS↔︎UI thread boundary. runOnUI schedules a worklet on the UI thread; runOnJS calls a JS function from a worklet.

Domain Terms

Term Definition
DID (Decentralized Identifier) A globally unique, persistent identifier for an AT Protocol account (e.g., did:plc:abc123). Unlike handles, DIDs never change.
Handle A human-readable username in the AT Protocol (e.g., alice.bsky.social). Handles can change; DIDs cannot.
AT URI A URI identifying a specific AT Protocol record: at://<did>/<collection>/<rkey>. Example: at://did:plc:abc/app.bsky.feed.post/xyz.
rkey (Record Key) The unique identifier for a specific record within a user's AT Protocol collection.
Lexicon AT Protocol's schema definition system. app.bsky.feed.post is the lexicon for Bluesky feed posts.
Convo / ConvoView A conversation object from the AT Protocol chat API (ChatBskyConvoDefs.ConvoView). Contains members, last message, unread count, and conversation ID.
Chat request A conversation with status: 'request' — initiated by another user but not yet accepted. Shown in the Inbox screen.
missing.invalid The handle assigned to deleted or deactivated Bluesky accounts. Used throughout the code to detect and handle deleted account conversations.
Feed Generator (feedgen) A custom algorithmic feed on Bluesky, identified by a URI. Users can subscribe to these feeds.
Starter Pack A curated bundle of accounts and feeds that new users can follow in bulk to bootstrap their experience.
Labeler A special Bluesky account type that provides content moderation labels. Users can subscribe to labelers to apply their label definitions.
ModerationDecision The result of moderateProfile() — an object with .ui(context) methods that return blur/hide/alert decisions for different UI contexts.
ModerationOpts Configuration passed to moderateProfile() containing the user's subscribed labelers and moderation settings.
viewer In AT Protocol API responses, viewer is a sub-object containing the current user's relationship to the subject (e.g., profile.viewer?.following).
Rich Text / Facets AT Protocol's structured text format. Facets are byte-range annotations marking spans as links, mentions, or hashtags.
PWI (Public Web Interface) The unauthenticated browsing mode. Used as a fallback storage key ('pwi') for search history when no account is logged in.
Soft Reset An event triggered when the user taps the active tab bar item. Causes the active feed to scroll to top and refresh.
MESSAGE_SCREEN_POLL_INTERVAL The interval (10 seconds) at which the messages event bus polls for new messages when a message screen is active.
Messages event bus An internal pub/sub system (useMessagesEventBus) that coordinates polling and real-time updates across message-related screens.
Age assurance / AgeRestrictedScreen A feature that gates access to certain content (like chat) behind age verification.
precacheConvoQuery / precacheProfile Functions that pre-populate the TanStack Query cache before navigation, preventing loading states on the destination screen.
DISCOVER_FEED_URI The constant URI for the Bluesky Discover (What's Hot) feed. Used for special-casing polling behavior and interstitial insertion.
KNOWN_SHUTDOWN_FEEDS A constant array of feed URIs that are known to be shut down. Triggers FeedShutdownMsg display.
isFallbackMarker A flag on a FeedPostSlice indicating that the Following feed ran out of posts and fell back to Discover content.
isIncompleteThread A flag on a FeedPostSlice indicating the thread is too long to show in full, requiring a "View full thread" link.
Live Now / liveNow A feature that shows a live status indicator on author avatars when they have an active live status (e.g., streaming).
Progress Guide An onboarding interstitial shown to new users tracking progress toward goals like following 10 accounts.
MainScrollProvider A context provider that translates scroll events into shell header show/hide animations.
NUX (New User Experience) A system for tracking one-time onboarding prompts. Nux.ExploreInterestsCard is one example.
recId Recommendation ID — an opaque identifier from the suggestions API used to track which recommendation algorithm produced a suggested account.
BCP 47 A standard for language tags (e.g., en, fr, zh-Hant). Used via bcp-47-match to determine if English is among the user's content language preferences.
allowSubscriptions A field on the app.bsky.notification.declaration record controlling who can subscribe to receive notifications about a user's activity.
TID (Timestamp ID) A lexicographically sortable, time-based identifier used as record keys in AT Protocol. Generated via TID.nextStr() from @atproto/common-web.
bulkWriteFollows A utility that creates follow records in bulk using applyWrites, chunked into batches of 50. Used in the Starter Pack and Find Contacts flows.
whenFollowsIndexed A polling utility that waits for AT Protocol follow records to be indexed by the AppView before returning.
sourceInterstitial A field indicating the UI surface from which a user entered the video feed ('discover', 'explore', 'none'). Used for analytics attribution.
Deduped_LANGUAGES The LANGUAGES list filtered to one entry per code2 value, preventing duplicate options in language dropdowns.
sanitizeDisplayName A utility that applies moderation decisions to a display name, potentially replacing or modifying it based on active labels.
sanitizeHandle A utility that formats an AT Protocol handle for display, optionally prepending @ and forcing LTR text direction.