diff --git a/skills/stream-react-native/RULES.md b/skills/stream-react-native/RULES.md index 1e2e55d..b08a667 100644 --- a/skills/stream-react-native/RULES.md +++ b/skills/stream-react-native/RULES.md @@ -53,6 +53,8 @@ Before installing Stream Chat, Video, or Feeds RN packages: Before changing an existing Chat, Video, or Feeds UI, fetch the manifest-selected markdown page that matches the requested change. Choose the implementation path from the docs and the existing app: theme for style-only changes (Chat / Video; Feeds is headless and changes are made directly in the components you wrote), component overrides for UI slots (Chat / Video only), documented props/hooks for behavior, and optional native packages only for requested native capabilities. +**Matching a provided design is a decompose-and-plan-first task, not a theming afterthought.** When a Chat request carries a target appearance (screenshot, Figma, "look like \"), run [`references/CHAT-REACT-NATIVE-design-matching.md`](references/CHAT-REACT-NATIVE-design-matching.md) **before** writing UI code: decompose the reference region by region and plan every difference as theming / layout / functional, then verify region-by-region against the reference. Implement **every** region, the composer included; a region left at the SDK default is a FAIL, not a "known cosmetic gap." Report a region as unmatched only when it is genuinely impossible (say what + why), never because it is risky or more effort. + --- ## Required peer setup diff --git a/skills/stream-react-native/SKILL.md b/skills/stream-react-native/SKILL.md index f87e34e..efd83d5 100644 --- a/skills/stream-react-native/SKILL.md +++ b/skills/stream-react-native/SKILL.md @@ -8,7 +8,7 @@ metadata: allowed-tools: >- Read, Write, Edit, Glob, Grep, WebFetch(domain:getstream.io), - Bash(ls *), Bash(find . *), Bash(grep *), + Bash(ls *), Bash(find . *), Bash(grep *), Bash(sips *), Bash(cat package.json), Bash(cat app.json), Bash(cat app.config.js), Bash(cat app.config.ts), Bash(cat babel.config.js), Bash(cat metro.config.js), Bash(command -v getstream), Bash(getstream *), @@ -154,11 +154,50 @@ Product-specific setup, docs lookup, gotchas, and UI blueprints live under **`re - **Video screen/component blueprints:** [`references/VIDEO-REACT-NATIVE-blueprints.md`](references/VIDEO-REACT-NATIVE-blueprints.md) - **Feeds setup/reference:** [`references/FEEDS-REACT-NATIVE.md`](references/FEEDS-REACT-NATIVE.md) - **Feeds screen/component blueprints:** [`references/FEEDS-REACT-NATIVE-blueprints.md`](references/FEEDS-REACT-NATIVE-blueprints.md) +- **iOS simulator verification (fast run/screenshot loop):** [`references/SIMULATOR-VERIFICATION.md`](references/SIMULATOR-VERIFICATION.md) If the requested product file is not bundled yet, say so plainly and only switch to live docs if the user asks. --- +## Docs-first triggers (consult docs before building) + +The bundled blueprints cover the **common scaffold path only** - root providers, auth gate, fresh-app scaffold, channel list, channel/message screen, threads (and the Video/Feeds equivalents). For anything that **customizes or extends** that path, the live `llms.txt`-selected page is the source of truth for the current API and recommended pattern. Fetch it **before** writing code. + +Docs-first applies when the request hits any of these: + +- **UI customization / theming / component overrides** - themes, design tokens, replacing a slot (message bubble, composer, call controls, participant tile, activity row), custom render props. +- **Cookbook recipes** - typing indicators, message actions, reactions UI, attachment handling, mentions, search, link previews, etc. +- **Advanced guides** - push notifications, offline support, PiP, ringing, screenshare, broadcasting, video filters, Chat+Video interop, notification/For-You feeds, polls, moderation. +- **Migration / version upgrades.** +- **Any exact API, prop, hook, or component detail** not already in the bundled blueprints. + +Mechanism (mirrors the `stream-react` skill): + +**match -> run the [`references/DOCS.md`](references/DOCS.md) manifest lookup -> `WebFetch` (or `curl -Ls`) the selected `.md` page -> implement to match.** On fetch failure, hand to the `stream-docs` skill; if neither resolves the API, **stop and ask the user - never build customization, cookbook, or advanced features from memory.** + +Enforced by [`RULES.md`](RULES.md) > Package version and docs discipline. + +--- + +## Styling-depth flag: matching a provided design (Chat) + +Orthogonal to the track above: if a **Chat** request carries a **target appearance** - an attached +screenshot, a Figma frame, or "make it look like WhatsApp / iMessage / Slack / \" - do not +treat it as a set-a-few-colors task. **Before** writing UI code, run +[`references/CHAT-REACT-NATIVE-design-matching.md`](references/CHAT-REACT-NATIVE-design-matching.md): +it decomposes the reference region by region and plans **every** difference as one of three axes - +**theming** (the `Theme` object), **layout** (`WithComponents` slot overrides + props), or +**functional** (props / config / SDK hooks) - then verifies region-by-region against the reference. +Implement every region, the composer included; a region left at the SDK default is a FAIL, not a +"known cosmetic gap." + +This runs in addition to (not instead of) the `DOCS.md` lookup: fetch the manifest-selected +theming/customization pages to confirm exact theme paths and component names. A plain Chat request +with **no** target appearance does not trip this flag - build from the blueprints as usual. + +--- + ## Track A - New app **Full detail:** [`builder.md`](builder.md) - use the new-app path. diff --git a/skills/stream-react-native/builder.md b/skills/stream-react-native/builder.md index b624099..cbec9f3 100644 --- a/skills/stream-react-native/builder.md +++ b/skills/stream-react-native/builder.md @@ -377,6 +377,8 @@ How to add one: After adding native optional packages, follow their platform permission steps. For Expo, keep the app in the dev-client/native-build lane and run `npx expo prebuild` when native config changes need to be regenerated. +**Batch capability packages before the first native build.** Each native capability package forces a prebuild + native rebuild (minutes). Decide the *complete* set the app needs up front and install them together **before** the first `expo run:ios` / `pod install`, so you build once — adding one later (e.g. discovering the composer mic needs `expo-audio` only after the app runs) costs a second full rebuild, the most common avoidable simulator time sink. See [references/SIMULATOR-VERIFICATION.md](references/SIMULATOR-VERIFICATION.md). + --- ## 5. Configure native/runtime requirements @@ -573,3 +575,11 @@ npx expo start ``` Run only commands that exist in the project. + +**Running on the iOS simulator (Expo dev-client).** When you actually boot the app to screenshot and +verify it (especially for a design match), follow the fast loop in +[references/SIMULATOR-VERIFICATION.md](references/SIMULATOR-VERIFICATION.md): batch all capability +packages before the first native build, start Metro **not** in CI mode (`npx expo start --dev-client +--clear`) so edits aren't served stale, start Metro separately from `expo run:ios` (which exits and +can take the bundler down), reach non-initial screens with temporary in-code navigation (`simctl` +can't tap), and wait for the client to reconnect before trusting a screenshot. diff --git a/skills/stream-react-native/credentials.md b/skills/stream-react-native/credentials.md index dfdf9c1..5b043a3 100644 --- a/skills/stream-react-native/credentials.md +++ b/skills/stream-react-native/credentials.md @@ -113,7 +113,7 @@ getstream api UpdateUsers --request '{"users":{"":{"id":"` and tag the channel with a `seeded_by_skill: true` marker in `data.custom` so later runs can detect this skill's own seeded data: ```bash -getstream api GetOrCreateChannel --type messaging --id general --request '{"data":{"name":"General (demo)","created_by_id":"","members":[{"user_id":""},{"user_id":"alice"},{"user_id":"bob"}],"custom":{"seeded_by_skill":true,"demo_prefix":""}}}' +getstream api GetOrCreateChannel --type messaging --id general --request '{"data":{"created_by_id":"","members":[{"user_id":""},{"user_id":"alice"},{"user_id":"bob"}],"custom":{"name":"General (demo)","seeded_by_skill":true,"demo_prefix":""}}} ``` Use namespaced channel ids such as `general`, `random`, `team-alpha`. Make sure the token user appears in `data.members`. `GetOrCreateChannel` is idempotent on the `(type, id)` pair - re-running with the same prefix returns the existing channel rather than duplicating it. @@ -124,7 +124,7 @@ After creating demo channels, summarize without secrets and **without printing u #### C3 - Send demo messages idempotently (only if the user asked for messages or more demo data) [Chat] -Use `SendMessage` (confirm its body shape with `getstream api SendMessage -h`). Each message's `user_id` must belong to an existing user (so use the namespaced demo users from C1, or the token user). Tag every seeded message with a stable `custom.seed_key` so a re-run can detect and skip already-seeded messages. +Use `SendMessage` (confirm its body shape with `getstream api SendMessage -h`). Each message's `user_id` must belong to an existing user (so use the namespaced demo users from C1, or the token user). Tag every seeded message with a stable `custom.seed_key` so a re-run can detect and skip already-seeded messages. Don't try to backdate messages. Before sending, check whether the channel already contains a message with the same `seed_key`: @@ -206,6 +206,20 @@ After C6 + C7, summarize without printing tokens: Do not run C4 through C7 when Feeds is not in scope, or when the user only asked for credentials. If only C4 + C5 ran (timeline-only demo, no Explore tab), `foryou` stays empty and that is fine - the Home tab still works. +### Channel-type configuration - disable threads (optional) [Chat] + +Some feature toggles live on the **channel type**, not on the client. Thread replies are one: whether the SDK surfaces a reply-in-thread affordance is governed by the `replies` setting on the `messaging` channel type (enabled by default). If a design has no threads (see the thread scope gate in [`references/CHAT-REACT-NATIVE-design-matching.md`](references/CHAT-REACT-NATIVE-design-matching.md)), turn them off at the source so the UI never offers a thread action the design lacks: + +```bash +# Confirm the body shape first, then disable thread replies on the messaging type. +getstream api UpdateChannelType -h +getstream api UpdateChannelType --name messaging --request '{"replies":false}' +``` + +- This is a **mutating, app-wide** setting - it affects **every** channel of the `messaging` type, not just demo channels. Only run it on a fresh / scaffold app, or after the same non-empty-app confirmation gate as Step C0. Do not silently reconfigure a channel type in an app that already has real data. +- Follow the CLI-safety rule (confirm the endpoint and body with `getstream api UpdateChannelType -h` before running; only mutate after the user confirms no threads). +- Reversible and symmetric: re-enable with `--request '{"replies":true}'`. + ### Step D - Proceed automatically After credentials and requested demo data succeed, return to [`SKILL.md`](SKILL.md) and continue into [`builder.md`](builder.md). No additional prompt is needed. diff --git a/skills/stream-react-native/references/CHAT-REACT-NATIVE-blueprints.md b/skills/stream-react-native/references/CHAT-REACT-NATIVE-blueprints.md index 4764f2d..c805787 100644 --- a/skills/stream-react-native/references/CHAT-REACT-NATIVE-blueprints.md +++ b/skills/stream-react-native/references/CHAT-REACT-NATIVE-blueprints.md @@ -18,10 +18,13 @@ Expo lane: change imports from `"stream-chat-react-native"` to `"stream-chat-exp | thread navigation, replies, thread list | Thread Screen or Thread List Screen | | React Navigation or Expo Router shell | Navigation Shell | | theme, dark mode, colors, design tokens | Theming Blueprint | +| screenshot / Figma / "match this design" / "make it look like \" | [CHAT-REACT-NATIVE-design-matching.md](CHAT-REACT-NATIVE-design-matching.md) first (decompose + plan every region), then Theming + Component Override Blueprints | | UI slot, component, behavior, or composer customization | DOCS.md -> primary manifest lookup, then Component Override Blueprint | -| offline support or sign-out cleanup | Offline and Sign-out Blueprint | +| cookbook / advanced feature (push, mentions, search, reactions UI, link previews, etc.) | DOCS.md -> primary manifest lookup, then implement to match | +| offline support | Offline and Sign-out Blueprint | +| sign-out cleanup | Offline and Sign-out Blueprint | -If no row matches, read [DOCS.md](DOCS.md) and [CHAT-REACT-NATIVE.md](CHAT-REACT-NATIVE.md) first, then verify symbols in manifest-selected docs or the installed package before coding. +If no row matches, run the [DOCS.md](DOCS.md) manifest lookup and `WebFetch` (or `curl -Ls`) the selected `.md` page before coding; also read [CHAT-REACT-NATIVE.md](CHAT-REACT-NATIVE.md) for setup/gotchas. Verify symbols in the manifest-selected docs or the installed package - never build customization or advanced features from memory. --- @@ -408,6 +411,8 @@ Wiring: --- +For custom header consult [DOCS.md](./DOCS.md) + ## Thread Screen Use explicit thread state. The main channel should receive the active `thread` while the thread screen is open; the thread screen renders `Channel` with `threadList`. @@ -488,6 +493,7 @@ Wiring: - `threadList` marks the screen as thread mode. - `onThreadDismount` should clear the active thread. - Offline mode does not support thread access in the referenced docs. +- There is no built-in thread header component exposed by the SDK, it has to be a custom component --- @@ -571,7 +577,7 @@ Wiring: ## Component Override Blueprint -Use `WithComponents` for custom subcomponents. Keep custom message rows memoized and use SDK context hooks. +Always consult [DOCS.md](./DOCS.md) to find a relevant guide/cookbook, if there is no match, read SDK context and hooks to reuse business logic. Aim for using SDK provided hooks and contexts, only use low-level client if there are no hooks. Use `WithComponents` for custom subcomponents. Keep custom message rows memoized and use SDK context hooks. ```tsx import React, { memo } from "react"; @@ -606,6 +612,8 @@ Wiring: - Prefer the smallest documented override that satisfies the requested customization. - Avoid replacing core message components unless required. - If replacing message row structure and still using the long-press overlay, preserve overlay anchor behavior by reading the manifest-selected context docs. +- Consult the theme object to adjust spacing as necessary +- Provide `WithComponents` at root level so overrides apply for all application screens --- diff --git a/skills/stream-react-native/references/CHAT-REACT-NATIVE-design-matching.md b/skills/stream-react-native/references/CHAT-REACT-NATIVE-design-matching.md new file mode 100644 index 0000000..301d61b --- /dev/null +++ b/skills/stream-react-native/references/CHAT-REACT-NATIVE-design-matching.md @@ -0,0 +1,362 @@ +# Chat React Native - matching a reference design (screenshot / Figma / "make it look like X") + +When the user gives a **target appearance** - an attached screenshot, a Figma frame, or "make the +chat look like WhatsApp / iMessage / Telegram / Slack / \" - the job is **not** "set a few +colors." A reference design is a **checklist of regions**, and real designs differ from Stream's +defaults in *layout* and *behavior*, not just color: the composer button set, where the timestamp +and read receipts sit, the bubble shape, the header, the date separators. Changing the bubble color +and calling it done is the classic failure - do not repeat it. + +Run this page **before** writing code, in addition to (not instead of) the normal `DOCS.md` lookup +in [SKILL.md](../SKILL.md). It is the *procedure* + the *routing map*; the exact theme keys and +component names come from the manifest-selected docs and the installed package, not from memory. + +**Implement EVERY region - the composer is first-class, not optional.** Do not deliver a partial +match and label the rest "known cosmetic gaps." "Risky" or "more effort" is not a reason to skip a +region; only genuine impossibility is, and then you say exactly what and why. The composer is the +region most often left at its default and is exactly where users notice the mismatch. + +--- + +## Work in batches - don't let a full match take all day + +- **Decompose all regions first**, then read the theme tree and the component slots you'll need in + **one** pass (manifest-selected theming + customization pages, plus the installed package's + `Theme` type and component names). Pull the theme paths and slot names up front; don't drip-feed + lookups while coding. +- **Implement all differing regions, THEN build/run once.** Don't rebuild-and-screenshot after each + tiny edit - batch a round of fixes, run once, compare once. +- Iterate only on the regions that actually fail. + +--- + +## Three axes of customization (internalize this first) + +RN Chat gives you three mechanisms. Map each design difference to the cheapest axis that reaches it, +and exhaust theming before overriding a component. + +| Axis | Mechanism | What it changes | What it CANNOT change | +|---|---|---|---| +| **Theming** | The `DeepPartial` object passed to **both** `` **and** `` (see the **Theming Blueprint** in [CHAT-REACT-NATIVE-blueprints.md](CHAT-REACT-NATIVE-blueprints.md)) | Colors, fonts, spacing, padding, border-radius, and dimensions - *within the existing layout*. In RN the theme object carries **both** color **and** padding/dimension, so most reskins are theme-only. | The structure - which views render, their arrangement, whether metadata sits inside or below the bubble, which buttons the composer has. | +| **Layout / structure** | Component overrides via `WithComponents overrides={{ … }}` and list-level props (`MessageList`, `Message`, composer slots) - see the **Component Override Blueprint** | The actual views: message row layout, bubble shape, metadata position, composer button set, channel-list preview row, attachment rendering. | Colors/fonts/spacing that a theme key already reaches (don't replace a component to change a padding). | +| **Functional** | Documented component props, channel config, and SDK context hooks (`useMessageContext`, `useChannelStateContext`, `useChannelActionContext`, …) | Which actions/behaviors are enabled, what's interactive, send/edit/reaction/thread behavior. | Pure appearance (that's theming). | + +**Two recurring mis-routings:** +- Solving a **structural** difference with a **theming** token. "Read receipts inside the bubble", "a + camera button in the composer", "the timestamp overlaid on the image", "an avatar on my own + messages" are **structural** -> a component override, not a color key. +- Solving a **spacing / padding / radius** difference by **overriding a component**. In RN those live + in the **theme object** - reach for the theme key first; only override the component when the + *arrangement* itself must change. + +**RN-specific: the channel header is app-owned.** Unlike other Stream SDKs, RN Chat has no +`ChannelHeader` slot baked into `Channel` - the nav header is **your** React Navigation +`Stack.Screen options` / Expo Router header (or a custom view above `MessageList`). Header +differences route to the **navigation layer**, not the theme. Match its height, title, subtitle, and +trailing affordances there; drive the title from channel state, never a hardcoded literal (every +channel would show the same wrong title). + +--- + +## Don't ship affordances the app can't back + +A reference design, a starting template, or a boilerplate example often carries buttons the app +doesn't actually have a feature for - most commonly a **video-call icon** in the header or composer +of an app that only implements chat. If a button has no wired behavior, **remove it** - don't leave +it rendered-but-disabled or wired to a no-op handler. A dead button is worse than no button: it reads +as broken, not as scoped-out. + +--- + +## Step 1: Decompose the reference into regions (every time) + +Go region by region. For **each** region: name what the design shows, compare it to the Stream RN +default, and decide **theming / layout / functional / already-default**. Produce an explicit task +list - one entry per region that differs. Do not skip a region because it "looks standard"; verify +it against the default. + +**Front-load the thinking - planning is cheap, UI validation is not.** The build -> run -> screenshot +-> compare loop in Step 3 is by far the most expensive part of a design match. Every region you name, +spec, and route now is one you won't rediscover through a costly visual-validation cycle later. Time +spent decomposing thoroughly up front is repaid many times over in iterations you never have to run. + +**Capture the spec, not just the identity.** For each region record the concrete attributes you'll +reproduce: bubble corner radius, tail/shape, max width, alignment; avatar shape/size and whether it +shows on own messages; font sizes and **weights** (a name is usually heavier than the body); +paddings and gaps; and the **sampled colors** (bubble fills, accent, ticks, background). "Looks +roughly like it" is the failure mode - a region with the right color but the wrong size or spacing +still fails the eye. + +**Getting sizes right (optional `sips` for scale).** Mobile screenshots are usually `@2x`/`@3x`, so +raw pixel counts are not points. When an exact dimension matters, get the scale and convert: + +```bash +sips -g pixelWidth -g pixelHeight # e.g. 1179 x 2556 -> ÷3 = 393x852 pt (@3x) +``` + +Then `points = pixels / scale`. Otherwise match by **proportion and theme tokens** (reuse the SDK's +spacing scale so custom pieces align with un-overridden parts) rather than eyeballed magic numbers. +Match **font weight** as its own dimension - measure/estimate each text role separately (name vs. +body vs. timestamp); a wrong weight reads "too bold / too thin" even at the right size. + +**Light/dark carve-out - don't pin structural surfaces to a light-mode literal.** The reference is +almost always a light screenshot. **Pin** the sampled **brand/content** colors (bubble fills, +glyphs, accent, read-receipt ticks) - they're the same in both modes. But keep **structural +surfaces** (message-list background, composer/input background, borders) on the theme's semantic +values so they still adapt; pinning a surface to `white` looks right in light mode and breaks in +dark. If the app supports dark mode, verify both. + +### Region checklist (walk all of these) + +**Channel list screen** (if in scope) +- [ ] List header (app-owned nav) · preview row layout · avatar · unread badge · timestamp · empty/loading state · background + +**Message screen - chrome** +- [ ] Nav header (**app-owned** - React Navigation / Expo Router): title, subtitle, back affordance, trailing avatar/buttons, height +- [ ] Chat background / wallpaper +- [ ] Date separators and the new-messages divider +- [ ] Scroll-to-bottom / jump-to-latest affordance + +**Message screen - the message itself** +- [ ] **Layout style: bubbles (messenger) vs. flat left-aligned rows (workplace/Slack).** This decides everything below. +- [ ] Bubble: fill color, border, corner radius, **shape/tail**, max width, alignment +- [ ] Grouping: consecutive same-author messages, who shows an avatar +- [ ] **Sender name placement** (own axis - verify separately from metadata below): shown at all + (1:1 chats often hide it, group chats show it)? **Inside** the bubble as a first line in its own + style use `MessageContentTopView`/`MessageContentBottomView` - apply padding, or **above/outside** it as a separate row - `MessageFooter`/`MessageHeader`? On incoming messages only, or own messages too? Default is `MessageFooter`, remove if you create a custom component. + Only on the first message of a consecutive group, or on every message? +- [ ] **Timestamp + delivery/read receipts placement**: **below/outside** the bubble (Stream default) controled by `MessageFooter` + or **inside it** (trailing corner, WhatsApp/iMessage style) - `MessageContentBottomView` / `MessageContentTrailingView`? Inside = structural. For `MessageContentTrailingView` always apply proper `alignSelf`. Default is `MessageFooter`, remove if you create a custom component. +- [ ] Pinned/sent to channel/saved for later/reminder status. Default is `MessageHeader`. +- [ ] Read/delivery indicator glyphs (single/double tick, color) +- [ ] Avatars beside messages (shown? side? shape? on own messages?) +- [ ] **Reactions placement** - one of the most common divergences from the Stream RN default, verify + it explicitly rather than assuming: **inside** the bubble's footprint (a pill overlapping the + bottom edge/corner) vs. **outside/below** it as a separate row? Also style and the reaction picker. +- [ ] **Spacing after any of the above three move.** Repositioning sender name, metadata, or + reactions relative to the bubble always changes the bubble's internal padding and the row's outer + margin/gap - re-derive both from the reference (match by proportion/theme tokens per the sizing + guidance above), don't just move the element and leave the old spacing. Stale padding shows up as + cramped bubble text once a name row is added inside, or as a leftover gap/overlap once reactions + move from below the bubble to overlapping it. +- [ ] Quoted / inline replies, thread-reply indicator +- [ ] Long-press message-actions menu + +**Message screen - attachments** +- [ ] Image/photo grid (the grouped collage is largely the RN default - restyle, don't rebuild) +- [ ] Video, file, giphy, link, voice-recording, poll, custom attachments +- [ ] Full-screen media viewer + +**Composer** (almost always differs - inspect closely, in BOTH states) +- [ ] **At rest** (no text): leading button(s), field, in-field glyphs, right-side buttons (camera/mic) +- [ ] **Typing**: send button appears / swaps in +- [ ] Input field container: background, border, corner radius, height +- [ ] Common slot overrides: see below -> Composer: common pattern -> slot +- [ ] Should input float? If yes use `messageInputFloating` flag +- [ ] Location sharing: https://getstream.io/chat/docs/sdk/react-native/guides/location-sharing/ + +**Cross-cutting** +- [ ] Fonts, accent color, icon set +- [ ] Light/dark behavior + +### When the reference is inconclusive - ask, don't guess + +**Thread scope decision.** A static screenshot usually does **not** decisively show whether threads +are in scope: the thread-reply indicator only renders on messages that already *have* replies, and +the reply screen + thread inbox are **separate screens** a message-list shot never captures. So +absence of a thread indicator is not evidence threads are unwanted. If the reference doesn't clearly +show threads and the user hasn't stated it, **ask one short question and wait** before building or +dropping them: + +> This design doesn't clearly show message threads. Should the app support threads (reply-in-thread + a thread screen), or keep conversations flat? + +- **Threads in scope** -> implement the Thread Screen (and the Thread List / inbox if the design + shows one) as routed in Step 2. +- **No threads wanted** -> don't merely omit the UI. **Disable thread replies on the `messaging` + channel type** so the SDK never surfaces a reply-in-thread affordance the design lacks - see + [credentials.md > disable threads](../credentials.md#disable-threads). With threads disabled at the + source, the message-row override doesn't have to reproduce a thread indicator, and Step 2.5 can + legitimately mark it `N/A - threads disabled on channel type`. + +**Composer placement decision.** A screenshot can likewise be inconclusive about whether the composer +is a **floating** input - a pill/bar inset from the screen edges with visible margin, corner radius, +and often a shadow, sitting *above* the content (common in iMessage/Telegram-style designs) - or a +**fixed/docked** bar flush with the screen's bottom edge and safe area. This is a structural +difference (container margin, corner radius, background, and possibly a layout override), not a +theming tweak, and getting it wrong changes the composer's relationship to the keyboard and the +message list both. If the reference doesn't make this clear, ask: + +> This design doesn't clearly show whether the message input should float above the content (inset, rounded corners) or dock flush at the bottom edge. Which do you want? + +State the result as a task list: `Region -> default vs. target -> mechanism (theme key / component +override / prop-or-hook / already-default)`. Implement **all** differing regions, not just the cheap +theming ones. + +--- + +## Step 1.5: Map design-implied features to optional native packages + +Some regions from Step 1 aren't reachable by theming or a component override alone - they need a +**native capability package** installed first. A screenshot signals a *capability*, not just a look: +voice messages, video attachments, a camera button in the composer, a document/file attachment, a +device photo-library picker, or a share action each imply an optional dependency. If the package +isn't installed you can style the slot perfectly and the region still won't work - the match fails at +the behavior level, not the pixel level. + +Walk the Step-1 task list and flag every region whose **capability** (not just its appearance) the +design requires, then map it to the package in the **Optional dependency map** in +[CHAT-REACT-NATIVE.md](CHAT-REACT-NATIVE.md#optional-dependency-map). Typical screenshot signals: + +- Voice-recording UI / audio waveform, or a voice-message bubble -> voice recording + audio packages +- Inline video / a video thumbnail with a play button -> video playback packages +- A camera button in the composer or a "take photo" affordance -> native image picker / camera +- A photo grid sourced from the device library, or an attachment-picker sheet -> media library packages +- File / document attachment rows -> document picker +- A share affordance on an attachment -> sharing packages + +Install only the packages the design actually implies, on the app's runtime lane (RN CLI vs. Expo), +following that map's install and permission notes - do NOT bulk-install the whole matrix for one +vague signal. If a region needs a capability package the app doesn't have, install it (or, if you +can't, flag it) **before** Step 2 - otherwise that region is a `GAP`, not a match. + +--- + +## Step 2: Route each region to a mechanism (RN Chat) + +Read [Theming Blueprint](./CHAT-REACT-NATIVE-blueprints.md#theming-blueprint) and [Component Override Blueprint](./CHAT-REACT-NATIVE-blueprints.md#component-override-blueprint) for rules. + +| Design region | Axis | Route to (confirm exact name in docs / package) | +|---|---|---| +| Bubble fill / text / border / radius / spacing | Theming | `theme.messageSimple.*` content/container keys | +| Bubble **shape / tail / sender name inside vs. outside / metadata inside bubble / avatar on own messages** | Layout | Override the message UI slot via `WithComponents` (reuse SDK sub-parts) | +| Message grouping / alignment | Layout | Message UI slot + `useMessageContext()` group flags | +| Read/delivery indicator glyph + color | Theming (+ Layout if repositioned) | `theme.messageSimple.status.*`; reposition via the message UI slot | +| Reactions appearance / **inside vs. outside the bubble** | Theming (+ Layout) | reaction theme keys for style; reaction slot override once placement crosses inside/outside the bubble (structural) - moving reactions outside the bubble also usually needs the row's bottom margin/gap theme key adjusted so the freed-up space doesn't look accidental | +| Reaction picker / message-actions menu | Layout / Functional | documented overlay/actions slots + props | +| Quoted reply / thread indicator | Theming (+ Layout) | reply/thread theme keys; slot if structural | +| **Thread reply screen** (parent message + replies) | Layout / **App-owned nav** | Separate nav screen: `Channel` with `threadList` + `Thread` (see the **Thread Screen** blueprint in [CHAT-REACT-NATIVE-blueprints.md](CHAT-REACT-NATIVE-blueprints.md)). Reuses your message-row + composer overrides; what's thread-specific is the parent-message header and the reply composer | +| **Thread list / inbox** (if in scope) | Theming (+ Layout) | `ThreadList` inside `Chat` (see the **Thread List Screen** blueprint); thread-list theme keys + `ThreadList` item props if the row layout differs | +| Attachments (image grid, file, giphy, voice) | Theming (+ Layout) | attachment theme keys; `Attachment` override only if structural | +| Composer field / buttons / layout | Theming + Layout | `theme.messageInput.*` for style; `MessageComposer` slot overrides for the button set/arrangement | +| Channel-list preview row | Theming (+ Layout) | `theme.channelPreview.*`; `ChannelList` `Preview*` props if structural | +| Nav header (title, subtitle, back, trailing) | **App-owned** | React Navigation `Stack.Screen options` / Expo Router header - not a theme key | +| Chat background / wallpaper | Theming (+ Layout) | `Channel`/message-list background theme key; a custom background view if it's a texture | +| Date separator / new-messages divider | Theming (+ Layout) | date-separator theme keys; slot override if the shape differs | + +**RN-specific: the thread reply screen is a separate screen that reuses your message + composer +overrides.** Tapping a thread indicator opens a distinct navigation screen - `Channel` with +`threadList` plus `` - not a slot styled in isolation. It re-renders the **same** message +row and composer, so any override you made for the main list applies here automatically, and any +sub-feature you dropped there (Step 2.5) is dropped here too. So most of the thread-screen match +falls out of the main-list work; what's genuinely thread-specific is the **parent-message header** at +the top of the replies and the **reply composer** (placeholder / affordances). If the design shows a +thread **inbox**, that's the separate `ThreadList` surface, not the reply screen. Verify the thread +screen on its real navigation path in Step 3 - it's a commonly-missed surface. + +### Composer: common pattern -> slot + +The composer row in the table above is one line; in practice it's the most-customized region. Map the +concrete pattern you see in the reference to the cheapest slot that reaches it (confirm exact names in +the manifest-selected docs / installed package). + +| Pattern (WhatsApp / Telegram / Slack / iMessage) | Slot(s) to use | +|---|---| +| Leading **"+"** / paperclip attach button | `AttachButton` | +| Extra leading icons (keyboard toggle, camera) beside "+" | `MessageComposerLeadingView` (compose your icons + ``) | +| Trailing button that **swaps mic <-> send** | Built-in via `OutputButtons`; style via `SendButton` + `StartAudioRecordingButton`, toggle mic via `audioRecordingEnabled` | +| Recolor / restyle the send button only | `SendButton` | +| Inline sticker / emoji icon inside the field | `InputView` (add icon to the text row) | +| Custom placeholder ("Jot something down", "iMessage") | `additionalTextInputProps.placeholder` | +| Attachment grid options (Photos / Camera / File / Poll / Location...) | `AttachmentPickerSelectionBar` (replace whole bar), or `handleAttachButtonPress` for a native sheet | +| Edit / reply banner above the text | `MessageInputHeaderView` or `Reply` | +| Pending upload thumbnails | `AttachmentUploadPreviewList` | +| "Also send to channel" (thread) | override `InputView` (hosts `ShowThreadMessageInChannelButton`) | +| Slow-mode countdown | `CooldownTimer` | +| Fully bespoke bar (none of the above fit) | `Input` (rebuild the whole body) | + +### Composer: prefer / avoid + +- **Prefer** overriding the **simple leaf buttons** (`SendButton`, `CooldownTimer`, `AttachButton`) + and the **zone views** (`MessageComposerLeadingView`, `MessageInputTrailingView`, + `MessageInputHeaderView`). They receive their state from context and are safe to restyle. +- To change the **mic <-> send** behavior, recompose via `MessageInputTrailingView` (which renders + ``) or restyle `SendButton` - and toggle the mic with the `audioRecordingEnabled` + flag. Do **not** reimplement `StartAudioRecordingButton` / `AudioRecordingButton` / `AudioRecorder`: + they carry the hold-to-record + slide-to-cancel + slide-up-to-lock gesture logic. Restyle the + provided component, keep the behavior. +- To add leading buttons, override `MessageComposerLeadingView` and render your icons alongside + `` - don't rebuild the picker plumbing. + +--- + +## Step 2.5: Overriding a slot inherits ALL of its sub-features + +The composite slots - the message row, the composer, the channel-list preview - each render **many** +sub-features. When you override one via `WithComponents`, every sub-feature the default drew +**disappears unless you reproduce it.** A custom row that handles only the case in front of you (one +outgoing text bubble) silently drops attachments, quoted replies, reactions, read receipts, +grouping, and edited/deleted state - and a near-empty test channel hides the loss until the user +spots it. + +**Rule:** before overriding a composite slot, read the default component in the manifest-selected +docs (and the installed package), enumerate every sub-view and conditional branch, and for each +decide **reproduce it** (reusing the SDK's own sub-component) or **consciously drop it** (and say so). +Prefer the **narrowest** slot that achieves the change; reach for a full row/composer replacement +only when the *arrangement* truly needs it. Keep custom rows `memo`-ized and read SDK context hooks +(`useMessageContext`, `useMessagesContext`) for data and handlers - do not hand-roll business logic. + +**Completion contract - fill before you ship (do NOT skip rows).** For **each** region you replace, +list every sub-feature the default rendered and mark it exactly one of: +- **Reproduce it** - reuse the named SDK piece; don't hand-roll it. +- **`N/A - `** - a real *design* reason the target doesn't need it (e.g. read + receipts in an anonymous livestream chat). **"Deferred", "later", "moving fast", "out of scope for + now" are NOT design reasons and are NOT valid N/A.** For the **thread-reply indicator** + specifically, `N/A` is valid **only** when threads are actually disabled on the channel type + (`replies:false`, per the Step 1 thread scope decision); if threads are still enabled server-side + but you skipped the indicator, that's a `GAP`, not `N/A`. +- **`GAP - not implemented`** - if you are knowingly skipping a needed capability, label it in + exactly those words so it stays visible. Never relabel a time-skip as `N/A`. + +A blank row - or an `N/A` whose real reason is "deferred" - means incomplete. + +Typical **custom message row** sub-features to account for: text (markdown/links/mentions/emoji), +attachments (image/file/video/voice/giphy), reactions (display + add), quoted/replied-to parent, +thread-reply indicator, read/delivery receipts on own messages, edited/deleted state, actions menu, +same-author grouping, and error/optimistic-send state. **Custom composer:** text input; attachments ++ upload (and removal); mentions/slash-command autocomplete; send (+ the at-rest↔typing swap); voice +recording (if enabled); edit-message mode. + +--- + +## Step 3: Verify against the reference - region by region (mandatory) + +A design match is **not done** until the app runs and the result is compared to the reference. +Presence-and-color is not enough; verify **size, position, and proportion** too. + +1. **Seed data that triggers every region.** An empty or one-message channel proves nothing and + hides exactly the elements that get dropped. Ensure the test channel has: **an incoming and an + outgoing** message; a **run of 3+ consecutive messages from the same author** (so grouping + the + avatar rule render); a **photo album**; a message **with reactions**; a **reply / thread**; a + **long multi-line** message; and enough history that a **date separator** appears. Mark messages + read if the design shows read receipts. (Use the Stream CLI / credentials flow to seed if needed.) +2. **Open the real message screen on its actual navigation path** - channel list -> tap -> message + screen - not a one-channel shortcut that never exercises the header/navigation. If you add any + throwaway scaffold to reach a screen for a screenshot, **DELETE it before delivery** (remove the + branch/flag/import - don't merely disable it) and re-verify on the real path. On the iOS simulator + `simctl` can't tap, so reach non-initial screens with temporary in-code navigation and drive + composer/picker states via SDK hooks - see the fast loop, stale-bundle trap, and cleanup steps in + [SIMULATOR-VERIFICATION.md](SIMULATOR-VERIFICATION.md). +3. **Build a comparison table.** For each region from Step 1: target attribute (size / position / + color / presence) -> what rendered -> **PASS / FAIL**. Walk the whole checklist; don't stop at the + regions that happen to look right. +4. **Re-check the silently-lost ones explicitly, every time:** the **incoming-message avatar** and + **grouping**; the **nav header** (height, title, back); the **composer in BOTH states** (at-rest + vs. typing - the send/mic swap); **metadata placement**; reaction display; attachment rendering. +5. **Iterate until every region passes.** Fix, re-run, re-compare. Don't declare done on the first + render. +6. If you genuinely cannot run the app, say so plainly and list which regions are + implemented-but-unverified - never imply a match you did not see. +7. **Do not deliver with a region left at its default and call it a "known gap."** Every region in + the Step-1 checklist - the composer especially - must be implemented to match. Report something as + unmatched only when it is genuinely impossible (and say what + why), never merely because it's + risky or more effort. diff --git a/skills/stream-react-native/references/CHAT-REACT-NATIVE.md b/skills/stream-react-native/references/CHAT-REACT-NATIVE.md index cbcf7e0..9fc2c17 100644 --- a/skills/stream-react-native/references/CHAT-REACT-NATIVE.md +++ b/skills/stream-react-native/references/CHAT-REACT-NATIVE.md @@ -27,6 +27,7 @@ First path: 6. Place `OverlayProvider` and `Chat` high in the tree. 7. Use `useCreateChatClient` for normal auth. 8. Render `ChannelList`, `Channel`, `MessageList`, `MessageComposer`, and optional `Thread`. +9. If the user provided a target appearance (screenshot, Figma, or "look like \"), **before** building run [CHAT-REACT-NATIVE-design-matching.md](CHAT-REACT-NATIVE-design-matching.md): decompose the reference region by region, plan every theming/layout/functional difference, then apply the `Theming Blueprint` and `Component Override Blueprint` and verify region-by-region. Full screen blueprints: [CHAT-REACT-NATIVE-blueprints.md](CHAT-REACT-NATIVE-blueprints.md). Load only the section you are implementing. diff --git a/skills/stream-react-native/references/SIMULATOR-VERIFICATION.md b/skills/stream-react-native/references/SIMULATOR-VERIFICATION.md new file mode 100644 index 0000000..df4eb53 --- /dev/null +++ b/skills/stream-react-native/references/SIMULATOR-VERIFICATION.md @@ -0,0 +1,220 @@ +# Verifying on the iOS simulator — the fast loop + +Running a Stream RN app on the iOS simulator to screenshot and verify it is the most **expensive** +part of a build (a native build is minutes, not seconds). Most of the wasted time comes from a +handful of avoidable mistakes: a second native rebuild, a stale Metro bundle, and fighting the +simulator's lack of touch input. This page is the playbook that avoids them. + +Two lanes, and they behave **differently** at launch/reload — pick yours and read its column: + +- **Expo dev-client / native-build** (`npx expo prebuild` + `expo run:ios`, Metro via `expo start`). +- **React Native Community CLI** (`pod install` + `npx react-native run-ios`, Metro via + `react-native start`). No expo-dev-launcher, so several Expo-only steps below **do not apply**. + +The lane differences are called out inline and summarized in **§6**. + +--- + +## 1. The run loop (boot → build once → launch to Metro → screenshot) + +```bash +# Pick a booted device (or boot one). Grab its UDID. +xcrun simctl list devices +xcrun simctl boot ; open -a Simulator +``` + +**Pin that one UDID for the whole verification loop.** Once you've booted a device, reuse its UDID +in every `simctl`/`run:ios` call for the task instead of re-picking or re-booting mid-loop — +juggling multiple booted simulators is how a screenshot ends up on the wrong device or a stale build. + +### Expo dev-client lane + +```bash +# 1) Start Metro SEPARATELY, in the background, NOT in CI mode +npx expo start --dev-client --clear # leave this running in the background + +# 2) Build + install the dev-client ONCE (the expensive native build). +# The BUILD + INSTALL is what you need here. expo run:ios also tries to *launch* the app at the +# end, and that launch step commonly fails with: +# Error: osascript -e tell app "System Events" to count processes … exited with non-zero code: 1 +# That is a macOS Automation-permission error on the Simulator-window activation, NOT a build +# failure — the .app is already built and installed. Ignore it and launch yourself in step 4. +npx expo run:ios --device + +# 3) Dismiss the dev-client onboarding sheet (takes effect now that the app is installed). +xcrun simctl spawn defaults write EXDevMenuIsOnboardingFinished -bool YES + +# 4) Launch (and RELAUNCH on every later iteration) straight onto the Metro bundle — tap-free. +# `--initialUrl` tells expo-dev-client which JS bundle to load, so it skips the dev-launcher +# menu AND never shows the "Open in ?" confirmation. This is the ONE reliable tap-free +# launch. Use the http:// Metro URL (localhost:8081 for a simulator) — NOT the exp+:// form. +xcrun simctl launch --initialUrl "http://localhost:8081" + +# 5) Screenshot whatever is on screen. +xcrun simctl io screenshot out.png +``` + +**Why `--initialUrl` and nothing else (Expo):** on a dev-client the app must load a JS bundle from Metro. + +- A **bare** `xcrun simctl launch ` (no `--initialUrl`) opens the **expo-dev-launcher menu** + ("Development Servers" list). Selecting the server needs a **tap** you can't perform — you're stuck + on the dev menu. +- `xcrun simctl openurl "://…"` triggers an iOS **"Open in ?"** confirmation that + itself needs a tap — **never use it** (see §3). +- `--initialUrl "http://localhost:8081"` loads the bundle directly: no menu, no modal. Passing the + full `exp+://…` deep link to `--initialUrl` re-triggers the "Open?" modal — plain `http://` only. + +The floating dev-menu **gear** icon still overlays the app (dev-only) — ignore it (see §5). + +### React Native CLI lane + +The CLI has **no dev-launcher**, so steps 3 and 4 above **do not apply** — no onboarding sheet, no +`--initialUrl`, no launcher menu, no "Open?" modal. `react-native run-ios` builds, installs **and +launches** the app itself, and it launches cleanly (no osascript error). The debug binary has the +`localhost:8081` bundle URL baked in, so it auto-connects to Metro on any launch. + +```bash +# 1) Start Metro SEPARATELY, in the background +npx react-native start & # leave running (see §2 for the watchman caveat) + +# 2) Build + install + launch ONCE (the expensive native build). This also launches cleanly. +npx react-native run-ios --udid + +# 3) FAST relaunch on every later iteration — bare launch, NO --initialUrl. Auto-connects to Metro. +xcrun simctl launch + +# 4) Screenshot. +xcrun simctl io screenshot out.png +``` + +The CLI's dev overlay is a **LogBox "Open debugger to view warnings" toast** (bottom of screen), not a +gear — also dev-only, ignore it (see §5). + +--- + +## 2. Force a clean relaunch after code changes (avoid a stale bundle) + +Fast Refresh usually applies edits in place, but when you **remove** a component or import — e.g. +deleting the temp navigation scaffold from §3 — the in-memory bundle can keep referencing the gone +code and the app crashes on next interaction. Don't debug that as a real bug; it's a stale bundle. + +**Expo lane:** relaunch the app to force a fresh bundle fetch from Metro: + +```bash +xcrun simctl launch --initialUrl "http://localhost:8081" +``` + +Each expo-dev-client launch re-downloads the bundle, so a relaunch can never carry a stale in-memory +bundle. You do **not** need another `npx expo run:ios` — the native binary hasn't changed, only JS. + +**RN CLI lane — the watchman caveat (important):** if **`watchman` is not installed**, Metro does +**not** detect file edits, so **no** reload path surfaces your change — not Fast Refresh, not the +packager `GET /reload`, not even a cold `simctl launch` (the CLI app reuses its on-disk cached +bundle). Symptom: you edit a file, relaunch, and the screen is unchanged. The fix is one of: + +```bash +# Best: install watchman once, then Fast Refresh + relaunch work normally. +brew install watchman + +# Or, per-change without watchman: restart Metro with a cleared cache, THEN relaunch the app. +# (kill the old Metro on 8081 first) +npx react-native start --reset-cache & +xcrun simctl launch +``` + +Confirm the served bundle actually contains your edit before trusting a screenshot: +`curl -s "http://localhost:8081/index.bundle?platform=ios&dev=true" | grep -c ""`. + +Metro's interactive `r` reload only exists when Metro runs in a **foreground** terminal; the +background Metro above has no TTY to receive it (true for both lanes). + +--- + +## 3. Reaching non-initial screens without taps + +`xcrun simctl` **cannot tap or scroll**, and GUI automation (AppleScript / System Events) is +unauthorized (this is also why the Expo first-launch dev-menu sheet needs the `defaults write` +workaround in §1, and why `expo run:ios`'s own launch step errors on osascript). To screenshot a +screen behind the first one, drive navigation from code with **temporary** scaffold, then remove it: + +- **Auto-navigate to a channel — Expo Router:** a temp + `useEffect(() => setTimeout(() => router.push(\`/channel/${encodeURIComponent(cid)}\`), 800), [])` + in the index screen. **Encode the `cid`** — the `:` in `messaging:` otherwise mis-parses the + Expo Router path segment (`useLocalSearchParams` returns it decoded). +- **Auto-navigate to a channel — React Navigation (RN CLI):** navigate with a **params object**, so + there is **no URL to encode**. Use the container ref so it fires once navigation is ready: + ```tsx + const navigationRef = createNavigationContainerRef(); + // + // setTimeout(() => navigationRef.navigate('Channel', { channelCid: cid }), 800)}> + ``` + (An in-screen `useEffect(() => navigation.navigate('Channel', { channelCid: cid }), [])` also works; + the `onReady` form is the most reliable.) +- **Exercise a state inside ``** (composer typing, send button, attachment picker): a temp + child that calls the SDK hooks, e.g. `useMessageComposer().textComposer.setText('…')`, or + `useMessageInputContext().openAttachmentPicker()`. Screenshot each state. +- **A custom-scheme deep link is NOT a shortcut (Expo):** `simctl openurl ://…` triggers an + iOS "Open in ?" confirmation that needs a tap. Worse, that alert is owned by SpringBoard: it + **survives `simctl terminate`/`launch`** and overlays every later screenshot. If you fire it by + accident, the only tap-free recovery is to **reboot the simulator** + (`xcrun simctl shutdown && xcrun simctl boot `). Prefer the in-code temp nav above, + and on Expo load the bundle with `--initialUrl "http://…"` (§1), never `openurl`. +- **Then DELETE all temp scaffold** (remove the branch/import, don't just disable it), re-typecheck, + and **force a clean relaunch** (§2 — mind the RN CLI watchman caveat) — otherwise a stale bundle + still referencing the removed temp component crashes the app. + +For a region that's off-screen and awkward to reach, an alternative is to **seed** the state via the +Stream CLI (`getstream api SendMessage …`), screenshot, then hard-delete +(`getstream api DeleteMessage --request '{"hard":true}'`). + +--- + +## 4. Wait for the client before you trust a screenshot + +If the app gates its splash on the chat/video/feeds client resolving (e.g. splash hides only once +`chatClient` is ready), a screenshot taken too soon captures the launch/splash screen (Expo splash, +or the RN CLI launch screen / white screen), which looks like a hang. After any relaunch, **wait for +the client to reconnect** (poll Metro logs or just re-screenshot after a short delay) before +concluding anything is broken. + +The same applies **within** a screen, not just at launch: after navigating or relaunching, give +images/avatars a moment to finish loading and any list entrance animation to settle before you take +the "real" screenshot for a design comparison — a shot fired immediately can catch a placeholder or +mid-transition frame and read as a mismatch that isn't one. + +--- + +## 5. Known environmental limits (don't fight these) + +- **Component overrides won't show if wired wrong:** in `stream-chat-react-native` v9 a slot such as + `MessageHeader` is applied through **`WithComponents overrides={{ MessageHeader: … }}`**, not by + passing it as a `` prop (that prop is silently ignored — no error, no + effect, which looks exactly like a stale bundle during verification). Same in both lanes. Also, the + *default* `MessageHeader` renders nothing unless the message is pinned / saved-for-later / reminder + / sent-to-channel, so verify an override with an explicit, visibly-distinct custom component. +- **iOS 26 Photo Library prompt:** opening the picker's gallery grid fires a Photo Library access + prompt that needs a tap, and **`xcrun simctl privacy grant photos ` does NOT suppress the + full-access upgrade prompt**. Your custom picker chrome renders *behind* it — verify the tab + row/sheet you can see and note the grid as prompt-blocked rather than chasing a fully clean shot. +- The simulator has **no camera or microphone** — voice/video *capture* can only be verified on a + real device (see the Video reference). The composer mic *button* still renders (with `expo-audio` + installed); its recording just won't produce audio. +- **Dev-only overlays — ignore them in screenshots:** the **Expo** dev-client overlays a small + floating **gear / dev-menu launcher**; the **RN CLI** shows a **LogBox "Open debugger to view + warnings" toast** at the bottom. Both are dev-only (gone in a release build) and not part of the + app. Never treat either as an app element or a design mismatch to fix. + +--- + +## 6. Expo vs RN CLI — quick reference + +| Step | Expo dev-client | React Native CLI | +|---|---|---| +| Metro | `npx expo start --dev-client --clear` | `npx react-native start` (install `watchman` — see §2) | +| Build once | `npx expo run:ios --device ` (its launch step errors on osascript — harmless) | `npx react-native run-ios --udid ` (builds **and** launches cleanly) | +| Onboarding sheet | `defaults write EXDevMenuIsOnboardingFinished -bool YES` | n/a (no dev-launcher) | +| Launch / relaunch | `simctl launch --initialUrl "http://localhost:8081"` | `simctl launch ` (bare — no `--initialUrl`) | +| Dev-launcher menu / "Open?" modal risk | Yes — avoid via `--initialUrl`, never `openurl` | None | +| Reload after edit | relaunch (re-fetches fresh) | Fast Refresh **iff** watchman installed; else `react-native start --reset-cache` + relaunch (§2) | +| Reach non-initial screen | Expo Router `router.push`, **encode the cid** | React Navigation `navigate('Channel', { channelCid })`, **no encoding** | +| Dev overlay to ignore | floating gear | LogBox "Open debugger" toast |