stream-chat-react-native-core 9.4.0 → 9.5.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +2 -1
  2. package/lib/commonjs/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
  3. package/lib/commonjs/components/ChannelDetails/components/members/ChannelAddMembers.js +7 -4
  4. package/lib/commonjs/components/ChannelDetails/components/members/ChannelAddMembers.js.map +1 -1
  5. package/lib/commonjs/components/ChannelDetails/components/members/ChannelAllMembersModal.js +2 -23
  6. package/lib/commonjs/components/ChannelDetails/components/members/ChannelAllMembersModal.js.map +1 -1
  7. package/lib/commonjs/components/ChannelDetails/components/members/ChannelMemberList.js +124 -22
  8. package/lib/commonjs/components/ChannelDetails/components/members/ChannelMemberList.js.map +1 -1
  9. package/lib/commonjs/components/ChannelDetails/components/navigation-section/PinnedMessageItem.js +1 -1
  10. package/lib/commonjs/components/ChannelDetails/components/navigation-section/PinnedMessageItem.js.map +1 -1
  11. package/lib/commonjs/components/ChannelDetails/components/navigation-section/PinnedMessageList.js +1 -1
  12. package/lib/commonjs/components/ChannelDetails/components/navigation-section/PinnedMessageList.js.map +1 -1
  13. package/lib/commonjs/components/ChannelDetails/hooks/members/index.js +0 -11
  14. package/lib/commonjs/components/ChannelDetails/hooks/members/index.js.map +1 -1
  15. package/lib/commonjs/components/Message/hooks/useMessageActions.js +20 -3
  16. package/lib/commonjs/components/Message/hooks/useMessageActions.js.map +1 -1
  17. package/lib/commonjs/components/UIComponents/SearchInput.js +1 -3
  18. package/lib/commonjs/components/UIComponents/SearchInput.js.map +1 -1
  19. package/lib/commonjs/contexts/channelMemberListContext/ChannelMemberListContext.js +63 -0
  20. package/lib/commonjs/contexts/channelMemberListContext/ChannelMemberListContext.js.map +1 -0
  21. package/lib/commonjs/contexts/index.js +11 -0
  22. package/lib/commonjs/contexts/index.js.map +1 -1
  23. package/lib/commonjs/contexts/themeContext/utils/theme.js +8 -0
  24. package/lib/commonjs/contexts/themeContext/utils/theme.js.map +1 -1
  25. package/lib/commonjs/hooks/actions/useChannelActionItems.js +54 -4
  26. package/lib/commonjs/hooks/actions/useChannelActionItems.js.map +1 -1
  27. package/lib/commonjs/hooks/actions/useChannelMemberActionItems.js +22 -3
  28. package/lib/commonjs/hooks/actions/useChannelMemberActionItems.js.map +1 -1
  29. package/lib/commonjs/i18n/ar.json +9 -1
  30. package/lib/commonjs/i18n/en.json +8 -0
  31. package/lib/commonjs/i18n/es.json +9 -1
  32. package/lib/commonjs/i18n/fr.json +9 -1
  33. package/lib/commonjs/i18n/he.json +9 -1
  34. package/lib/commonjs/i18n/hi.json +9 -1
  35. package/lib/commonjs/i18n/it.json +9 -1
  36. package/lib/commonjs/i18n/ja.json +9 -1
  37. package/lib/commonjs/i18n/ko.json +9 -1
  38. package/lib/commonjs/i18n/nl.json +9 -1
  39. package/lib/commonjs/i18n/pt-br.json +9 -1
  40. package/lib/commonjs/i18n/ru.json +9 -1
  41. package/lib/commonjs/i18n/tr.json +9 -1
  42. package/lib/commonjs/version.json +1 -1
  43. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js +2 -1
  44. package/lib/module/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.js.map +1 -1
  45. package/lib/module/components/ChannelDetails/components/members/ChannelAddMembers.js +7 -4
  46. package/lib/module/components/ChannelDetails/components/members/ChannelAddMembers.js.map +1 -1
  47. package/lib/module/components/ChannelDetails/components/members/ChannelAllMembersModal.js +2 -23
  48. package/lib/module/components/ChannelDetails/components/members/ChannelAllMembersModal.js.map +1 -1
  49. package/lib/module/components/ChannelDetails/components/members/ChannelMemberList.js +124 -22
  50. package/lib/module/components/ChannelDetails/components/members/ChannelMemberList.js.map +1 -1
  51. package/lib/module/components/ChannelDetails/components/navigation-section/PinnedMessageItem.js +1 -1
  52. package/lib/module/components/ChannelDetails/components/navigation-section/PinnedMessageItem.js.map +1 -1
  53. package/lib/module/components/ChannelDetails/components/navigation-section/PinnedMessageList.js +1 -1
  54. package/lib/module/components/ChannelDetails/components/navigation-section/PinnedMessageList.js.map +1 -1
  55. package/lib/module/components/ChannelDetails/hooks/members/index.js +0 -11
  56. package/lib/module/components/ChannelDetails/hooks/members/index.js.map +1 -1
  57. package/lib/module/components/Message/hooks/useMessageActions.js +20 -3
  58. package/lib/module/components/Message/hooks/useMessageActions.js.map +1 -1
  59. package/lib/module/components/UIComponents/SearchInput.js +1 -3
  60. package/lib/module/components/UIComponents/SearchInput.js.map +1 -1
  61. package/lib/module/contexts/channelMemberListContext/ChannelMemberListContext.js +63 -0
  62. package/lib/module/contexts/channelMemberListContext/ChannelMemberListContext.js.map +1 -0
  63. package/lib/module/contexts/index.js +11 -0
  64. package/lib/module/contexts/index.js.map +1 -1
  65. package/lib/module/contexts/themeContext/utils/theme.js +8 -0
  66. package/lib/module/contexts/themeContext/utils/theme.js.map +1 -1
  67. package/lib/module/hooks/actions/useChannelActionItems.js +54 -4
  68. package/lib/module/hooks/actions/useChannelActionItems.js.map +1 -1
  69. package/lib/module/hooks/actions/useChannelMemberActionItems.js +22 -3
  70. package/lib/module/hooks/actions/useChannelMemberActionItems.js.map +1 -1
  71. package/lib/module/i18n/ar.json +9 -1
  72. package/lib/module/i18n/en.json +8 -0
  73. package/lib/module/i18n/es.json +9 -1
  74. package/lib/module/i18n/fr.json +9 -1
  75. package/lib/module/i18n/he.json +9 -1
  76. package/lib/module/i18n/hi.json +9 -1
  77. package/lib/module/i18n/it.json +9 -1
  78. package/lib/module/i18n/ja.json +9 -1
  79. package/lib/module/i18n/ko.json +9 -1
  80. package/lib/module/i18n/nl.json +9 -1
  81. package/lib/module/i18n/pt-br.json +9 -1
  82. package/lib/module/i18n/ru.json +9 -1
  83. package/lib/module/i18n/tr.json +9 -1
  84. package/lib/module/version.json +1 -1
  85. package/lib/typescript/components/ChannelDetails/components/members/ChannelAddMembers.d.ts.map +1 -1
  86. package/lib/typescript/components/ChannelDetails/components/members/ChannelAllMembersModal.d.ts +1 -1
  87. package/lib/typescript/components/ChannelDetails/components/members/ChannelAllMembersModal.d.ts.map +1 -1
  88. package/lib/typescript/components/ChannelDetails/components/members/ChannelMemberList.d.ts +4 -2
  89. package/lib/typescript/components/ChannelDetails/components/members/ChannelMemberList.d.ts.map +1 -1
  90. package/lib/typescript/components/ChannelDetails/hooks/members/index.d.ts +0 -1
  91. package/lib/typescript/components/ChannelDetails/hooks/members/index.d.ts.map +1 -1
  92. package/lib/typescript/components/Message/hooks/useMessageActions.d.ts.map +1 -1
  93. package/lib/typescript/contexts/channelMemberListContext/ChannelMemberListContext.d.ts +21 -0
  94. package/lib/typescript/contexts/channelMemberListContext/ChannelMemberListContext.d.ts.map +1 -0
  95. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts +1 -1
  96. package/lib/typescript/contexts/index.d.ts +1 -0
  97. package/lib/typescript/contexts/index.d.ts.map +1 -1
  98. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts +8 -0
  99. package/lib/typescript/contexts/themeContext/ThemeContext.d.ts.map +1 -1
  100. package/lib/typescript/contexts/themeContext/utils/theme.d.ts +8 -0
  101. package/lib/typescript/contexts/themeContext/utils/theme.d.ts.map +1 -1
  102. package/lib/typescript/hooks/actions/useChannelActionItems.d.ts.map +1 -1
  103. package/lib/typescript/hooks/actions/useChannelMemberActionItems.d.ts.map +1 -1
  104. package/lib/typescript/i18n/ar.json +9 -1
  105. package/lib/typescript/i18n/en.json +8 -0
  106. package/lib/typescript/i18n/es.json +9 -1
  107. package/lib/typescript/i18n/fr.json +9 -1
  108. package/lib/typescript/i18n/he.json +9 -1
  109. package/lib/typescript/i18n/hi.json +9 -1
  110. package/lib/typescript/i18n/it.json +9 -1
  111. package/lib/typescript/i18n/ja.json +9 -1
  112. package/lib/typescript/i18n/ko.json +9 -1
  113. package/lib/typescript/i18n/nl.json +9 -1
  114. package/lib/typescript/i18n/pt-br.json +9 -1
  115. package/lib/typescript/i18n/ru.json +9 -1
  116. package/lib/typescript/i18n/tr.json +9 -1
  117. package/lib/typescript/utils/i18n/Streami18n.d.ts +8 -0
  118. package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
  119. package/package.json +1 -1
  120. package/src/components/AttachmentPicker/components/AttachmentMediaPicker/AttachmentPickerItem.tsx +2 -1
  121. package/src/components/ChannelDetails/__tests__/members/ChannelMemberList.test.tsx +265 -151
  122. package/src/components/ChannelDetails/components/members/ChannelAddMembers.tsx +10 -4
  123. package/src/components/ChannelDetails/components/members/ChannelAllMembersModal.tsx +4 -25
  124. package/src/components/ChannelDetails/components/members/ChannelMemberList.tsx +137 -23
  125. package/src/components/ChannelDetails/components/navigation-section/PinnedMessageItem.tsx +1 -1
  126. package/src/components/ChannelDetails/components/navigation-section/PinnedMessageList.tsx +1 -1
  127. package/src/components/ChannelDetails/hooks/members/index.ts +0 -1
  128. package/src/components/Message/hooks/useMessageActions.tsx +30 -6
  129. package/src/components/UIComponents/SearchInput.tsx +1 -3
  130. package/src/contexts/channelMemberListContext/ChannelMemberListContext.tsx +72 -0
  131. package/src/contexts/index.ts +1 -0
  132. package/src/contexts/themeContext/utils/theme.ts +16 -0
  133. package/src/hooks/actions/__tests__/useChannelActionItems.test.tsx +4 -4
  134. package/src/hooks/actions/__tests__/useChannelMemberActionItems.test.tsx +3 -3
  135. package/src/hooks/actions/useChannelActionItems.tsx +56 -2
  136. package/src/hooks/actions/useChannelMemberActionItems.tsx +23 -1
  137. package/src/i18n/ar.json +9 -1
  138. package/src/i18n/en.json +8 -0
  139. package/src/i18n/es.json +9 -1
  140. package/src/i18n/fr.json +9 -1
  141. package/src/i18n/he.json +9 -1
  142. package/src/i18n/hi.json +9 -1
  143. package/src/i18n/it.json +9 -1
  144. package/src/i18n/ja.json +9 -1
  145. package/src/i18n/ko.json +9 -1
  146. package/src/i18n/nl.json +9 -1
  147. package/src/i18n/pt-br.json +9 -1
  148. package/src/i18n/ru.json +9 -1
  149. package/src/i18n/tr.json +9 -1
  150. package/src/version.json +1 -1
  151. package/lib/commonjs/components/ChannelDetails/hooks/members/useChannelAllMembers.js +0 -139
  152. package/lib/commonjs/components/ChannelDetails/hooks/members/useChannelAllMembers.js.map +0 -1
  153. package/lib/module/components/ChannelDetails/hooks/members/useChannelAllMembers.js +0 -139
  154. package/lib/module/components/ChannelDetails/hooks/members/useChannelAllMembers.js.map +0 -1
  155. package/lib/typescript/components/ChannelDetails/hooks/members/useChannelAllMembers.d.ts +0 -14
  156. package/lib/typescript/components/ChannelDetails/hooks/members/useChannelAllMembers.d.ts.map +0 -1
  157. package/src/components/ChannelDetails/__tests__/members/useChannelAllMembers.test.tsx +0 -234
  158. package/src/components/ChannelDetails/hooks/members/useChannelAllMembers.ts +0 -131
@@ -1,16 +1,37 @@
1
- import React, { useCallback, useMemo, useState } from 'react';
2
- import { ActivityIndicator, FlatList, type FlatListProps } from 'react-native';
1
+ import React, { useCallback, useEffect, useRef, useState } from 'react';
2
+ import { ActivityIndicator, FlatList, type FlatListProps, StyleSheet, View } from 'react-native';
3
3
 
4
- import type { ChannelMemberResponse } from 'stream-chat';
4
+ import type { ChannelMemberResponse, SearchSourceState } from 'stream-chat';
5
5
 
6
6
  import { MemberListLoadingSkeleton } from './MemberListLoadingSkeleton';
7
7
 
8
8
  import { useChannelDetailsContext } from '../../../../contexts/channelDetailsContext/channelDetailsContext';
9
+ import {
10
+ ChannelMemberListProvider,
11
+ useChannelMemberListContext,
12
+ } from '../../../../contexts/channelMemberListContext/ChannelMemberListContext';
9
13
  import { useComponentsContext } from '../../../../contexts/componentsContext/ComponentsContext';
10
- import { useChannelAllMembers } from '../../hooks/members/useChannelAllMembers';
14
+ import { useTheme } from '../../../../contexts/themeContext/ThemeContext';
15
+ import { useTranslationContext } from '../../../../contexts/translationContext/TranslationContext';
16
+ import { getNotificationErrorOptions } from '../../../../hooks/actions/useChannelActions';
17
+ import { useStateStore } from '../../../../hooks/useStateStore';
18
+ import { primitives } from '../../../../theme';
19
+ import { useNotificationApi } from '../../../Notifications/hooks/useNotificationApi';
20
+ import { NotificationList } from '../../../Notifications/NotificationList';
21
+ import { NotificationTargetProvider } from '../../../Notifications/NotificationTargetContext';
22
+ import { EmptySearchResult } from '../../../UIComponents/EmptySearchResult';
23
+ import { SearchInput, SearchInputProps } from '../../../UIComponents/SearchInput';
11
24
 
12
25
  const keyExtractor = (member: ChannelMemberResponse) => member.user?.id ?? member.user_id ?? '';
13
26
 
27
+ const listStateSelector = (state: SearchSourceState<ChannelMemberResponse>) => ({
28
+ error: state.lastQueryError,
29
+ hasNext: state.hasNext,
30
+ loading: state.isLoading,
31
+ members: state.items,
32
+ searchQuery: state.searchQuery,
33
+ });
34
+
14
35
  export type ChannelMemberListProps = {
15
36
  /**
16
37
  * Besides the existing default behavior of the members list, you can attach
@@ -19,18 +40,54 @@ export type ChannelMemberListProps = {
19
40
  * See https://reactnative.dev/docs/flatlist#props for the full list.
20
41
  */
21
42
  additionalFlatListProps?: Partial<FlatListProps<ChannelMemberResponse>>;
43
+ searchInputProps?: SearchInputProps;
22
44
  };
23
45
 
24
- /**
25
- * Lists all channel members.
26
- * @experimental This component is experimental and is subject to change.
27
- */
28
- export const ChannelMemberList = ({ additionalFlatListProps }: ChannelMemberListProps = {}) => {
29
- const { channel, onMemberPress } = useChannelDetailsContext();
46
+ const ChannelMemberListContent = ({
47
+ additionalFlatListProps,
48
+ searchInputProps,
49
+ }: ChannelMemberListProps) => {
50
+ const { t } = useTranslationContext();
51
+ const {
52
+ theme: {
53
+ channelDetails: { memberList },
54
+ },
55
+ } = useTheme();
56
+ const { onMemberPress } = useChannelDetailsContext();
30
57
  const { ChannelMemberActionsSheet, ChannelMemberItem } = useComponentsContext();
31
- const { hasMore, loading, loadMore, results } = useChannelAllMembers({ channel });
58
+ const { addNotification } = useNotificationApi();
59
+
60
+ const { channel, searchSource } = useChannelMemberListContext();
61
+ const { error, hasNext, loading, members, searchQuery } = useStateStore(
62
+ searchSource.state,
63
+ listStateSelector,
64
+ );
65
+
32
66
  const [selectedMember, setSelectedMember] = useState<ChannelMemberResponse | null>(null);
33
67
 
68
+ const initialized = useRef(false);
69
+ useEffect(() => {
70
+ if (!initialized.current) {
71
+ initialized.current = true;
72
+ searchSource.search();
73
+ }
74
+ }, [searchSource]);
75
+
76
+ useEffect(() => {
77
+ if (!error) {
78
+ return;
79
+ }
80
+ addNotification({
81
+ message: t('Failed to load members'),
82
+ options: {
83
+ ...getNotificationErrorOptions(error),
84
+ severity: 'error',
85
+ type: 'api:channel:query-members:failed',
86
+ },
87
+ origin: { context: { channel }, emitter: 'ChannelMemberList' },
88
+ });
89
+ }, [error, addNotification, channel, t]);
90
+
34
91
  const handleMemberActionsClose = useCallback(() => setSelectedMember(null), []);
35
92
 
36
93
  const handleMemberPress = useCallback(
@@ -51,24 +108,46 @@ export const ChannelMemberList = ({ additionalFlatListProps }: ChannelMemberList
51
108
  [ChannelMemberItem, handleMemberPress],
52
109
  );
53
110
 
54
- const ListFooterComponent = useMemo(
55
- () => (loading && results.length > 0 ? <ActivityIndicator /> : null),
56
- [loading, results.length],
57
- );
111
+ const loadMore = useCallback(() => {
112
+ // hasNext is true by default, !!members prevents calling search on initial load
113
+ if (hasNext && !!members) {
114
+ searchSource.search();
115
+ }
116
+ }, [hasNext, members, searchSource]);
58
117
 
59
- if (loading && results.length === 0) {
60
- return <MemberListLoadingSkeleton />;
61
- }
118
+ const emptyState =
119
+ loading && !members ? (
120
+ <MemberListLoadingSkeleton />
121
+ ) : (
122
+ <EmptySearchResult label={t('No members found')} />
123
+ );
62
124
 
63
125
  return (
64
- <>
126
+ <View style={[styles.container, memberList.container]}>
127
+ <SearchInput
128
+ accessibilityLabel={t('a11y/Search members')}
129
+ onChangeText={(text) => {
130
+ searchSource.state.partialNext({ searchQuery: text });
131
+ searchSource.search(text);
132
+ }}
133
+ value={searchQuery}
134
+ {...searchInputProps}
135
+ />
65
136
  <FlatList
66
- data={results}
137
+ contentContainerStyle={[styles.listContent, memberList.listContent]}
138
+ data={members}
139
+ keyboardDismissMode='interactive'
140
+ keyboardShouldPersistTaps='handled'
67
141
  keyExtractor={keyExtractor}
68
- ListFooterComponent={ListFooterComponent}
69
- onEndReached={hasMore ? loadMore : undefined}
142
+ ListEmptyComponent={emptyState}
143
+ ListFooterComponent={
144
+ loading && members && members.length > 0 ? <ActivityIndicator /> : null
145
+ }
146
+ onEndReached={loadMore}
70
147
  onEndReachedThreshold={0.2}
71
148
  renderItem={renderItem}
149
+ style={[styles.list, memberList.list]}
150
+ testID='channel-member-list'
72
151
  {...additionalFlatListProps}
73
152
  />
74
153
  {selectedMember ? (
@@ -78,6 +157,41 @@ export const ChannelMemberList = ({ additionalFlatListProps }: ChannelMemberList
78
157
  visible
79
158
  />
80
159
  ) : null}
81
- </>
160
+ <NotificationList />
161
+ </View>
162
+ );
163
+ };
164
+
165
+ /**
166
+ * Lists all channel members with the ability to search them.
167
+ * @experimental This component is experimental and is subject to change.
168
+ */
169
+ export const ChannelMemberList = (props: ChannelMemberListProps = {}) => {
170
+ const { channel } = useChannelDetailsContext();
171
+ const notificationHostId = channel?.cid ? `channel-member-list:${channel.cid}` : undefined;
172
+
173
+ if (!notificationHostId) {
174
+ return null;
175
+ }
176
+
177
+ return (
178
+ <ChannelMemberListProvider channel={channel}>
179
+ <NotificationTargetProvider hostId={notificationHostId} panel='channel-details'>
180
+ <ChannelMemberListContent {...props} />
181
+ </NotificationTargetProvider>
182
+ </ChannelMemberListProvider>
82
183
  );
83
184
  };
185
+
186
+ const styles = StyleSheet.create({
187
+ container: {
188
+ flex: 1,
189
+ },
190
+ list: {
191
+ flex: 1,
192
+ },
193
+ listContent: {
194
+ flexGrow: 1,
195
+ paddingBottom: primitives.spacing3xl,
196
+ },
197
+ });
@@ -69,7 +69,7 @@ const useStyles = () => {
69
69
  alignItems: 'center',
70
70
  flexDirection: 'row',
71
71
  gap: primitives.spacingSm,
72
- paddingHorizontal: primitives.spacingSm,
72
+ paddingHorizontal: primitives.spacingMd,
73
73
  paddingVertical: primitives.spacingMd,
74
74
  },
75
75
  content: {
@@ -190,7 +190,7 @@ const useStyles = () => {
190
190
  },
191
191
  listContent: {
192
192
  flexGrow: 1,
193
- paddingBottom: primitives.spacingXl,
193
+ paddingBottom: primitives.spacing3xl,
194
194
  },
195
195
  }),
196
196
  [],
@@ -1,2 +1 @@
1
- export * from './useChannelAllMembers';
2
1
  export * from './useMemberRoleLabel';
@@ -1,4 +1,5 @@
1
1
  import React, { useMemo } from 'react';
2
+ import { Alert } from 'react-native';
2
3
 
3
4
  import { LocalMessage } from 'stream-chat';
4
5
 
@@ -244,18 +245,41 @@ export const useMessageActions = ({
244
245
  onOpenThread();
245
246
  });
246
247
 
248
+ const isMuted = useUserMuteActive(message.user);
249
+ const isBlocked = new Set(client.blockedUsers.getLatestValue().userIds).has(
250
+ message.user?.id ?? '',
251
+ );
252
+
247
253
  const onBlockUser = useStableCallback(() => {
248
254
  if (handleBlockUser) {
249
255
  handleBlockUser(message.user);
250
256
  }
251
257
 
252
- handleToggleBlockUser(message.user);
253
- });
258
+ if (isBlocked) {
259
+ handleToggleBlockUser(message.user);
260
+ return;
261
+ }
254
262
 
255
- const isMuted = useUserMuteActive(message.user);
256
- const isBlocked = new Set(client.blockedUsers.getLatestValue().userIds).has(
257
- message.user?.id ?? '',
258
- );
263
+ const name = message.user?.name || message.user?.id || '';
264
+
265
+ Alert.alert(
266
+ t('Block {{ name }}', { name }),
267
+ t("They won't be able to message or call you. You can unblock them later."),
268
+ [
269
+ {
270
+ style: 'cancel',
271
+ text: t('Cancel'),
272
+ },
273
+ {
274
+ onPress: () => {
275
+ handleToggleBlockUser(message.user);
276
+ },
277
+ style: 'destructive',
278
+ text: t('Block'),
279
+ },
280
+ ],
281
+ );
282
+ });
259
283
 
260
284
  return useMemo(() => {
261
285
  const handleReaction =
@@ -83,9 +83,7 @@ SearchInput.displayName = 'SearchInput{searchInput}';
83
83
 
84
84
  const styles = StyleSheet.create({
85
85
  container: {
86
- paddingBottom: primitives.spacingSm,
87
- paddingHorizontal: primitives.spacingMd,
88
- paddingTop: primitives.spacingXs,
86
+ padding: primitives.spacingMd,
89
87
  },
90
88
  input: {
91
89
  borderRadius: primitives.radiusMax,
@@ -0,0 +1,72 @@
1
+ import React, { PropsWithChildren, useContext, useState } from 'react';
2
+
3
+ import { Channel, ChannelMemberSearchSource } from 'stream-chat';
4
+
5
+ import { DEFAULT_BASE_CONTEXT_VALUE } from '../utils/defaultBaseContextValue';
6
+ import { isTestEnvironment } from '../utils/isTestEnvironment';
7
+
8
+ /**
9
+ * @experimental This API is experimental and is subject to change.
10
+ */
11
+ export type ChannelMemberListContextValue = {
12
+ channel: Channel;
13
+ searchSource: ChannelMemberSearchSource;
14
+ };
15
+
16
+ export const ChannelMemberListContext = React.createContext(
17
+ DEFAULT_BASE_CONTEXT_VALUE as ChannelMemberListContextValue,
18
+ );
19
+
20
+ /**
21
+ * @experimental This API is experimental and is subject to change.
22
+ */
23
+ export const ChannelMemberListProvider = ({
24
+ channel,
25
+ children,
26
+ }: PropsWithChildren<{ channel: Channel }>) => {
27
+ const [searchSource] = useState(() => {
28
+ const source = new ChannelMemberSearchSource(
29
+ channel,
30
+ {
31
+ allowEmptySearchString: true,
32
+ pageSize: 25,
33
+ resetOnNewSearchQuery: false,
34
+ },
35
+ {
36
+ initialFilterConfig: {
37
+ $or: {
38
+ enabled: true,
39
+ generate: ({ searchQuery }) =>
40
+ searchQuery ? { name: { $autocomplete: searchQuery } } : {},
41
+ },
42
+ },
43
+ },
44
+ );
45
+ source.sort = { name: 1 };
46
+ source.activate();
47
+ return source;
48
+ });
49
+
50
+ return (
51
+ <ChannelMemberListContext.Provider value={{ channel, searchSource }}>
52
+ {children}
53
+ </ChannelMemberListContext.Provider>
54
+ );
55
+ };
56
+
57
+ /**
58
+ * @experimental This API is experimental and is subject to change.
59
+ */
60
+ export const useChannelMemberListContext = () => {
61
+ const contextValue = useContext(
62
+ ChannelMemberListContext,
63
+ ) as unknown as ChannelMemberListContextValue;
64
+
65
+ if (contextValue === DEFAULT_BASE_CONTEXT_VALUE && !isTestEnvironment()) {
66
+ throw new Error(
67
+ 'The useChannelMemberListContext hook was called outside of the ChannelMemberListContext provider. Render the member list UI inside a ChannelMemberListProvider.',
68
+ );
69
+ }
70
+
71
+ return contextValue;
72
+ };
@@ -34,6 +34,7 @@ export * from './channelAddMembersContext/ChannelAddMembersContext';
34
34
  export * from './channelEditDetailsContext';
35
35
  export * from './channelFileAttachmentListContext/ChannelFileAttachmentListContext';
36
36
  export * from './channelPinnedMessageListContext/ChannelPinnedMessageListContext';
37
+ export * from './channelMemberListContext/ChannelMemberListContext';
37
38
  export * from './channelMediaListContext/ChannelMediaListContext';
38
39
  export * from './pollContext';
39
40
  export * from './liveLocationManagerContext';
@@ -219,6 +219,11 @@ export type Theme = {
219
219
  role: TextStyle;
220
220
  status: TextStyle;
221
221
  };
222
+ memberList: {
223
+ container: ViewStyle;
224
+ list: ViewStyle;
225
+ listContent: ViewStyle;
226
+ };
222
227
  memberActionsSheet: {
223
228
  actionsList: ViewStyle;
224
229
  container: ViewStyle;
@@ -236,6 +241,9 @@ export type Theme = {
236
241
  headerTitle: TextStyle;
237
242
  };
238
243
  addMembers: {
244
+ container: ViewStyle;
245
+ list: ViewStyle;
246
+ listContent: ViewStyle;
239
247
  searchResultItem: {
240
248
  alreadyMemberInfo: ViewStyle;
241
249
  memberLabel: TextStyle;
@@ -1326,6 +1334,11 @@ export const defaultTheme: Theme = {
1326
1334
  role: {},
1327
1335
  status: {},
1328
1336
  },
1337
+ memberList: {
1338
+ container: {},
1339
+ list: {},
1340
+ listContent: {},
1341
+ },
1329
1342
  memberActionsSheet: {
1330
1343
  actionsList: {},
1331
1344
  container: {},
@@ -1343,6 +1356,9 @@ export const defaultTheme: Theme = {
1343
1356
  headerTitle: {},
1344
1357
  },
1345
1358
  addMembers: {
1359
+ container: {},
1360
+ list: {},
1361
+ listContent: {},
1346
1362
  searchResultItem: {
1347
1363
  alreadyMemberInfo: {},
1348
1364
  memberLabel: {},
@@ -105,7 +105,7 @@ describe('useChannelActionItems', () => {
105
105
  expect(result.current.map((item) => item.action)).toEqual([
106
106
  channelActions.muteChannel,
107
107
  channelActions.pin,
108
- channelActions.leave,
108
+ expect.any(Function),
109
109
  expect.any(Function),
110
110
  ]);
111
111
  expect(result.current.map((item) => item.id)).toEqual([
@@ -268,7 +268,7 @@ describe('getChannelActionItems', () => {
268
268
  expect(actionItems.map((item) => item.action)).toEqual([
269
269
  channelActions.muteChannel,
270
270
  channelActions.pin,
271
- channelActions.leave,
271
+ expect.any(Function),
272
272
  expect.any(Function),
273
273
  ]);
274
274
  expect(actionItems.map((item) => item.id)).toEqual(['mute', 'pin', 'leave', 'deleteChannel']);
@@ -308,7 +308,7 @@ describe('getChannelActionItems', () => {
308
308
  channelActions.pin,
309
309
  channelActions.unmuteUser,
310
310
  channelActions.unblockUser,
311
- channelActions.leave,
311
+ expect.any(Function),
312
312
  expect.any(Function),
313
313
  ]);
314
314
  expect(actionItems.map((item) => item.label)).toEqual([
@@ -383,7 +383,7 @@ describe('getChannelActionItems', () => {
383
383
  expect(actionItems[0].placement).toBe('swipe');
384
384
 
385
385
  const leaveItem = actionItems.find((item) => item.id === 'leave');
386
- expect(leaveItem?.action).toBe(channelActions.leave);
386
+ expect(leaveItem?.action).toEqual(expect.any(Function));
387
387
  expect(leaveItem?.label).toBe('Leave Group');
388
388
  expect(leaveItem?.placement).toBe('sheet');
389
389
  });
@@ -76,7 +76,7 @@ describe('useChannelMemberActionItems', () => {
76
76
  expect(result.current.map((item) => item.id)).toEqual(['muteUser', 'block']);
77
77
  expect(result.current.map((item) => item.action)).toEqual([
78
78
  userActions.muteUser,
79
- userActions.blockUser,
79
+ expect.any(Function),
80
80
  ]);
81
81
  expect(result.current.map((item) => item.type)).toEqual(['standard', 'destructive']);
82
82
  expect(result.current.map((item) => item.label)).toEqual(['Mute User', 'Block User']);
@@ -259,7 +259,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
259
259
  });
260
260
 
261
261
  expect(items.map((item) => item.id)).toEqual(['muteUser', 'block']);
262
- expect(items.map((item) => item.action)).toEqual([actions.muteUser, actions.blockUser]);
262
+ expect(items.map((item) => item.action)).toEqual([actions.muteUser, expect.any(Function)]);
263
263
  expect(items.map((item) => item.label)).toEqual(['Mute User', 'Block User']);
264
264
  expect(items.map((item) => item.type)).toEqual(['standard', 'destructive']);
265
265
  });
@@ -335,7 +335,7 @@ describe('buildDefaultChannelMemberActionItems', () => {
335
335
  });
336
336
 
337
337
  expect(items.find((item) => item.id === 'muteUser')?.action).toBe(actions.unmuteUser);
338
- expect(items.find((item) => item.id === 'block')?.action).toBe(actions.blockUser);
338
+ expect(items.find((item) => item.id === 'block')?.action).toEqual(expect.any(Function));
339
339
  });
340
340
 
341
341
  it('default getChannelMemberActionItems returns defaultItems unchanged', () => {
@@ -179,8 +179,31 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
179
179
  type: 'standard',
180
180
  });
181
181
 
182
+ const blockUserWithConfirmation = (...args: Parameters<ChannelActionHandler>) => {
183
+ const otherUser = getOtherUserInDirectChannel(channel)?.user;
184
+ const name = otherUser?.name || otherUser?.id || '';
185
+
186
+ Alert.alert(
187
+ t('Block {{ name }}', { name }),
188
+ t("They won't be able to message or call you. You can unblock them later."),
189
+ [
190
+ {
191
+ style: 'cancel',
192
+ text: t('Cancel'),
193
+ },
194
+ {
195
+ onPress: async () => {
196
+ await blockUser(...args);
197
+ },
198
+ style: 'destructive',
199
+ text: t('Block'),
200
+ },
201
+ ],
202
+ );
203
+ };
204
+
182
205
  actionItems.push({
183
- action: isBlocked ? unblockUser : blockUser,
206
+ action: isBlocked ? unblockUser : blockUserWithConfirmation,
184
207
  Icon: (props) => <ChannelActionsIcon Icon={BlockUser} {...props} />,
185
208
  id: 'block',
186
209
  label: isBlocked ? t('Unblock User') : t('Block User'),
@@ -189,8 +212,39 @@ export const buildDefaultChannelActionItems: BuildDefaultChannelActionItems = (
189
212
  });
190
213
  }
191
214
 
215
+ const leaveWithConfirmation = (...args: Parameters<ChannelActionHandler>) => {
216
+ const channelName = channel.data?.name;
217
+ let name: string;
218
+ if (channelName) {
219
+ name = channelName;
220
+ } else if (isDirectChat) {
221
+ const otherUser = getOtherUserInDirectChannel(channel)?.user;
222
+ name = otherUser?.name || otherUser?.id || '';
223
+ } else {
224
+ name = t('group');
225
+ }
226
+
227
+ Alert.alert(
228
+ isDirectChat ? t('Leave Chat') : t('Leave Group'),
229
+ t("You'll stop receiving messages from {{ name }}. You can rejoin anytime.", { name }),
230
+ [
231
+ {
232
+ style: 'cancel',
233
+ text: t('Cancel'),
234
+ },
235
+ {
236
+ onPress: async () => {
237
+ await leave(...args);
238
+ },
239
+ style: 'destructive',
240
+ text: t('Leave'),
241
+ },
242
+ ],
243
+ );
244
+ };
245
+
192
246
  actionItems.push({
193
- action: leave,
247
+ action: leaveWithConfirmation,
194
248
  Icon: (props) => <ChannelActionsIcon Icon={ArrowBoxLeft} {...props} />,
195
249
  id: 'leave',
196
250
  label: isDirectChat ? t('Leave Chat') : t('Leave Group'),
@@ -83,7 +83,29 @@ export const buildDefaultChannelMemberActionItems: BuildDefaultChannelMemberActi
83
83
  type: 'standard',
84
84
  },
85
85
  {
86
- action: isBlocked ? unblockUser : blockUser,
86
+ action: isBlocked
87
+ ? unblockUser
88
+ : (...args: Parameters<UserActions['blockUser']>) => {
89
+ const name = member.user?.name || member.user?.id || '';
90
+
91
+ Alert.alert(
92
+ t('Block {{ name }}', { name }),
93
+ t("They won't be able to message or call you. You can unblock them later."),
94
+ [
95
+ {
96
+ style: 'cancel',
97
+ text: t('Cancel'),
98
+ },
99
+ {
100
+ onPress: async () => {
101
+ await blockUser(...args);
102
+ },
103
+ style: 'destructive',
104
+ text: t('Block'),
105
+ },
106
+ ],
107
+ );
108
+ },
87
109
  Icon: (props) => <ChannelMemberActionsIcon Icon={BlockUser} {...props} />,
88
110
  id: 'block',
89
111
  label: isBlocked ? t('Unblock User') : t('Block User'),
package/src/i18n/ar.json CHANGED
@@ -457,5 +457,13 @@
457
457
  "Pin Chat": "تثبيت الدردشة",
458
458
  "Pin Group": "تثبيت المجموعة",
459
459
  "Unpin Chat": "إلغاء تثبيت الدردشة",
460
- "Unpin Group": "إلغاء تثبيت المجموعة"
460
+ "Unpin Group": "إلغاء تثبيت المجموعة",
461
+ "Block": "حظر",
462
+ "Block {{ name }}": "حظر {{ name }}",
463
+ "They won't be able to message or call you. You can unblock them later.": "لن يتمكن/تتمكن من مراسلتك أو الاتصال بك. يمكنك إلغاء الحظر لاحقًا.",
464
+ "Leave": "مغادرة",
465
+ "You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "ستتوقف عن تلقي الرسائل من {{ name }}. يمكنك الانضمام مجددًا في أي وقت.",
466
+ "group": "المجموعة",
467
+ "No members found": "لم يتم العثور على أعضاء",
468
+ "a11y/Search members": "البحث عن الأعضاء"
461
469
  }
package/src/i18n/en.json CHANGED
@@ -99,6 +99,7 @@
99
99
  "No chats here yet…": "No chats here yet…",
100
100
  "No files": "No files",
101
101
  "No items exist": "No items exist",
102
+ "No members found": "No members found",
102
103
  "No photos or videos": "No photos or videos",
103
104
  "No pinned messages": "No pinned messages",
104
105
  "No threads here yet": "No threads here yet",
@@ -242,6 +243,12 @@
242
243
  "No conversations yet": "No conversations yet",
243
244
  "Are you sure you want to delete this group? This can't be undone.": "Are you sure you want to delete this group? This can't be undone.",
244
245
  "Are you sure you want to delete this chat? This can't be undone.": "Are you sure you want to delete this chat? This can't be undone.",
246
+ "Block": "Block",
247
+ "Block {{ name }}": "Block {{ name }}",
248
+ "They won't be able to message or call you. You can unblock them later.": "They won't be able to message or call you. You can unblock them later.",
249
+ "Leave": "Leave",
250
+ "You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "You'll stop receiving messages from {{ name }}. You can rejoin anytime.",
251
+ "group": "this group",
245
252
  "Delete chat": "Delete chat",
246
253
  "Delete group": "Delete group",
247
254
  "Archive Chat": "Archive Chat",
@@ -350,6 +357,7 @@
350
357
  "a11y/Select image": "Select image",
351
358
  "a11y/Select video": "Select video",
352
359
  "a11y/Send voice recording": "Send voice recording",
360
+ "a11y/Search members": "Search members",
353
361
  "a11y/Search pinned messages": "Search pinned messages",
354
362
  "a11y/Search users to add": "Search users to add",
355
363
  "a11y/Select {{name}}": "Select {{name}}",
package/src/i18n/es.json CHANGED
@@ -457,5 +457,13 @@
457
457
  "Pin Chat": "Fijar chat",
458
458
  "Pin Group": "Fijar grupo",
459
459
  "Unpin Chat": "Desfijar chat",
460
- "Unpin Group": "Desfijar grupo"
460
+ "Unpin Group": "Desfijar grupo",
461
+ "Block": "Bloquear",
462
+ "Block {{ name }}": "Bloquear a {{ name }}",
463
+ "They won't be able to message or call you. You can unblock them later.": "No podrá enviarte mensajes ni llamarte. Puedes desbloquearlo más tarde.",
464
+ "Leave": "Salir",
465
+ "You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Dejarás de recibir mensajes de {{ name }}. Puedes volver a unirte cuando quieras.",
466
+ "group": "el grupo",
467
+ "No members found": "No se encontraron miembros",
468
+ "a11y/Search members": "Buscar miembros"
461
469
  }
package/src/i18n/fr.json CHANGED
@@ -457,5 +457,13 @@
457
457
  "Pin Chat": "Épingler la discussion",
458
458
  "Pin Group": "Épingler le groupe",
459
459
  "Unpin Chat": "Détacher la discussion",
460
- "Unpin Group": "Détacher le groupe"
460
+ "Unpin Group": "Détacher le groupe",
461
+ "Block": "Bloquer",
462
+ "Block {{ name }}": "Bloquer {{ name }}",
463
+ "They won't be able to message or call you. You can unblock them later.": "Cette personne ne pourra plus vous envoyer de messages ni vous appeler. Vous pourrez la débloquer plus tard.",
464
+ "Leave": "Quitter",
465
+ "You'll stop receiving messages from {{ name }}. You can rejoin anytime.": "Vous ne recevrez plus de messages de {{ name }}. Vous pouvez rejoindre à tout moment.",
466
+ "group": "ce groupe",
467
+ "No members found": "Aucun membre trouvé",
468
+ "a11y/Search members": "Rechercher des membres"
461
469
  }