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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
85 changes: 83 additions & 2 deletions .claude/skills/accessibility/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Use this skill whenever code changes can affect screen-reader users (VoiceOver o
## Non-negotiable rules

1. **Native semantics first.** Use `Pressable`, `TextInput`, `Switch`, `Image` directly. Use `accessibilityRole` only when native semantics cannot represent the widget (`menu`, `menuitem`, `progressbar`, `radio`, `checkbox`, `article`, `alert`, `tablist`, `tab`).
2. **Never hardcode English** in `accessibilityLabel`/`accessibilityHint`/announcement strings. For SDK `Button`, pass `accessibilityLabelKey='a11y/...'` (and `accessibilityLabelParams` when needed). For non-Button components, use `useA11yLabel('a11y/...', params)` or `t('a11y/...')` directly when you don't need the disabled-state short-circuit. Add the key to all 12 locale files in `package/src/i18n/`.
2. **Never hardcode English** in `accessibilityLabel`/`accessibilityHint`/announcement strings. For SDK `Button`, pass `accessibilityLabelKey='a11y/...'` (and `accessibilityLabelParams` when needed). For non-Button components, use `useA11yLabel('a11y/...', params)` or `t('a11y/...')` directly when you don't need the disabled-state short-circuit. Add the key to all 13 locale files in `package/src/i18n/` (`ar, en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`).
3. **Gate behavior on `useAccessibilityContext().enabled`.** A11y is opt-in. New listeners, subscriptions, and announcer mounts must be no-ops when `enabled` is false. New `accessibilityRole`/`accessibilityState` props are fine to render unconditionally — they cost ~zero.
4. **One focusable target per action.** Don't nest `Pressable` inside `Pressable`. Mark inner decorative views with `accessibilityElementsHidden` (iOS) + `importantForAccessibility='no-hide-descendants'` (Android) so the parent carries the label.
5. **Decorative visuals stay hidden from AT.** Icon-only buttons must carry an `accessibilityLabel` on the wrapper, and the SVG icon should be hidden.
Expand All @@ -21,7 +21,7 @@ Use this skill whenever code changes can affect screen-reader users (VoiceOver o
- **Foundation primitives** → `package/src/a11y/` (utilities + low-level hooks).
- **Runtime announcer infra** → `package/src/components/Accessibility/` (`NotificationAnnouncer`, `useAccessibilityAnnouncer`, `useIncomingMessageAnnouncements`).
- **Config + provider** → `package/src/contexts/accessibilityContext/`, mounted by `OverlayProvider`.
- **i18n** → `a11y/*` keys in all 12 locale JSONs (`en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`).
- **i18n** → `a11y/*` keys in all 13 locale JSONs (`ar, en, es, fr, he, hi, it, ja, ko, nl, pt-br, ru, tr`).
- **Component-level a11y attributes** → in the component itself.
- **Platform divergence (iOS vs Android)** → use `Platform.OS` or `useResolvedModalAccessibilityProps`. Don't duplicate the file — RN doesn't need `.ios.tsx`/`.android.tsx` splits for a11y.
- **Tests** → nearest `__tests__/` folder; use `@testing-library/react-native` semantic queries (`getByRole`, `getByLabelText`).
Expand Down Expand Up @@ -97,6 +97,81 @@ const transitionDuration = reduceMotion ? 0 : 250;

Disable spring animations and limit fade durations when this is true.

### 6) Curated single focus stop for visual content — `CompositeAccessibilityProbe`

```tsx
import { CompositeAccessibilityProbe } from 'stream-chat-react-native';

<CompositeAccessibilityProbe label={accessibilityLabel}>
{/* avatars, icons, composed graphics — visually decorative */}
</CompositeAccessibilityProbe>
```

Wraps non-Text visual content with a single, cross-platform-stable focus stop carrying the provided `label`. Renders a hidden `Text` sibling that carries the label + a `View accessibilityElementsHidden importantForAccessibility='no-hide-descendants'` around the children. Use for avatars, mute icons, isolated badges, composed graphics that should announce as one semantic unit.

Pass the result of `useA11yLabel(...)` directly — when `label` is `undefined` (a11y opt-out), the probe is a no-op and renders children untouched.

Live examples: `ChannelAvatar.tsx`, `ChannelPreviewMutedStatus.tsx`, `ChannelMessagePreviewDeliveryStatus.tsx`.

### 7) Splicing extra a11y info into compose — `HiddenA11yText`

```tsx
import { HiddenA11yText } from 'stream-chat-react-native';

<Pressable>
<Icon />
{selected ? <HiddenA11yText label={useA11yLabel('a11y/you reacted')} /> : null}
</Pressable>
```

A visually-invisible `<Text>` that exists only to contribute extra information to a parent's compose loop. Use it to splice in supplementary state ("you reacted", "and N more", "unread") that doesn't have a natural visible Text in the tree.

Different concern from `CompositeAccessibilityProbe`:
- `HiddenA11yText` — "inject extra a11y-only text into a compose chain"
- `CompositeAccessibilityProbe` — "make this whole visual element one focus stop with a curated label"

Live examples: `MessageStatus.tsx`, `ReactionListClustered.tsx`, `ReactionListItem.tsx`.

### 8) Cross-platform auto-compose on a plain View

```tsx
<View accessible accessibilityRole='text'>
{/* children whose labels should auto-compose into one announcement */}
</View>
```

iOS auto-composes descendant labels when a `View` is `accessible={true}` without an explicit `accessibilityLabel`. Android requires the parent to trip a gate — set any of `accessibilityRole`, `accessibilityState`, `accessibilityActions`, or `accessibilityLabelledBy`. `accessibilityRole='text'` (or `'none'`) is the lightest gate-tripper and a no-op for iOS composition.

`Pressable` defaults `accessibilityRole='button'`, so it auto-trips the gate. Plain `View accessible={true}` without a role does NOT — Android falls back to its default heuristic (reads one visible Text descendant only).

Live example: `MessageFooter.tsx` — `<View accessible accessibilityRole='text'>` makes the footer one focus stop on both platforms reading `"Read 11:05 AM"`.

See full memory: `rn_android_a11y_compose_gate.md`.

### 9) Drill-in for interactive children inside a Pressable

```tsx
<Pressable accessible={hasInteractiveContent ? false : undefined} onLongPress={...}>
{/* mix of interactive children — attachments, quoted reply, poll options, etc. */}
</Pressable>
```

When a Pressable wraps mixed content that includes interactive children, the row's default single-focus-stop behavior subsumes them — screen-reader users can't activate the children individually. Setting `accessible={false}` on the Pressable removes the row stop, so VO/TalkBack drill into each interactive child. The Pressable's `onPress` / `onLongPress` still fire because VO/TalkBack synthesize taps at the focused child's coordinates, which land inside the Pressable's hit area.

Live example: `MessageContent.tsx` — `accessible={hasInteractiveContent ? false : undefined}` where `hasInteractiveContent` covers poll, quoted message, attachments, shared location.

### 10) Reshow announcements — `useAnnounceOnShow`

```tsx
useAnnounceOnShow(visible, useA11yLabel('a11y/Replying to {{user}}', { user: name }));
```

Announces `label` once each time `visible` flips from `false` to `true`. Resets on hide, so reshows re-announce — unlike `useAnnounceOnStateChange` which dedupes consecutive identical strings.

Use for transient surfaces that appear and disappear repeatedly within a session (modals, autocomplete pickers, reply previews) where the user benefits from hearing the affordance on every reappearance.

Live example: `Reply.tsx` — fires when a reply preview shows in the composer.

## Anti-patterns to avoid

- **Hardcoded English `accessibilityLabel`** strings inside component code. For SDK `Button`, use `accessibilityLabelKey='a11y/...'`; otherwise use `useA11yLabel('a11y/...')` or `t('a11y/...')`.
Expand Down Expand Up @@ -134,11 +209,17 @@ Recommended for non-trivial changes:

- `package/src/contexts/accessibilityContext/AccessibilityContext.tsx` — config schema + provider + imperative announcer context.
- `package/src/components/Accessibility/hooks/useIncomingMessageAnnouncements.ts` — port of stream-chat-react's hook.
- `package/src/components/Accessibility/CompositeAccessibilityProbe.tsx` — curated-single-focus-stop wrapper for visual content (avatar, icons, badges).
- `package/src/components/Accessibility/HiddenA11yText.tsx` — visually-invisible Text that splices extra info into a parent's compose chain ("you reacted", "and N more", etc).
- `package/src/a11y/hooks/useA11yLabel.ts` — translated-label-or-undefined.
- `package/src/a11y/hooks/useAnnounceOnStateChange.ts` — announce on string-change with dedup.
- `package/src/a11y/hooks/useAnnounceOnShow.ts` — announce on `visible: false → true` transitions, resets on hide (no dedup).
- `package/src/a11y/hooks/useResolvedModalAccessibilityProps.ts` — modal a11y props.
- `package/src/components/ui/Avatar/Avatar.tsx` — example of `name` + `useA11yLabel` usage.
- `package/src/components/UIComponents/BottomSheetModal.tsx` — example of `useResolvedModalAccessibilityProps`.
- `package/src/components/AITypingIndicatorView/AITypingIndicatorView.tsx` — example of `useAnnounceOnStateChange`.
- `package/src/components/Message/MessageItemView/MessageFooter.tsx` — example of cross-platform auto-compose on a View (`accessible + accessibilityRole='text'`).
- `package/src/components/Message/MessageItemView/MessageContent.tsx` — example of conditional drill-in (`accessible={hasInteractiveContent ? false : undefined}`).

## Cross-SDK parity

Expand Down
File renamed without changes.
20 changes: 3 additions & 17 deletions .github/actions/install-and-build-sdk/action.yml
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
name: 'Install and Build SDK'
description: 'Runs yarn install for all the packages and sample and fails if yarn lock has a change that is not committed'
description: 'Runs yarn install at the workspace root and fails if yarn.lock has uncommitted changes'
runs:
using: 'composite'
steps:
- name: Install Root repo dependencies
run: yarn --frozen-lockfile
shell: bash
- name: Install & Build the Core Package
run: |
cd package/
yarn --frozen-lockfile
shell: bash
- name: Install & Build the Native Package
run: |
cd package/native-package/
yarn
shell: bash
- name: Install & Build the Sample App
working-directory: examples/SampleApp
run: yarn
- name: Install workspace dependencies
run: yarn install --immutable
shell: bash
31 changes: 11 additions & 20 deletions .github/workflows/changelog-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ jobs:
matrix:
node-version: [ 24.x ]
steps:
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- name: Install Linux build tools
run: sudo apt-get update && sudo apt-get install -y build-essential
- uses: actions/checkout@v6
with:
ref: develop
Expand All @@ -31,15 +25,12 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Install Linux build tools
run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Installation
run: |
yarn --frozen-lockfile
cd package/
yarn --frozen-lockfile
cd native-package/
yarn
cd ../../examples/SampleApp/
yarn
run: yarn install --immutable

- name: Generate Changelog
id: generate_changelog
Expand All @@ -54,13 +45,13 @@ jobs:

echo "Changelog file ready! Setting up outputs"
CHANGELOG_PREVIEW=$(cat NEXT_RELEASE_CHANGELOG.md)

CHANGELOG_PREVIEW_ESCAPED="${CHANGELOG_PREVIEW//'%'/'%25'}"
CHANGELOG_PREVIEW_ESCAPED="${CHANGELOG_PREVIEW_ESCAPED//$'\n'/'%0A'}"
CHANGELOG_PREVIEW_ESCAPED="${CHANGELOG_PREVIEW_ESCAPED//$'\r'/'%0D'}"

echo "::set-output name=exists::true"
echo "::set-output name=preview::$CHANGELOG_PREVIEW_ESCAPED"
echo "exists=true" >> "$GITHUB_OUTPUT"
{
echo "preview<<CHANGELOG_PREVIEW_EOF"
echo "$CHANGELOG_PREVIEW"
echo "CHANGELOG_PREVIEW_EOF"
} >> "$GITHUB_OUTPUT"
fi
- uses: marocchino/sticky-pull-request-comment@v3
if: steps.generate_changelog.outputs.exists == 'true'
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/check-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Install Linux build tools
run: sudo apt-get update && sudo apt-get install -y build-essential
- name: Install && Build - SDK and Sample App
uses: ./.github/actions/install-and-build-sdk
- name: Lint
run: yarn lerna-workspaces run lint
run: yarn lint
- name: Typecheck tests
run: cd package && yarn test:typecheck
run: yarn workspace stream-chat-react-native-core test:typecheck
- name: Test
run: yarn test:coverage
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
with:
node-version: ${{ matrix.node-version }}
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Install Linux build tools
run: sudo apt-get update && sudo apt-get install -y build-essential

Expand All @@ -43,7 +45,7 @@ jobs:
uses: ./.github/actions/install-and-build-sdk

- name: Lint
run: yarn lerna-workspaces run lint
run: yarn lint

- name: Test
if: github.ref == 'refs/heads/develop'
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/sample-distribution.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,13 @@ jobs:
uses: webfactory/ssh-agent@v0.10.0
with:
ssh-private-key: ${{ secrets.BOT_SSH_PRIVATE_KEY }}
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@v6
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: '26.2' # Update as needed
Expand Down Expand Up @@ -64,11 +66,13 @@ jobs:
matrix:
node-version: [24.x]
steps:
- uses: actions/checkout@v6
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node-version }}
- uses: actions/checkout@v6
cache: 'yarn'
cache-dependency-path: 'yarn.lock'
- name: Install Linux build tools
run: sudo apt-get update && sudo apt-get install -y build-essential
- uses: actions/setup-java@v5
Expand Down
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@
!.yarn/sdks
!.yarn/versions

# In case someone runs `yarn install` inside a workspace child, Yarn 4 will
# emit a transient .yarn/install-state.gz there; the root-anchored pattern
# above doesn't catch nested copies.
**/.yarn/install-state.gz
**/.yarn/cache

node_modules
NEXT_RELEASE_CHANGELOG.md
artifacts
Expand Down
4 changes: 0 additions & 4 deletions .husky/commit-msg
Original file line number Diff line number Diff line change
@@ -1,5 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx commitlint --edit $1
npx commitlint --edit $1
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

dotgit/hooks/pre-commit-format.sh && dotgit/hooks/pre-commit-reject-binaries.py
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v22
v24
40 changes: 39 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,45 @@
# Dependencies / build artifacts
**/node_modules/
**/build/
**/dist/
**/lib/
**/.expo/
**/.gradle/
**/vendor/
**/ios/build/
**/ios/Pods/
**/android/build/
**/android/app/build/
**/coverage/

# Native scaffolding (not in this project's purview)
**/ios/
**/android/
**/fastlane/
examples/SampleApp/patches/

# Lockfiles & generated metadata
**/*.lock
**/Podfile.lock
.yarn/
yarn.lock

# Generated SDK assets
package/src/components/docs/
package/lib/
package/src/theme/generated/

# Workflow / tooling configs not owned by this project's Prettier
.github/
.husky/
dotgit/

# Repo metadata & docs that don't follow Prettier rules
.claude/
ai-docs/
AGENTS.md
CLAUDE.md
PULL_REQUEST_TEMPLATE.md
RELEASE_PROCESS.md
SECURITY.md
**/CHANGELOG.md
**/.watchmanconfig
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
diff --git a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useBoundDetection.js b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useBoundDetection.js
index 767fc5d..f8d4644 100644
--- a/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useBoundDetection.js
+++ b/node_modules/@shopify/flash-list/dist/recyclerview/hooks/useBoundDetection.js
diff --git a/dist/recyclerview/hooks/useBoundDetection.js b/dist/recyclerview/hooks/useBoundDetection.js
index 767fc5da251d53621cb19615ee9bfdacf7609284..f8d4644b4e3344eeecedc7f02b3b97d3a04e6df4 100644
--- a/dist/recyclerview/hooks/useBoundDetection.js
+++ b/dist/recyclerview/hooks/useBoundDetection.js
@@ -100,6 +100,14 @@ export function useBoundDetection(recyclerViewManager, scrollViewRef) {
}
}, [recyclerViewManager]);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
diff --git a/node_modules/expo-audio/android/src/main/java/expo/modules/audio/AudioModule.kt b/node_modules/expo-audio/android/src/main/java/expo/modules/audio/AudioModule.kt
index 72a6e9e..6e12fa5 100644
--- a/node_modules/expo-audio/android/src/main/java/expo/modules/audio/AudioModule.kt
+++ b/node_modules/expo-audio/android/src/main/java/expo/modules/audio/AudioModule.kt
@@ -27,6 +27,9 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
diff --git a/android/src/main/java/expo/modules/audio/AudioModule.kt b/android/src/main/java/expo/modules/audio/AudioModule.kt
index 992239ba7631ce0c97c0b41c5fbf61cc842386fe..ebc0933beaf40414887ff634029ca8eb0d0558c9 100644
--- a/android/src/main/java/expo/modules/audio/AudioModule.kt
+++ b/android/src/main/java/expo/modules/audio/AudioModule.kt
@@ -28,6 +28,9 @@ import androidx.media3.exoplayer.hls.HlsMediaSource
import androidx.media3.exoplayer.smoothstreaming.SsMediaSource
import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.ProgressiveMediaSource
Expand All @@ -12,7 +12,7 @@ index 72a6e9e..6e12fa5 100644
import expo.modules.interfaces.permissions.Permissions
import expo.modules.kotlin.Promise
import expo.modules.kotlin.exception.Exceptions
@@ -851,7 +854,12 @@ class AudioModule : Module() {
@@ -931,7 +934,12 @@ class AudioModule : Module() {
CONTENT_TYPE_SS -> SsMediaSource.Factory(factory)
CONTENT_TYPE_DASH -> DashMediaSource.Factory(factory)
CONTENT_TYPE_HLS -> HlsMediaSource.Factory(factory)
Expand All @@ -26,10 +26,10 @@ index 72a6e9e..6e12fa5 100644
else -> throw IllegalStateException("Unsupported type: $type")
}.createMediaSource(mediaItem)
}
diff --git a/node_modules/expo-audio/expo-module.config.json b/node_modules/expo-audio/expo-module.config.json
index 52e634b..9a131dd 100644
--- a/node_modules/expo-audio/expo-module.config.json
+++ b/node_modules/expo-audio/expo-module.config.json
diff --git a/expo-module.config.json b/expo-module.config.json
index 9ee5fdcd7a6e698c8cdcf42ee137239220aec190..9a131dd948d2eb5adf3cf9b759829a0e8fed15a1 100644
--- a/expo-module.config.json
+++ b/expo-module.config.json
@@ -4,12 +4,6 @@
"modules": ["AudioModule"]
},
Expand All @@ -38,7 +38,7 @@ index 52e634b..9a131dd 100644
- "publication": {
- "groupId": "expo.modules.audio",
- "artifactId": "expo.modules.audio",
- "version": "55.0.14",
- "version": "56.0.11",
- "repository": "local-maven-repo"
- }
+ "modules": ["expo.modules.audio.AudioModule"]
Expand Down
Loading
Loading