Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ package/src/theme/generated/
perf/gauntlet/out/
perf/gauntlet/runtime/generated/

# A2UI's conformance cases, kept as upstream wrote them
package/__fixtures__/a2ui-conformance/

# Workflow / tooling configs not owned by this project's Prettier
.github/
.husky/
Expand Down
1 change: 1 addition & 0 deletions .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ npmPreapprovedPackages:
- '@stream-io/state-store'
- '@stream-io/worker-timer'
- '@stream-io/ws'
- '@a2ui/web_core'

npmPublishProvenance: true

Expand Down
6 changes: 4 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Targets iOS and Android.
- **Languages:** TypeScript + React Native
- **Runtime:** Node 24 (`.nvmrc` is `v24`; root `engines.node` is `>=22.13.0`; CI runs `24.x`)
- **Package manager:** Yarn 4.15.0 (Berry). The binary lives at `.yarn/releases/yarn-4.15.0.cjs` and is activated via `yarnPath` in `.yarnrc.yml`. Any globally installed `yarn` (even the Homebrew classic 1.x) acts only as a launcher — no Corepack required.
- **Workspaces:** single root `yarn.lock`; workspaces are `configs/typescript-config`, `package`, `package/native-package`, `package/expo-package`, `examples/SampleApp`, `examples/E2ETestApp`, `examples/ExpoMessaging`, `examples/ExpoChatGPTSample`. **No Lerna.**
- **Workspaces:** single root `yarn.lock`; workspaces are `configs/typescript-config`, `package`, `package/native-package`, `package/expo-package`, `examples/SampleApp`, `examples/E2ETestApp`, `examples/ExpoMessaging`. **No Lerna.**
- **Testing:** Jest with the `@react-native/jest-preset` + `@testing-library/react-native`.
- **Build:** `react-native-builder-bob` → CommonJS (`lib/commonjs`), ESM (`lib/module`), types (`lib/typescript`)
- **Lint/format:** ESLint 9 flat config + Prettier, strict (`--max-warnings 0`)
Expand All @@ -41,7 +41,7 @@ Respect repo-specific rules. Do not suppress lint rules broadly; justify and sco
- `native-package/` — bare RN wrapper (`stream-chat-react-native`)
- `expo-package/` — Expo wrapper (`stream-chat-expo`)
- `shared-native/{ios,android}` — native source shared by both wrappers; synced into them, never edited in place
- `examples/` — `SampleApp` (full-featured), `E2ETestApp` (minimal app for the Detox e2e suite), `ExpoMessaging`, `ExpoChatGPTSample` (AI components)
- `examples/` — `SampleApp` (full-featured), `E2ETestApp` (minimal app for the Detox e2e suite), `ExpoMessaging`. The AI components samples live in Vision-Agents (`examples/voice_agents/react_native_assistant`, `examples/voice_agents/crew_assistant/app`)
- `configs/typescript-config/` — shared `tsconfig` presets
- `ai-docs/` — agent-facing deep dives (see [References](#references))
- `perf/` — on-device performance toolkit (see [Accessibility, RTL & performance](#accessibility-rtl--performance))
Expand Down Expand Up @@ -219,6 +219,8 @@ Native code shared by both wrappers lives in `package/shared-native/{ios,android
- the main entry points must never reach `src/ai-components` or the AI-only packages (`react-syntax-highlighter`, `@khanacademy/simple-markdown`, `zod`, `victory-native`, Skia). `@stream-io/state-store` and `lodash` are shared: the main entry imports them too (the stores in `state-store/`, the Poll contexts);
- the AI entry points may only reach `src/ai-components` plus an explicit allowlist (`src/native.ts`, the native shimmer). The wrappers' `ai-components/index.js` registers the native pieces the AI code needs itself and must never import the wrappers' `src/index.js`.

`AIConversation` and its list sit behind a third entry point, `ai-components/conversation` (`package/ai-components/conversation/`, `package/{native,expo}-package/ai-components/conversation/`), because `@legendapp/list/keyboard` requires `react-native-keyboard-controller` unguarded. The isolation check keeps the `ai-components` and `a2ui` entry points from reaching `src/ai-components/conversation` or `@legendapp/list/keyboard`, and lets them load keyboard-controller only through the guarded `src/ai-components/internal/keyboard.tsx`. The conversation's state layer (`AIConversationController`, its types, `useAIConversationState`) lives in `src/ai-components/conversation-state` and stays in the plain entry.

Do not add AI slots to `DEFAULT_COMPONENTS`; core `require`s the defaults, which would break isolation. Skia and `victory-native` are optional peers (native libraries must be owned by the app); the JS-only AI dependencies are regular dependencies.

Native pieces live in `package/shared-native/`: the Android-only `PerfText` Fabric view (Kotlin plus a C++ shadow node in `shared-native/android/jni`, built through each wrapper's `android/src/main/jni/CMakeLists.txt`, registered with `cmakeListsPath`/`componentDescriptors` in `react-native.config.js`) and the `AIDictation` TurboModule. **The iOS dictation sources are excluded from the pod unless the app opts in** (`$StreamChatEnableDictation = true` in the Podfile, or the `stream-chat-expo` config plugin in `package/expo-package/plugin`), because referencing the Speech framework makes App Store Connect demand a speech recognition usage description from every app. Never declare `RECORD_AUDIO` in a library manifest. The dictation codegen spec is duplicated into each wrapper's `src/native/` (codegen only reads a package's own `jsSrcsDir`); the isolation check keeps the copies identical.
Expand Down
1 change: 0 additions & 1 deletion examples/E2ETestApp/e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ test names and expectations. Where the SDK disagrees with the native expectation
grouped channel lists, channel/message/member search, reminders, push notifications (app-level in RN), Android's
`BackendTests` (real backend), hard delete from the message menu, iOS read counts and the iOS scroll performance tests.
Tests that cut the network run only on Android (`itOnAndroid`): the iOS simulator shares the Mac's connection.
Tests that take the Android CI emulator down run only on iOS (`itOnIOS`), with a comment linking the ticket.

## Impact analysis

Expand Down
14 changes: 10 additions & 4 deletions examples/E2ETestApp/e2e/robots/UserRobotMessageListAsserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,16 @@ UserRobot.prototype.assertMessageDeliveryStatus = function (status, count) {
case MessageDeliveryStatus.FAILED:
return assertFailedIcon(true, count);
case MessageDeliveryStatus.NIL:
await assertVisibility(Message.deliveryStatusIsRead, false);
await assertVisibility(Message.deliveryStatusIsDelivered, false);
await assertVisibility(Message.deliveryStatusIsPending, false);
await assertVisibility(Message.deliveryStatusIsSent, false);
// Only the list's message cells: a thread's parent message sits above the replies outside
// any cell (ThreadFooterComponent) and shows its own status.
for (const status of [
Message.deliveryStatusIsRead,
Message.deliveryStatusIsDelivered,
Message.deliveryStatusIsPending,
Message.deliveryStatusIsSent,
]) {
await assertVisibility(status.withAncestor(MessageList.messages), false);
}
return;
default:
throw new Error(`Unsupported delivery status: ${status}`);
Expand Down
7 changes: 0 additions & 7 deletions examples/E2ETestApp/e2e/support/StreamTestCase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,6 @@ export const itOnAndroid: typeof it = ((name: string, fn: () => Promise<void>, t
? it(name, fn, timeout)
: it.skip(name, fn, timeout)) as typeof it;

/**
* `it` on iOS, `it.skip` on Android: for tests that take the Android CI emulator down with them
* (RN-513). Name the ticket in a comment above each one.
*/
export const itOnIOS: typeof it = ((name: string, fn: () => Promise<void>, timeout?: number) =>
device.getPlatform() === 'ios' ? it(name, fn, timeout) : it.skip(name, fn, timeout)) as typeof it;

/**
* `it`, except `it.skip` on iOS below 18: for tests that hit an SDK bug only older iOS versions
* show. Name the ticket in a comment above each one. Runs when the version can't be read.
Expand Down
29 changes: 10 additions & 19 deletions examples/E2ETestApp/e2e/tests/GiphyTests.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,12 @@ import { describe, it } from '@jest/globals';

import { MessageDeliveryStatus } from '../mock-server/DataTypes';
import { allureId, step } from '../support/step';
import { itOnIOS, setUpStreamTestCase } from '../support/StreamTestCase';
import { setUpStreamTestCase } from '../support/StreamTestCase';

describe('GiphyTests', () => {
const { backendRobot, participantRobot, userRobot } = setUpStreamTestCase();

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessage', async () => {
it('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessage', async () => {
allureId(13081);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN user sends a giphy using giphy command', () => userRobot.uploadGiphy());
Expand All @@ -17,16 +16,14 @@ describe('GiphyTests', () => {
);
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_whenParticipantAddsGiphyMessage', async () => {
it('test_userObservesAnimatedGiphy_whenParticipantAddsGiphyMessage', async () => {
allureId(13082);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN participant sends a giphy', () => participantRobot.uploadGiphy());
await step('THEN user observes the animated gif', () => userRobot.assertGiphyImage());
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessageInThread', async () => {
it('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessageInThread', async () => {
allureId(13083);
await step('GIVEN user opens a channel', async () => {
await backendRobot.generateChannels({ channelsCount: 1, messagesCount: 1 });
Expand All @@ -38,8 +35,7 @@ describe('GiphyTests', () => {
await step('THEN user observes the animated gif in thread', () => userRobot.assertGiphyImage());
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_whenParticipantAddsGiphyMessageInThread', async () => {
it('test_userObservesAnimatedGiphy_whenParticipantAddsGiphyMessageInThread', async () => {
allureId(13084);
await step('GIVEN user opens a channel', async () => {
await backendRobot.generateChannels({ channelsCount: 1, messagesCount: 1 });
Expand All @@ -54,8 +50,7 @@ describe('GiphyTests', () => {
);
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_afterAddingGiphyThroughComposerMenu', async () => {
it('test_userObservesAnimatedGiphy_afterAddingGiphyThroughComposerMenu', async () => {
allureId(13085);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN user sends a giphy using the command suggestions', () =>
Expand All @@ -64,8 +59,7 @@ describe('GiphyTests', () => {
await step('THEN user observes the animated gif', () => userRobot.assertGiphyImage());
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessage_AfterShuffling', async () => {
it('test_userObservesAnimatedGiphy_whenUserAddsGiphyMessage_AfterShuffling', async () => {
allureId(13086);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN user shuffles a giphy', () =>
Expand All @@ -80,8 +74,7 @@ describe('GiphyTests', () => {
);
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_messageIsNotSent_whenUserCancelsEphemeralMessage', async () => {
it('test_messageIsNotSent_whenUserCancelsEphemeralMessage', async () => {
allureId(13087);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN user cancels a giphy', () =>
Expand Down Expand Up @@ -126,8 +119,7 @@ describe('GiphyTests', () => {
);
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_deliveryStatusHidden_whenEphemeralMessageShown', async () => {
it('test_deliveryStatusHidden_whenEphemeralMessageShown', async () => {
allureId(13090);
await step('GIVEN user opens a channel', () => userRobot.login().openChannel());
await step('WHEN user runs a giphy command', () =>
Expand All @@ -140,8 +132,7 @@ describe('GiphyTests', () => {
);
});

// https://linear.app/stream/issue/RN-513
itOnIOS('test_deliveryStatusHidden_whenEphemeralMessageShownInThread', async () => {
it('test_deliveryStatusHidden_whenEphemeralMessageShownInThread', async () => {
allureId(13091);
await step('GIVEN user opens a channel', async () => {
await backendRobot.generateChannels({ channelsCount: 1, messagesCount: 1 });
Expand Down
40 changes: 0 additions & 40 deletions examples/ExpoChatGPTSample/.gitignore

This file was deleted.

30 changes: 0 additions & 30 deletions examples/ExpoChatGPTSample/README.md

This file was deleted.

32 changes: 0 additions & 32 deletions examples/ExpoChatGPTSample/app.json

This file was deleted.

94 changes: 0 additions & 94 deletions examples/ExpoChatGPTSample/app/_layout.tsx

This file was deleted.

6 changes: 0 additions & 6 deletions examples/ExpoChatGPTSample/app/index.tsx

This file was deleted.

Loading
Loading