Skip to content

Commit 989a7a3

Browse files
committed
refactor: add isMuted check to member iteme
1 parent 7c925e0 commit 989a7a3

18 files changed

Lines changed: 152 additions & 98 deletions

package/src/components/ChannelDetailsScreen/__tests__/ChannelDetailsActionsSection.test.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { act, fireEvent, render, screen } from '@testing-library/react-native';
55
import type { Channel } from 'stream-chat';
66

77
import { ChannelDetailsContextProvider } from '../../../contexts/channelDetailsContext/channelDetailsContext';
8+
import { ChatContext } from '../../../contexts/chatContext/ChatContext';
89
import { WithComponents } from '../../../contexts/componentsContext/ComponentsContext';
910
import { ThemeProvider } from '../../../contexts/themeContext/ThemeContext';
1011
import { defaultTheme } from '../../../contexts/themeContext/utils/theme';
@@ -59,11 +60,23 @@ const sectionElement = () => (
5960
userLanguage: 'en',
6061
}}
6162
>
62-
<ChannelDetailsContextProvider value={{ channel }}>
63-
<WithComponents overrides={{ ChannelDetailsActionItem: ActionItemProbe }}>
64-
<ChannelDetailsActionsSection />
65-
</WithComponents>
66-
</ChannelDetailsContextProvider>
63+
<ChatContext.Provider
64+
value={
65+
{
66+
client: {
67+
mutedUsers: [],
68+
on: () => ({ unsubscribe: () => undefined }),
69+
userID: 'me',
70+
},
71+
} as never
72+
}
73+
>
74+
<ChannelDetailsContextProvider value={{ channel }}>
75+
<WithComponents overrides={{ ChannelDetailsActionItem: ActionItemProbe }}>
76+
<ChannelDetailsActionsSection />
77+
</WithComponents>
78+
</ChannelDetailsContextProvider>
79+
</ChatContext.Provider>
6780
</TranslationProvider>
6881
</ThemeProvider>
6982
);
@@ -256,7 +269,7 @@ describe('ChannelDetailsActionsSection', () => {
256269
it('reflects userMuted state on the muteUser item Switch in direct chats', () => {
257270
useIsDirectChatSpy.mockReturnValue(true);
258271
getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
259-
useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' } }]);
272+
useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' }, user: { id: 'me' } }]);
260273
useActionItemsSpy.mockReturnValue([buildItem({ id: 'muteUser', label: 'Unmute User' })]);
261274
renderSection();
262275
const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');
@@ -297,7 +310,7 @@ describe('ChannelDetailsActionsSection', () => {
297310
fireEvent(userMuteSwitch, 'valueChange', true);
298311
expect(screen.getByTestId('channel-details-action-muteUser-switch').props.value).toBe(true);
299312
// A server event reports the user as muted before the request resolves.
300-
useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' } }]);
313+
useMutedUsersSpy.mockReturnValue([{ target: { id: 'other-user' }, user: { id: 'me' } }]);
301314
rerender(sectionElement());
302315
// The request fails: revert to the current hook value (true), not !value (false).
303316
act(() => {
@@ -309,7 +322,7 @@ describe('ChannelDetailsActionsSection', () => {
309322
it('userMuted is false when the other user is not in mutedUsers', () => {
310323
useIsDirectChatSpy.mockReturnValue(true);
311324
getOtherUserSpy.mockReturnValue({ user: { id: 'other-user' } });
312-
useMutedUsersSpy.mockReturnValue([{ target: { id: 'someone-else' } }]);
325+
useMutedUsersSpy.mockReturnValue([{ target: { id: 'someone-else' }, user: { id: 'me' } }]);
313326
useActionItemsSpy.mockReturnValue([buildItem({ id: 'muteUser', label: 'Mute User' })]);
314327
renderSection();
315328
const userMuteSwitch = screen.getByTestId('channel-details-action-muteUser-switch');

package/src/components/ChannelDetailsScreen/__tests__/members/ChannelMemberActionsSheet.test.tsx

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -153,21 +153,30 @@ describe('ChannelMemberActionsSheet', () => {
153153
actionsSpy.mockReturnValue([]);
154154

155155
render(
156-
<ThemeProvider theme={defaultTheme}>
157-
<TranslationProvider
158-
value={{
159-
t: ((key: string) => key) as never,
160-
tDateTimeParser: ((input: unknown) => input) as never,
161-
userLanguage: 'en',
162-
}}
163-
>
164-
<ChannelDetailsContextProvider value={{ channel, getChannelMemberActionItems }}>
165-
<WithComponents overrides={{ ChannelDetailsActionItem: ActionItemProbe }}>
166-
<ChannelMemberActionsSheet member={member} onClose={jest.fn()} visible />
167-
</WithComponents>
168-
</ChannelDetailsContextProvider>
169-
</TranslationProvider>
170-
</ThemeProvider>,
156+
<ChatContext.Provider
157+
value={
158+
{
159+
client: { userID: 'me', mutedUsers: [], on: () => ({ unsubscribe: () => undefined }) },
160+
} as never
161+
}
162+
>
163+
<ThemeProvider theme={defaultTheme}>
164+
<TranslationProvider
165+
value={{
166+
t: ((key: string) => key) as never,
167+
tDateTimeParser: ((input: unknown) => input) as never,
168+
userLanguage: 'en',
169+
}}
170+
>
171+
<ChannelDetailsContextProvider value={{ channel, getChannelMemberActionItems }}>
172+
<WithComponents overrides={{ ChannelDetailsActionItem: ActionItemProbe }}>
173+
<ChannelMemberActionsSheet member={member} onClose={jest.fn()} visible />
174+
</WithComponents>
175+
</ChannelDetailsContextProvider>
176+
</TranslationProvider>
177+
</ThemeProvider>
178+
,
179+
</ChatContext.Provider>,
171180
);
172181

173182
expect(actionsSpy).toHaveBeenCalledWith({

package/src/components/ChannelDetailsScreen/__tests__/members/ChannelMemberItem.test.tsx

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ const renderRow = ({
3737
channel = defaultChannel,
3838
currentUserId,
3939
getMemberRoleLabel,
40+
mutedUsers = [],
4041
...props
4142
}: React.ComponentProps<typeof ChannelMemberItem> & {
4243
channel?: Channel;
4344
currentUserId?: string;
4445
getMemberRoleLabel?: GetMemberRoleLabel;
46+
mutedUsers?: Array<{ target: { id: string }; user: { id: string } }>;
4547
}) =>
4648
render(
4749
<ThemeProvider theme={defaultTheme}>
@@ -57,7 +59,17 @@ const renderRow = ({
5759
userLanguage: 'en',
5860
}}
5961
>
60-
<ChatContext.Provider value={{ client: { userID: currentUserId } } as never}>
62+
<ChatContext.Provider
63+
value={
64+
{
65+
client: {
66+
mutedUsers,
67+
on: () => ({ unsubscribe: () => undefined }),
68+
userID: currentUserId,
69+
},
70+
} as never
71+
}
72+
>
6173
<ChannelDetailsContextProvider value={{ channel, getMemberRoleLabel }}>
6274
<ChannelMemberItem {...props} />
6375
</ChannelDetailsContextProvider>
@@ -91,14 +103,22 @@ describe('ChannelMemberItem accessibility', () => {
91103
});
92104

93105
it('includes "Muted" in the accessible label when the member is muted', () => {
94-
renderRow({ isMuted: true, member: memberFor() });
106+
renderRow({
107+
currentUserId: 'me',
108+
member: memberFor(),
109+
mutedUsers: [{ target: { id: 'alice' }, user: { id: 'me' } }],
110+
});
95111
expect(screen.getByLabelText('Alice, Muted, Offline')).toBeTruthy();
96112
});
97113
});
98114

99115
describe('ChannelMemberItem muted indicator', () => {
100116
it('renders the muted icon when the member is muted', () => {
101-
renderRow({ isMuted: true, member: memberFor() });
117+
renderRow({
118+
currentUserId: 'me',
119+
member: memberFor(),
120+
mutedUsers: [{ target: { id: 'alice' }, user: { id: 'me' } }],
121+
});
102122
expect(screen.getByTestId('channel-member-muted-indicator')).toBeTruthy();
103123
});
104124

package/src/components/ChannelDetailsScreen/components/ChannelDetailsActionsSection.tsx

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import { getOtherUserInDirectChannel } from '../../../hooks/actions/useChannelAc
99
import { useIsDirectChat } from '../../../hooks/useIsDirectChat';
1010
import { primitives } from '../../../theme';
1111
import { useRtlMirrorSwitchStyle } from '../../../utils/rtlMirrorSwitchStyle';
12-
import { useMutedUsers } from '../../ChannelList/hooks/useMutedUsers';
1312
import { useIsChannelMuted } from '../../ChannelPreview/hooks/useIsChannelMuted';
13+
import { useUserMuteActive } from '../../Message/hooks/useUserMuteActive';
1414
import { useChannelDetailsActionItems } from '../hooks';
1515

1616
const ChannelMuteToggleRow = ({ item }: { item: ChannelActionItem }) => {
@@ -62,12 +62,8 @@ const UserMuteToggleRow = ({ item }: { item: ChannelActionItem }) => {
6262
const isDirect = useIsDirectChat(channel);
6363
const rtlMirrorSwitchStyle = useRtlMirrorSwitchStyle();
6464
const switchColors = useSwitchColors();
65-
const mutedUsers = useMutedUsers(channel);
66-
const otherUserId = isDirect ? getOtherUserInDirectChannel(channel)?.user?.id : undefined;
67-
const userMuted =
68-
isDirect && !!otherUserId
69-
? mutedUsers.some((mutedUser) => mutedUser.target.id === otherUserId)
70-
: false;
65+
const otherUser = isDirect ? getOtherUserInDirectChannel(channel)?.user : undefined;
66+
const userMuted = useUserMuteActive(otherUser);
7167
const [isUserMuted, setIsUserMuted] = useState(userMuted);
7268
const userMutedRef = useRef(userMuted);
7369

package/src/components/ChannelDetailsScreen/components/ChannelDetailsMemberSection.tsx

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { useComponentsContext } from '../../../contexts/componentsContext/Compon
1111
import { useTheme } from '../../../contexts/themeContext/ThemeContext';
1212
import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
1313
import { useChannelOwnCapabilities } from '../../../hooks/useChannelOwnCapabilities';
14-
import { useMutedMemberIds } from '../../../hooks/useMutedMemberIds';
1514
import { primitives } from '../../../theme';
1615
import { Button } from '../../ui/Button/Button';
1716
import { useChannelDetailsMembersPreview } from '../hooks/useChannelDetailsMembersPreview';
@@ -38,7 +37,6 @@ export const ChannelDetailsMemberSection = () => {
3837
} = useTheme();
3938
const { ChannelMemberActionsSheet, ChannelMemberItem } = useComponentsContext();
4039
const { hasMore, total, visible } = useChannelDetailsMembersPreview(channel);
41-
const mutedMemberIds = useMutedMemberIds(channel);
4240
const styles = useStyles();
4341
const [isMemberListVisible, setMemberListVisible] = useState(false);
4442
const [isAddMembersVisible, setAddMembersVisible] = useState(false);
@@ -113,7 +111,6 @@ export const ChannelDetailsMemberSection = () => {
113111
if (!member.user?.id) return null;
114112
return (
115113
<ChannelMemberItem
116-
isMuted={mutedMemberIds.has(member.user.id)}
117114
key={member.user.id}
118115
member={member}
119116
onPress={() => handleMemberPress(member)}

package/src/components/ChannelDetailsScreen/components/ChannelEditName.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ export const ChannelEditName = ({ onNameChange }: ChannelEditNameProps) => {
3131

3232
const handleNameChange = useCallback(
3333
(newName: string) => {
34-
console.log('newName', newName);
3534
setName(newName);
3635
onNameChange(newName);
3736
},

package/src/components/ChannelDetailsScreen/components/members/ChannelMemberActionsSheet.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
ChannelMemberActionItem,
1111
useChannelMemberActionItems,
1212
} from '../../../../hooks/actions/useChannelMemberActionItems';
13-
import { useMutedMemberIds } from '../../../../hooks/useMutedMemberIds';
1413
import { primitives } from '../../../../theme';
1514
import { BottomSheetModal } from '../../../UIComponents/BottomSheetModal';
1615
import { StreamBottomSheetModalFlatList } from '../../../UIComponents/StreamBottomSheetModalFlatList';
@@ -41,7 +40,6 @@ const ChannelMemberActionsSheetInner = ({
4140
},
4241
} = useTheme();
4342
const styles = useStyles();
44-
const mutedMemberIds = useMutedMemberIds(channel);
4543

4644
const actionItems = useChannelMemberActionItems({
4745
channel,
@@ -70,11 +68,7 @@ const ChannelMemberActionsSheetInner = ({
7068
return (
7169
<View style={[styles.container, containerOverride]}>
7270
<View style={headerOverride}>
73-
<ChannelMemberItem
74-
isMuted={mutedMemberIds.has(member.user?.id ?? '')}
75-
member={member}
76-
size='lg'
77-
/>
71+
<ChannelMemberItem member={member} size='lg' />
7872
</View>
7973
<StreamBottomSheetModalFlatList<ChannelMemberActionItem>
8074
contentContainerStyle={[styles.actionsList, actionsListOverride]}

package/src/components/ChannelDetailsScreen/components/members/ChannelMemberItem.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
99
import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
1010
import { Mute } from '../../../../icons';
1111
import { primitives } from '../../../../theme';
12+
import { useUserMuteActive } from '../../../Message/hooks/useUserMuteActive';
1213
import { UserAvatar } from '../../../ui/Avatar/UserAvatar';
1314
import { useMemberRoleLabel } from '../../hooks/members/useMemberRoleLabel';
1415
import { useUserActivityStatus } from '../../hooks/useUserActivityStatus';
@@ -17,12 +18,6 @@ export type ChannelMemberItemSize = 'sm' | 'lg';
1718

1819
export type ChannelMemberItemProps = {
1920
member: ChannelMemberResponse;
20-
/**
21-
* Whether the current user has muted this member. Compute once at the list level
22-
* (see `useMutedMemberIds`) and pass it down — when `true` a muted
23-
* indicator icon is rendered in the row's trailing area.
24-
*/
25-
isMuted?: boolean;
2621
onPress?: () => void;
2722
/**
2823
* Visual size of the row.
@@ -35,7 +30,6 @@ export type ChannelMemberItemProps = {
3530
};
3631

3732
const ChannelMemberItemInner = ({
38-
isMuted,
3933
member,
4034
onPress,
4135
size = 'sm',
@@ -59,6 +53,7 @@ const ChannelMemberItemInner = ({
5953
const styles = useStyles();
6054
const statusLine = useUserActivityStatus(member.user);
6155
const roleLabel = useMemberRoleLabel(member);
56+
const isMuted = useUserMuteActive(member.user);
6257

6358
const user = member.user;
6459
if (!user) return null;
@@ -155,7 +150,6 @@ const ChannelMemberItemInner = ({
155150
};
156151

157152
const areEqual = (prev: ChannelMemberItemProps, next: ChannelMemberItemProps) => {
158-
if (prev.isMuted !== next.isMuted) return false;
159153
if (prev.onPress !== next.onPress) return false;
160154
if (prev.size !== next.size) return false;
161155
if (prev.testID !== next.testID) return false;

package/src/components/ChannelDetailsScreen/components/members/ChannelMemberList.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { MemberListLoadingSkeleton } from './MemberListLoadingSkeleton';
77

88
import { useChannelDetailsContext } from '../../../../contexts/channelDetailsContext/channelDetailsContext';
99
import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext';
10-
import { useMutedMemberIds } from '../../../../hooks/useMutedMemberIds';
1110
import { useChannelAllMembers } from '../../hooks/members/useChannelAllMembers';
1211

1312
const keyExtractor = (member: ChannelMemberResponse) => member.user?.id ?? member.user_id ?? '';
@@ -29,7 +28,6 @@ export const ChannelMemberList = ({ additionalFlatListProps }: ChannelMemberList
2928
const { channel, onMemberPress } = useChannelDetailsContext();
3029
const { ChannelMemberActionsSheet, ChannelMemberItem } = useComponentsContext();
3130
const { hasMore, loading, loadingMore, loadMore, results } = useChannelAllMembers({ channel });
32-
const mutedMemberIds = useMutedMemberIds(channel);
3331
const [selectedMember, setSelectedMember] = useState<ChannelMemberResponse | null>(null);
3432

3533
const handleMemberActionsClose = useCallback(() => setSelectedMember(null), []);
@@ -47,13 +45,9 @@ export const ChannelMemberList = ({ additionalFlatListProps }: ChannelMemberList
4745

4846
const renderItem = useCallback(
4947
({ item }: { item: ChannelMemberResponse }) => (
50-
<ChannelMemberItem
51-
isMuted={mutedMemberIds.has(item.user?.id ?? '')}
52-
member={item}
53-
onPress={() => handleMemberPress(item)}
54-
/>
48+
<ChannelMemberItem member={item} onPress={() => handleMemberPress(item)} />
5549
),
56-
[ChannelMemberItem, handleMemberPress, mutedMemberIds],
50+
[ChannelMemberItem, handleMemberPress],
5751
);
5852

5953
const ListFooterComponent = useMemo(
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import { Channel, EventTypes, Mute, StreamChat } from 'stream-chat';
22

33
import { useChatContext } from '../../../contexts';
4-
import { useSyncClientEventsToChannel } from '../../../hooks/useSyncClientEvents';
4+
import { useSyncClientEvents } from '../../../hooks/useSyncClientEvents';
55

6-
const selector = (_channel: Channel, client: StreamChat) => client.mutedUsers;
6+
const selector = (client: StreamChat) => client.mutedUsers;
77
const keys: EventTypes[] = ['health.check', 'notification.mutes_updated'];
8-
export function useMutedUsers(channel: Channel): Array<Mute>;
9-
export function useMutedUsers(channel?: Channel): Array<Mute> | undefined;
10-
export function useMutedUsers(channel?: Channel) {
8+
9+
export function useMutedUsers(_channel?: Channel): Array<Mute> {
1110
const { client } = useChatContext();
12-
return useSyncClientEventsToChannel({ channel, client, selector, stateChangeEventKeys: keys });
11+
return useSyncClientEvents({ client, selector, stateChangeEventKeys: keys });
1312
}

0 commit comments

Comments
 (0)