Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4029214
feat: channel info screen - first iteration
szuperaz May 11, 2026
da46562
feat: run accessibility skill
szuperaz May 12, 2026
cc977b7
feat: add missing translations
szuperaz May 12, 2026
0338d5c
fix lint errors
szuperaz May 12, 2026
0549c48
fix isBlocked state update in useChannelActionItems
szuperaz May 12, 2026
7b589d4
fix lint
szuperaz May 12, 2026
3d91f1b
refactor: extract gather action items into a separate hook
szuperaz May 13, 2026
49a99ea
test: unit tests for channel details components
szuperaz May 13, 2026
b219341
fix error
szuperaz May 13, 2026
e1523f5
refactor: move useChannelActionItems dependencies to src/hooks
szuperaz May 13, 2026
f77e0b2
remove archive from channel action item ids
szuperaz May 13, 2026
17329f6
refactor: similify channel details screen props
szuperaz May 13, 2026
0654e27
add muted icon to channel details
szuperaz May 13, 2026
f6bb4ea
members list all screen, add button
szuperaz May 14, 2026
a61ec6e
remove Admin badge
szuperaz May 14, 2026
f7eb432
add channel members screen
szuperaz May 15, 2026
cb9ffaa
small fixes
szuperaz May 15, 2026
493b3be
Merge branch 'develop' into channel-info-screens
szuperaz May 15, 2026
d5c6c3c
refactor: move capabilities to ChannelDetails context
szuperaz May 15, 2026
30ee37c
fix lint
szuperaz May 15, 2026
2e3da00
Add notification for channel delete, add onSucess for channel actions
szuperaz May 18, 2026
8c06ef1
fix: missing translation
szuperaz May 18, 2026
83898b5
fix: typecheck error
szuperaz May 18, 2026
18bc287
fix: unit test
szuperaz May 18, 2026
5373a80
refactor: move addMembers to channel actions
szuperaz May 18, 2026
8473d36
refactor: read members from hook, update claude to run typecheck; fix…
szuperaz May 18, 2026
ed21df8
refactor: rename useChannelAddMembersUsers to useChannelAddMembers
szuperaz May 18, 2026
1c42b2c
refactor: split details member section into multiple (inline) components
szuperaz May 18, 2026
a1881d6
feat: RTL audit + new RTL skill
szuperaz May 18, 2026
fa51fb9
example: add composition example for channel details screen
szuperaz May 18, 2026
0e63619
Add useMemo to sample app
szuperaz May 18, 2026
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
10 changes: 6 additions & 4 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 12 locale files in `package/src/i18n/`. You can omit a11y keys if a button contains a text label that describes what it does.
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 Down Expand Up @@ -68,6 +68,7 @@ const a11yProps = useResolvedModalAccessibilityProps();
```

This returns:

- iOS: `{ accessibilityViewIsModal: true }`
- Android: `{ importantForAccessibility: 'yes' }`
- Either platform when `enabled` is false: `{}`
Expand All @@ -81,9 +82,7 @@ Mobile gestures (long-press, hold-to-record, pinch/pan) must have a tap-equivale
```tsx
const { audioRecorderTapMode } = useAccessibilityContext();
const screenReaderOn = useScreenReaderEnabled();
const useTapMode =
audioRecorderTapMode === 'always' ||
(audioRecorderTapMode === 'auto' && screenReaderOn);
const useTapMode = audioRecorderTapMode === 'always' || (audioRecorderTapMode === 'auto' && screenReaderOn);
```

Three-state semantics: `'auto'` (swap when SR is on), `'always'` (swap for everyone), `'never'` (integrator handles).
Expand All @@ -109,10 +108,12 @@ Disable spring animations and limit fade durations when this is true.
## Testing requirements per change

Minimum:

- Unit tests for new keyboard/focus/semantics behavior in nearest `__tests__/`.
- Use `@testing-library/react-native` semantic queries: `getByRole`, `getByLabelText`, `getByA11yState`, `getByA11yValue`.

Recommended for non-trivial changes:

- Render with `<OverlayProvider accessibility={{ enabled: true, forceScreenReaderMode: true }}>` and assert the accessible variant renders.
- Render with `<OverlayProvider accessibility={{ enabled: false }}>` and assert the legacy behavior is unchanged (no extra buttons, no listeners).

Expand Down Expand Up @@ -143,6 +144,7 @@ Recommended for non-trivial changes:
## Cross-SDK parity

API shapes mirror [`stream-chat-react#3146`](https://github.com/GetStream/stream-chat-react/pull/3146):

- `useAccessibilityAnnouncer` ≈ React's `useAriaLiveAnnouncer`
- `useIncomingMessageAnnouncements` — same params, same throttle/batch logic
- `a11y/*` i18n namespace shared
Expand Down
269 changes: 269 additions & 0 deletions .claude/skills/rtl/SKILL.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,12 @@ cd package && yarn lint-fix # Auto-fix lint and formatting issues
```bash
cd package && yarn test:unit # Run all unit tests (sets TZ=UTC)
cd package && yarn test:coverage # Run with coverage report
cd package && yarn test:typecheck # Type-check tests against tsconfig.test.json (run after any code change)
cd package && TZ=UTC npx jest path/to/test.test.tsx # Run a single test file
```

Always run `yarn test:typecheck` after making code changes — `yarn lint` and `yarn test:unit` do not catch all type errors.

Tests use Jest with `react-native` preset and `@testing-library/react-native`. Test files live alongside source at `src/**/__tests__/*.test.ts(x)`. Mock builders are in `src/mock-builders/`.

To run a single test, you can also temporarily add the file path to the `testRegex` array in `package/jest.config.js`.
Expand Down
20 changes: 13 additions & 7 deletions examples/SampleApp/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,13 @@ import { ChannelFilesScreen } from './src/screens/ChannelFilesScreen';
import { ChannelImagesScreen } from './src/screens/ChannelImagesScreen';
import { ChannelScreen } from './src/screens/ChannelScreen';
import { ChannelPinnedMessagesScreen } from './src/screens/ChannelPinnedMessagesScreen';
import { ChannelDetailsScreen } from './src/screens/ChannelDetailsScreen';
import { ChatScreen } from './src/screens/ChatScreen';
import { GroupChannelDetailsScreen } from './src/screens/GroupChannelDetailsScreen';
import { LoadingScreen } from './src/screens/LoadingScreen';
import { MenuDrawer } from './src/components/MenuDrawer';
import { NewDirectMessagingScreen } from './src/screens/NewDirectMessagingScreen';
import { NewGroupChannelAddMemberScreen } from './src/screens/NewGroupChannelAddMemberScreen';
import { NewGroupChannelAssignNameScreen } from './src/screens/NewGroupChannelAssignNameScreen';
import { OneOnOneChannelDetailScreen } from './src/screens/OneOnOneChannelDetailScreen';
import { SharedGroupsScreen } from './src/screens/SharedGroupsScreen';
import { ThreadScreen } from './src/screens/ThreadScreen';
import { UserSelectorScreen } from './src/screens/UserSelectorScreen';
Expand Down Expand Up @@ -66,6 +65,8 @@ import {
MessageListModeConfigItem,
MessageListPruningConfigItem,
} from './src/components/SecretMenu.tsx';
import { ChannelAllMembersScreen } from './src/screens/ChannelAllMembersScreen.tsx';
import { ChannelAddMembersScreen } from './src/screens/ChannelAddMembersScreen.tsx';

init({ data });

Expand Down Expand Up @@ -417,14 +418,19 @@ const HomeScreen = () => {
options={{ headerShown: false }}
/>
<Stack.Screen
component={OneOnOneChannelDetailScreen}
name='OneOnOneChannelDetailScreen'
component={ChannelDetailsScreen}
name='ChannelDetailsScreen'
options={{ headerShown: false }}
/>
<Stack.Screen
component={GroupChannelDetailsScreen}
name='GroupChannelDetailsScreen'
options={{ headerShown: false }}
component={ChannelAllMembersScreen}
name='ChannelAllMembersScreen'
options={{ headerShown: true }}
/>
<Stack.Screen
component={ChannelAddMembersScreen}
name='ChannelAddMembersScreen'
options={{ headerShown: true }}
/>
<Stack.Screen
component={ChannelImagesScreen}
Expand Down
76 changes: 76 additions & 0 deletions examples/SampleApp/src/screens/ChannelAddMembersScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import React, { useCallback, useMemo, useState } from 'react';

import type { RouteProp } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';

import type { StackNavigatorParamList } from '../types';
import {
ChannelAddMembers,
ChannelDetailsContext,
NotificationList,
NotificationTargetProvider,
useChannelActions,
} from 'stream-chat-react-native-core';
import type { UserResponse } from 'stream-chat';
import { KeyboardAvoidingView, Platform, Pressable, Text } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';

type ChannelAddMembersScreenRouteProp = RouteProp<
StackNavigatorParamList,
'ChannelAddMembersScreen'
>;

type ChannelAddMembersScreenNavigationProp = NativeStackNavigationProp<
StackNavigatorParamList,
'ChannelAddMembersScreen'
>;

type Props = {
navigation: ChannelAddMembersScreenNavigationProp;
route: ChannelAddMembersScreenRouteProp;
};

export const ChannelAddMembersScreen: React.FC<Props> = ({
navigation,
route: {
params: { channel },
},
}) => {
const { addMembers } = useChannelActions(channel);
const [selectedUsers, setSelectedUsers] = useState<UserResponse[]>([]);

const channelDetailsContextValue = useMemo(() => ({ channel }), [channel]);

const goBack = useCallback(() => navigation.goBack(), [navigation]);

const onSavePress = useCallback(async () => {
await addMembers(
selectedUsers.map((user) => user.id),
{
onSuccess: () => {
setSelectedUsers([]);
goBack();
},
},
);
}, [addMembers, selectedUsers, goBack]);

return (
<ChannelDetailsContext.Provider value={channelDetailsContextValue}>
<KeyboardAvoidingView
behavior={Platform.OS === 'ios' ? 'padding' : undefined}
style={{ flex: 1 }}
>
<SafeAreaView edges={['bottom']} style={{ flex: 1 }}>
<NotificationTargetProvider hostId={channel.cid} panel='channel-details'>
<ChannelAddMembers onSelectionChange={setSelectedUsers} />
<Pressable onPress={onSavePress} style={{ alignItems: 'center', padding: 16 }}>
<Text>Save</Text>
</Pressable>
<NotificationList />
</NotificationTargetProvider>
</SafeAreaView>
</KeyboardAvoidingView>
</ChannelDetailsContext.Provider>
);
};
36 changes: 36 additions & 0 deletions examples/SampleApp/src/screens/ChannelAllMembersScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React, { useMemo } from 'react';

import type { RouteProp } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';

import type { StackNavigatorParamList } from '../types';
import { ChannelDetailsContext, ChannelDetailsMemberList } from 'stream-chat-react-native-core';

type ChannelAllMembersScreenRouteProp = RouteProp<
StackNavigatorParamList,
'ChannelAllMembersScreen'
>;

type ChannelAllMembersScreenNavigationProp = NativeStackNavigationProp<
StackNavigatorParamList,
'ChannelAllMembersScreen'
>;

type Props = {
navigation: ChannelAllMembersScreenNavigationProp;
route: ChannelAllMembersScreenRouteProp;
};

export const ChannelAllMembersScreen: React.FC<Props> = ({
route: {
params: { channel },
},
}) => {
const channelDetailsContextValue = useMemo(() => ({ channel }), [channel]);

return (
<ChannelDetailsContext.Provider value={channelDetailsContextValue}>
<ChannelDetailsMemberList />
</ChannelDetailsContext.Provider>
);
};
64 changes: 64 additions & 0 deletions examples/SampleApp/src/screens/ChannelDetailsScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useCallback } from 'react';

import type { RouteProp } from '@react-navigation/native';
import type { NativeStackNavigationProp } from '@react-navigation/native-stack';

import { ChannelDetailsScreen as StreamChannelDetailsScreen } from 'stream-chat-react-native';

import type { StackNavigatorParamList } from '../types';

type ChannelDetailsScreenRouteProp = RouteProp<StackNavigatorParamList, 'ChannelDetailsScreen'>;

type ChannelDetailsScreenNavigationProp = NativeStackNavigationProp<
StackNavigatorParamList,
'ChannelDetailsScreen'
>;

type Props = {
navigation: ChannelDetailsScreenNavigationProp;
route: ChannelDetailsScreenRouteProp;
};

export const ChannelDetailsScreen: React.FC<Props> = ({
navigation,
route: {
params: { channel },
},
}) => {
const onBack = useCallback(() => navigation.goBack(), [navigation]);
const popToRoot = useCallback(
() =>
navigation.reset({
index: 0,
routes: [{ name: 'MessagingScreen' }],
}),
[navigation],
);
const onViewAllMembersPress = useCallback(() => {
navigation.navigate('ChannelAllMembersScreen', { channel });
}, [navigation, channel]);

const onAddMembersPress = useCallback(() => {
navigation.navigate('ChannelAddMembersScreen', { channel });
}, [navigation, channel]);

return (
<>
{(channel.data?.member_count ?? 0) % 2 === 0 ? (
<StreamChannelDetailsScreen
channel={channel}
onBack={onBack}
onChannelDismiss={popToRoot}
/>
) : (
<StreamChannelDetailsScreen
channel={channel}
onBack={onBack}
onChannelDismiss={popToRoot}
onViewAllMembersPress={onViewAllMembersPress}
onAddMembersPress={onAddMembersPress}
/>
)}
</>
);
};
14 changes: 4 additions & 10 deletions examples/SampleApp/src/screens/ChannelListScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -133,21 +133,15 @@ export const ChannelListScreen: React.FC = () => {
);

const getChannelActionItems = useStableCallback(
({ context: { isDirectChat, channel }, defaultItems }) => {
({ context: { channel }, defaultItems }) => {
const viewInfo = () => {
if (!channel) {
return;
}
if (navigation) {
if (isDirectChat) {
navigation.navigate('OneOnOneChannelDetailScreen', {
channel,
});
} else {
navigation.navigate('GroupChannelDetailsScreen', {
channel,
});
}
navigation.navigate('ChannelDetailsScreen', {
channel,
});
}
};

Expand Down
19 changes: 4 additions & 15 deletions examples/SampleApp/src/screens/ChannelScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,6 @@ const ChannelHeader: React.FC<ChannelHeaderProps> = ({ channel }) => {
const { chatClient } = useAppContext();
const navigation = useNavigation<ChannelScreenNavigationProp>();

const isOneOnOneConversation =
channel &&
Object.values(channel.state.members).length === 2 &&
channel.id?.indexOf('!members-') === 0;

const onBackPress = useCallback(() => {
if (!navigation.canGoBack()) {
// if no previous screen was present in history, go to the list screen
Expand All @@ -75,16 +70,10 @@ const ChannelHeader: React.FC<ChannelHeaderProps> = ({ channel }) => {

const onRightContentPress = useCallback(() => {
closePicker();
if (isOneOnOneConversation) {
navigation.navigate('OneOnOneChannelDetailScreen', {
channel,
});
} else {
navigation.navigate('GroupChannelDetailsScreen', {
channel,
});
}
}, [channel, closePicker, isOneOnOneConversation, navigation]);
navigation.navigate('ChannelDetailsScreen', {
channel,
});
}, [channel, closePicker, navigation]);

if (!channel || !chatClient) {
return null;
Expand Down
Loading
Loading