stream-chat-react-native-core 9.1.2-beta.3 → 9.1.2-beta.4

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 (53) hide show
  1. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -2
  2. package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  3. package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -2
  4. package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
  5. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js +29 -1
  6. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
  7. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteSuggestionItem.js +3 -1
  8. package/lib/commonjs/components/AutoCompleteInput/AutoCompleteSuggestionItem.js.map +1 -1
  9. package/lib/commonjs/components/ui/GiphyChip.js +0 -1
  10. package/lib/commonjs/components/ui/GiphyChip.js.map +1 -1
  11. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +16 -4
  12. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  13. package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
  14. package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
  15. package/lib/commonjs/version.json +1 -1
  16. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -2
  17. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  18. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -2
  19. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
  20. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +29 -1
  21. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
  22. package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js +3 -1
  23. package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js.map +1 -1
  24. package/lib/module/components/ui/GiphyChip.js +0 -1
  25. package/lib/module/components/ui/GiphyChip.js.map +1 -1
  26. package/lib/module/contexts/messageInputContext/MessageInputContext.js +16 -4
  27. package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  28. package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
  29. package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
  30. package/lib/module/version.json +1 -1
  31. package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerContent.d.ts.map +1 -1
  32. package/lib/typescript/components/AttachmentPicker/components/AttachmentTypePickerButton.d.ts.map +1 -1
  33. package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts +1 -1
  34. package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts.map +1 -1
  35. package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts +1 -1
  36. package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts.map +1 -1
  37. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts +6 -2
  38. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts.map +1 -1
  39. package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts +3 -0
  40. package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts.map +1 -0
  41. package/package.json +2 -2
  42. package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx +10 -2
  43. package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx +1 -3
  44. package/src/components/AttachmentPicker/components/__tests__/AttachmentPickerContent.test.tsx +104 -0
  45. package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +41 -5
  46. package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx +9 -2
  47. package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.tsx +47 -0
  48. package/src/components/ui/GiphyChip.tsx +1 -1
  49. package/src/contexts/messageInputContext/MessageInputContext.tsx +23 -8
  50. package/src/contexts/messageInputContext/__tests__/sendMessage.test.tsx +48 -0
  51. package/src/contexts/messageInputContext/__tests__/useIsCommandDisabled.test.tsx +110 -0
  52. package/src/contexts/messageInputContext/hooks/useIsCommandDisabled.ts +24 -0
  53. package/src/version.json +1 -1
@@ -7,7 +7,7 @@ import { MessageInputHeightStore } from '../../state-store/message-input-height-
7
7
  import { File } from '../../types/types';
8
8
  export type LocalMessageInputContext = {
9
9
  closeAttachmentPicker: () => void;
10
- inputBoxRef: React.RefObject<TextInput | null>;
10
+ inputBoxRef: React.RefObject<InputBoxRef | null>;
11
11
  openAttachmentPicker: () => void;
12
12
  /**
13
13
  * Function for picking a photo from native image picker and uploading it.
@@ -18,7 +18,7 @@ export type LocalMessageInputContext = {
18
18
  /**
19
19
  * Ref callback to set reference on input box
20
20
  */
21
- setInputBoxRef: Ref<TextInput> | undefined;
21
+ setInputBoxRef: Ref<InputBoxRef> | undefined;
22
22
  /**
23
23
  * Function for taking a photo and uploading it
24
24
  */
@@ -33,6 +33,10 @@ export type LocalMessageInputContext = {
33
33
  uploadVoiceRecording: (sendOnComplete: boolean) => Promise<void>;
34
34
  stopVoiceRecording: () => Promise<void>;
35
35
  };
36
+ export type InputBoxRef = TextInput & {
37
+ clearState: () => void;
38
+ restoreState: (text: string) => void;
39
+ };
36
40
  export type InputMessageInputContextValue = {
37
41
  /**
38
42
  * Controls how many pixels to the top side the user has to scroll in order to lock the recording view and allow the
@@ -1 +1 @@
1
- {"version":3,"file":"MessageInputContext.d.ts","sourceRoot":"","sources":["../../../../src/contexts/messageInputContext/MessageInputContext.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MessageInputContext.d.ts","sourceRoot":"","sources":["../../../../src/contexts/messageInputContext/MessageInputContext.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ import type { CommandSuggestion } from 'stream-chat';
2
+ export declare const useIsCommandDisabled: (command: CommandSuggestion) => boolean;
3
+ //# sourceMappingURL=useIsCommandDisabled.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useIsCommandDisabled.d.ts","sourceRoot":"","sources":["../../../../../src/contexts/messageInputContext/hooks/useIsCommandDisabled.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAwB,MAAM,aAAa,CAAC;AAU3E,eAAO,MAAM,oBAAoB,GAAI,SAAS,iBAAiB,YAW9D,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "stream-chat-react-native-core",
3
3
  "description": "The official React Native and Expo components for Stream Chat, a service for building chat applications",
4
- "version": "9.1.2-beta.3",
4
+ "version": "9.1.2-beta.4",
5
5
  "author": {
6
6
  "company": "Stream.io Inc",
7
7
  "name": "Stream.io Inc"
@@ -83,7 +83,7 @@
83
83
  "path": "0.12.7",
84
84
  "react-native-markdown-package": "1.8.2",
85
85
  "react-native-url-polyfill": "^2.0.0",
86
- "stream-chat": "^9.42.1",
86
+ "stream-chat": "^9.43.0",
87
87
  "use-sync-external-store": "^1.5.0"
88
88
  },
89
89
  "peerDependencies": {
@@ -58,9 +58,13 @@ export const AttachmentCommandNativePickerItem = ({ item }: { item: CommandSugge
58
58
  const { close } = useBottomSheetContext();
59
59
 
60
60
  const handlePress = useCallback(() => {
61
+ if (messageComposer.isCommandDisabled(item)) {
62
+ return;
63
+ }
64
+
61
65
  textComposer.setCommand(item);
62
66
  close(() => inputBoxRef.current?.focus());
63
- }, [textComposer, item, close, inputBoxRef]);
67
+ }, [messageComposer, item, textComposer, close, inputBoxRef]);
64
68
 
65
69
  return <AttachmentCommandPickerItemUI item={item} onPress={handlePress} />;
66
70
  };
@@ -73,9 +77,13 @@ export const AttachmentCommandPickerItem = ({ item }: { item: CommandSuggestion
73
77
  const { inputBoxRef } = useMessageInputContext();
74
78
 
75
79
  const handlePress = useCallback(() => {
80
+ if (messageComposer.isCommandDisabled(item)) {
81
+ return;
82
+ }
83
+
76
84
  textComposer.setCommand(item);
77
85
  inputBoxRef.current?.focus();
78
- }, [textComposer, item, inputBoxRef]);
86
+ }, [messageComposer, item, textComposer, inputBoxRef]);
79
87
 
80
88
  if (disableAttachmentPicker) {
81
89
  return <AttachmentCommandNativePickerItem item={item} />;
@@ -9,7 +9,6 @@ import { AttachmentCommandPicker } from './AttachmentPickerContent';
9
9
  import {
10
10
  useAttachmentPickerContext,
11
11
  useChannelContext,
12
- useMessageComposer,
13
12
  useMessageInputContext,
14
13
  useMessagesContext,
15
14
  useOwnCapabilitiesContext,
@@ -182,7 +181,6 @@ export const PollPickerButton = () => {
182
181
 
183
182
  export const CommandsPickerButton = () => {
184
183
  const [showCommandsSheet, setShowCommandsSheet] = useState(false);
185
- const messageComposer = useMessageComposer();
186
184
  const { hasCommands } = useMessageInputContext();
187
185
  const { attachmentPickerStore, disableAttachmentPicker } = useAttachmentPickerContext();
188
186
  const { selectedPicker } = useAttachmentPickerState();
@@ -197,7 +195,7 @@ export const CommandsPickerButton = () => {
197
195
 
198
196
  const onClose = useStableCallback(() => setShowCommandsSheet(false));
199
197
 
200
- return hasCommands && !messageComposer.editedMessage ? (
198
+ return hasCommands ? (
201
199
  <>
202
200
  <AttachmentTypePickerButton
203
201
  testID='commands-touchable'
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+
3
+ import { fireEvent, render, screen } from '@testing-library/react-native';
4
+ import type { CommandSuggestion } from 'stream-chat';
5
+
6
+ import {
7
+ AttachmentCommandNativePickerItem,
8
+ AttachmentCommandPickerItem,
9
+ } from '../AttachmentPickerContent';
10
+
11
+ jest.mock('stream-chat', () => ({
12
+ CommandSearchSource: jest.fn(() => ({
13
+ query: jest.fn(() => ({ items: [] })),
14
+ })),
15
+ }));
16
+
17
+ jest.mock('../AttachmentMediaPicker/AttachmentMediaPicker', () => ({
18
+ AttachmentMediaPicker: () => null,
19
+ }));
20
+
21
+ const mockClose = jest.fn((callback?: () => void) => callback?.());
22
+ const mockFocus = jest.fn();
23
+ const mockIsCommandDisabled = jest.fn();
24
+ const mockSetCommand = jest.fn();
25
+
26
+ jest.mock('../../../../contexts', () => ({
27
+ useAttachmentPickerContext: jest.fn(() => ({
28
+ disableAttachmentPicker: false,
29
+ })),
30
+ useBottomSheetContext: jest.fn(() => ({
31
+ close: mockClose,
32
+ })),
33
+ useMessageComposer: jest.fn(() => ({
34
+ isCommandDisabled: mockIsCommandDisabled,
35
+ textComposer: { setCommand: mockSetCommand },
36
+ })),
37
+ useMessageInputContext: jest.fn(() => ({
38
+ inputBoxRef: { current: { focus: mockFocus } },
39
+ })),
40
+ }));
41
+
42
+ jest.mock('../../../../contexts/themeContext/ThemeContext', () => ({
43
+ useTheme: jest.fn(() => ({
44
+ theme: {
45
+ semantics: {
46
+ backgroundUtilityPressed: '#f5f5f5',
47
+ },
48
+ },
49
+ })),
50
+ }));
51
+
52
+ jest.mock('../../../../hooks', () => ({
53
+ useAttachmentPickerState: jest.fn(() => ({ selectedPicker: 'images' })),
54
+ useStableCallback: (callback: unknown) => callback,
55
+ }));
56
+
57
+ jest.mock('../../../AutoCompleteInput/AutoCompleteSuggestionItem', () => {
58
+ const { Text } = require('react-native');
59
+
60
+ return {
61
+ CommandSuggestionItem: ({ name }: CommandSuggestion) => <Text>{name}</Text>,
62
+ };
63
+ });
64
+
65
+ const command = {
66
+ args: '',
67
+ id: 'ban',
68
+ name: 'ban',
69
+ set: 'moderation_set',
70
+ } as CommandSuggestion;
71
+
72
+ describe('AttachmentPickerContent commands', () => {
73
+ beforeEach(() => {
74
+ mockClose.mockClear();
75
+ mockFocus.mockClear();
76
+ mockIsCommandDisabled.mockReset();
77
+ mockSetCommand.mockClear();
78
+ });
79
+
80
+ it('does not focus the input when a disabled command is pressed', () => {
81
+ mockIsCommandDisabled.mockReturnValue(true);
82
+
83
+ render(<AttachmentCommandPickerItem item={command} />);
84
+
85
+ fireEvent.press(screen.getByText('ban'));
86
+
87
+ expect(mockIsCommandDisabled).toHaveBeenCalledWith(command);
88
+ expect(mockSetCommand).not.toHaveBeenCalled();
89
+ expect(mockFocus).not.toHaveBeenCalled();
90
+ });
91
+
92
+ it('does not close the picker or focus the input when a disabled command is pressed in native picker mode', () => {
93
+ mockIsCommandDisabled.mockReturnValue(true);
94
+
95
+ render(<AttachmentCommandNativePickerItem item={command} />);
96
+
97
+ fireEvent.press(screen.getByText('ban'));
98
+
99
+ expect(mockIsCommandDisabled).toHaveBeenCalledWith(command);
100
+ expect(mockSetCommand).not.toHaveBeenCalled();
101
+ expect(mockClose).not.toHaveBeenCalled();
102
+ expect(mockFocus).not.toHaveBeenCalled();
103
+ });
104
+ });
@@ -17,16 +17,15 @@ import {
17
17
  useChannelContext,
18
18
  } from '../../contexts/channelContext/ChannelContext';
19
19
  import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
20
- import {
21
- MessageInputContextValue,
22
- useMessageInputContext,
23
- } from '../../contexts/messageInputContext/MessageInputContext';
20
+ import type { MessageInputContextValue } from '../../contexts/messageInputContext/MessageInputContext';
21
+ import { useMessageInputContext } from '../../contexts/messageInputContext/MessageInputContext';
24
22
  import { useTheme } from '../../contexts/themeContext/ThemeContext';
25
23
  import {
26
24
  TranslationContextValue,
27
25
  useTranslationContext,
28
26
  } from '../../contexts/translationContext/TranslationContext';
29
27
 
28
+ import { useStableCallback } from '../../hooks';
30
29
  import { useStateStore } from '../../hooks/useStateStore';
31
30
  import { useCooldownRemaining } from '../MessageInput/hooks/useCooldownRemaining';
32
31
 
@@ -45,6 +44,19 @@ const TextInputRenderer = React.forwardRef<RNTextInput, AnimatedTextInputRendere
45
44
 
46
45
  const AnimatedTextInputRenderer = Animated.createAnimatedComponent(TextInputRenderer);
47
46
 
47
+ const setRef = <T,>(ref: React.Ref<T> | undefined, value: T | null) => {
48
+ if (!ref) {
49
+ return;
50
+ }
51
+
52
+ if (typeof ref === 'function') {
53
+ ref(value);
54
+ return;
55
+ }
56
+
57
+ (ref as React.RefObject<T | null>).current = value;
58
+ };
59
+
48
60
  type AutoCompleteInputPropsWithContext = TextInputProps &
49
61
  Pick<ChannelContextValue, 'channel'> &
50
62
  Pick<MessageInputContextValue, 'setInputBoxRef'> &
@@ -109,6 +121,30 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
109
121
  setLocalText(text);
110
122
  }, [text]);
111
123
 
124
+ const clearState = useCallback(() => {
125
+ setLocalText('');
126
+ }, []);
127
+
128
+ const restoreState = useStableCallback((restoredText: string) => {
129
+ setLocalText(restoredText);
130
+ });
131
+
132
+ const setExtendedInputRef = useCallback(
133
+ (ref: RNTextInput | null) => {
134
+ if (!ref) {
135
+ setRef(setInputBoxRef, null);
136
+ return;
137
+ }
138
+
139
+ const inputBoxRef = Object.assign(ref, {
140
+ clearState,
141
+ restoreState,
142
+ });
143
+ setRef(setInputBoxRef, inputBoxRef);
144
+ },
145
+ [clearState, restoreState, setInputBoxRef],
146
+ );
147
+
112
148
  const handleSelectionChange = useCallback(
113
149
  (e: TextInputSelectionChangeEvent) => {
114
150
  const { selection } = e.nativeEvent;
@@ -161,7 +197,7 @@ const AutoCompleteInputWithContext = (props: AutoCompleteInputPropsWithContext)
161
197
  onSelectionChange={handleSelectionChange}
162
198
  placeholder={placeholderText}
163
199
  placeholderTextColor={semantics.inputTextPlaceholder}
164
- ref={setInputBoxRef}
200
+ ref={setExtendedInputRef}
165
201
  style={[
166
202
  styles.inputBox,
167
203
  {
@@ -1,10 +1,11 @@
1
1
  import React, { useCallback, useMemo } from 'react';
2
2
  import { Pressable, StyleSheet, Text, View } from 'react-native';
3
3
 
4
- import { CommandSuggestion, TextComposerSuggestion, UserSuggestion } from 'stream-chat';
4
+ import type { CommandSuggestion, TextComposerSuggestion, UserSuggestion } from 'stream-chat';
5
5
 
6
6
  import { AutoCompleteSuggestionCommandIcon } from './AutoCompleteSuggestionCommandIcon';
7
7
 
8
+ import { useIsCommandDisabled } from '../../contexts/messageInputContext/hooks/useIsCommandDisabled';
8
9
  import { useMessageComposer } from '../../contexts/messageInputContext/hooks/useMessageComposer';
9
10
  import { useTheme } from '../../contexts/themeContext/ThemeContext';
10
11
  import { primitives } from '../../theme';
@@ -81,11 +82,17 @@ export const CommandSuggestionItem = (item: CommandSuggestion) => {
81
82
  } = useTheme();
82
83
  const styles = useStyles();
83
84
 
85
+ const isDisabled = useIsCommandDisabled(item);
86
+
84
87
  return (
85
88
  <View style={[styles.commandContainer, commandContainer]}>
86
89
  {name ? <AutoCompleteSuggestionCommandIcon name={name} /> : null}
87
90
  <Text
88
- style={[styles.title, { color: semantics.textPrimary }, title]}
91
+ style={[
92
+ styles.title,
93
+ { color: isDisabled ? semantics.textTertiary : semantics.textPrimary },
94
+ title,
95
+ ]}
89
96
  testID='commands-item-title'
90
97
  >
91
98
  {(name || '').replace(/^\w/, (char) => char.toUpperCase())}
@@ -1,9 +1,12 @@
1
1
  import React from 'react';
2
2
 
3
+ import type { TextInput } from 'react-native';
4
+
3
5
  import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
4
6
  import type { Channel as ChannelType, StreamChat } from 'stream-chat';
5
7
 
6
8
  import { OverlayProvider } from '../../../contexts';
9
+ import type { InputBoxRef } from '../../../contexts/messageInputContext/MessageInputContext';
7
10
  import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
8
11
  import type { ChannelProps } from '../../Channel/Channel';
9
12
  import { Channel } from '../../Channel/Channel';
@@ -123,6 +126,50 @@ describe('AutoCompleteInput', () => {
123
126
  });
124
127
  });
125
128
 
129
+ it('should expose imperative state handlers on the input ref', async () => {
130
+ let inputRef: InputBoxRef | null = null;
131
+ const text = 'hello';
132
+ const channelProps = {
133
+ channel,
134
+ setInputRef: (ref: TextInput | null) => {
135
+ inputRef = ref as InputBoxRef | null;
136
+ },
137
+ };
138
+ const props = {};
139
+
140
+ renderComponent({ channelProps, client, props });
141
+
142
+ await waitFor(() => {
143
+ expect(inputRef?.clearState).toBeTruthy();
144
+ expect(inputRef?.restoreState).toBeTruthy();
145
+ });
146
+
147
+ act(() => {
148
+ fireEvent.changeText(screen.getByTestId('auto-complete-text-input'), text);
149
+ });
150
+
151
+ await waitFor(() => {
152
+ expect(screen.getByTestId('auto-complete-text-input').props.value).toBe(text);
153
+ });
154
+
155
+ act(() => {
156
+ inputRef?.clearState();
157
+ });
158
+
159
+ await waitFor(() => {
160
+ expect(screen.getByTestId('auto-complete-text-input').props.value).toBe('');
161
+ expect(channel.messageComposer.textComposer.text).toBe(text);
162
+ });
163
+
164
+ act(() => {
165
+ inputRef?.restoreState(text);
166
+ });
167
+
168
+ await waitFor(() => {
169
+ expect(screen.getByTestId('auto-complete-text-input').props.value).toBe(text);
170
+ });
171
+ });
172
+
126
173
  it('should call the textComposer setSelection when the onSelectionChange is triggered', async () => {
127
174
  const { textComposer } = channel.messageComposer;
128
175
 
@@ -27,7 +27,7 @@ export const GiphyChip = () => {
27
27
 
28
28
  const onPressHandler = () => {
29
29
  textComposer.clearCommand();
30
- messageComposer?.restore();
30
+ // messageComposer?.restore();
31
31
  };
32
32
 
33
33
  return (
@@ -51,7 +51,7 @@ import { isTestEnvironment } from '../utils/isTestEnvironment';
51
51
 
52
52
  export type LocalMessageInputContext = {
53
53
  closeAttachmentPicker: () => void;
54
- inputBoxRef: React.RefObject<TextInput | null>;
54
+ inputBoxRef: React.RefObject<InputBoxRef | null>;
55
55
  openAttachmentPicker: () => void;
56
56
  /**
57
57
  * Function for picking a photo from native image picker and uploading it.
@@ -62,7 +62,7 @@ export type LocalMessageInputContext = {
62
62
  /**
63
63
  * Ref callback to set reference on input box
64
64
  */
65
- setInputBoxRef: Ref<TextInput> | undefined;
65
+ setInputBoxRef: Ref<InputBoxRef> | undefined;
66
66
  /**
67
67
  * Function for taking a photo and uploading it
68
68
  */
@@ -77,6 +77,11 @@ export type LocalMessageInputContext = {
77
77
  stopVoiceRecording: () => Promise<void>;
78
78
  };
79
79
 
80
+ export type InputBoxRef = TextInput & {
81
+ clearState: () => void;
82
+ restoreState: (text: string) => void;
83
+ };
84
+
80
85
  export type InputMessageInputContextValue = {
81
86
  /**
82
87
  * Controls how many pixels to the top side the user has to scroll in order to lock the recording view and allow the
@@ -215,7 +220,7 @@ export const MessageInputProvider = ({
215
220
  const { clearEditingState } = useMessageComposerAPIContext();
216
221
  const { thread } = useThreadContext();
217
222
  const { t } = useTranslationContext();
218
- const inputBoxRef = useRef<TextInput | null>(null);
223
+ const inputBoxRef = useRef<InputBoxRef | null>(null);
219
224
 
220
225
  const [showPollCreationDialog, setShowPollCreationDialog] = useState(false);
221
226
 
@@ -368,14 +373,18 @@ export const MessageInputProvider = ({
368
373
  }, [closePicker, attachmentPickerStore]);
369
374
 
370
375
  const sendMessage = useStableCallback(async () => {
371
- if (inputBoxRef.current) {
372
- inputBoxRef.current.clear();
373
- }
376
+ const textToRestore = messageComposer.textComposer.text;
377
+ let compositionAccepted = false;
378
+
379
+ inputBoxRef.current?.clearState();
374
380
 
375
381
  try {
376
382
  const composition = await messageComposer.compose();
377
383
 
378
- if (!composition || !composition.message) return;
384
+ if (!composition || !composition.message) {
385
+ inputBoxRef.current?.restoreState(textToRestore);
386
+ return;
387
+ }
379
388
 
380
389
  const { localMessage, message, sendOptions } = composition;
381
390
  const linkInfos = parseLinksFromText(localMessage.text);
@@ -386,9 +395,12 @@ export const MessageInputProvider = ({
386
395
  t('Sending links is not allowed in this conversation'),
387
396
  );
388
397
 
398
+ inputBoxRef.current?.restoreState(textToRestore);
389
399
  return;
390
400
  }
391
401
 
402
+ compositionAccepted = true;
403
+
392
404
  // MODERATION: This is for the case where the message is of type 'error' and if you try to edit it, it will throw an error.
393
405
  if (editedMessage && editedMessage.type !== 'error') {
394
406
  try {
@@ -425,11 +437,14 @@ export const MessageInputProvider = ({
425
437
  }
426
438
  }
427
439
  } catch (error) {
440
+ if (!compositionAccepted) {
441
+ inputBoxRef.current?.restoreState(textToRestore);
442
+ }
428
443
  console.error('Error while sending message:', error);
429
444
  }
430
445
  });
431
446
 
432
- const setInputBoxRef = useStableCallback((ref: TextInput | null) => {
447
+ const setInputBoxRef = useStableCallback((ref: InputBoxRef | null) => {
433
448
  inputBoxRef.current = ref;
434
449
  if (value.setInputRef) {
435
450
  value.setInputRef(ref);
@@ -22,6 +22,7 @@ import {
22
22
  MessageInputProvider,
23
23
  useMessageInputContext,
24
24
  } from '../MessageInputContext';
25
+ import type { InputBoxRef } from '../MessageInputContext';
25
26
 
26
27
  const Wrapper = ({
27
28
  messageComposerContextValue,
@@ -98,6 +99,53 @@ describe("MessageInputContext's sendMessage", () => {
98
99
  });
99
100
  });
100
101
 
102
+ it('should restore input state if composition is discarded', async () => {
103
+ const sendMessageMock = jest.fn();
104
+ const clearState = jest.fn();
105
+ const restoreState = jest.fn();
106
+ const initialProps = {
107
+ sendMessage: sendMessageMock,
108
+ };
109
+
110
+ const { result } = renderHook(() => useMessageInputContext(), {
111
+ initialProps,
112
+ wrapper: (props) => (
113
+ <Wrapper
114
+ client={chatClient}
115
+ messageComposerContextValue={{ channel }}
116
+ props={{ ...props, ...initialProps }}
117
+ />
118
+ ),
119
+ });
120
+
121
+ const text = 'Hello there';
122
+ const inputRef = {
123
+ clearState,
124
+ restoreState,
125
+ } as unknown as InputBoxRef;
126
+ (result.current.setInputBoxRef as (ref: InputBoxRef | null) => void)(inputRef);
127
+
128
+ await act(async () => {
129
+ await channel.messageComposer.textComposer.handleChange({
130
+ selection: {
131
+ end: text.length,
132
+ start: text.length,
133
+ },
134
+ text,
135
+ });
136
+ });
137
+
138
+ jest.spyOn(channel.messageComposer, 'compose').mockResolvedValue(undefined);
139
+
140
+ await act(async () => {
141
+ await result.current.sendMessage();
142
+ });
143
+
144
+ expect(clearState).toHaveBeenCalledTimes(1);
145
+ expect(restoreState).toHaveBeenCalledWith(text);
146
+ expect(sendMessageMock).not.toHaveBeenCalled();
147
+ });
148
+
101
149
  it('should get into the catch block if the sendMessage throws an error', async () => {
102
150
  const sendMessageMock = jest.fn();
103
151
  sendMessageMock.mockRejectedValue(new Error('Error sending message'));
@@ -0,0 +1,110 @@
1
+ import { act, cleanup, renderHook } from '@testing-library/react-native';
2
+ import type { CommandSuggestion, MessageComposerState } from 'stream-chat';
3
+
4
+ import { generateMessage } from '../../../mock-builders/generator/message';
5
+ import { useIsCommandDisabled } from '../hooks/useIsCommandDisabled';
6
+ import { useMessageComposer } from '../hooks/useMessageComposer';
7
+
8
+ jest.mock('../hooks/useMessageComposer', () => ({
9
+ useMessageComposer: jest.fn(),
10
+ }));
11
+
12
+ type TestMessageComposerStateStore = {
13
+ getLatestValue: () => MessageComposerState;
14
+ partialNext: (nextValue: Partial<MessageComposerState>) => void;
15
+ subscribeWithSelector: (
16
+ selector: (state: MessageComposerState) => Record<string, unknown>,
17
+ onStoreChange: () => void,
18
+ ) => () => void;
19
+ };
20
+
21
+ const createMessageComposerState = (): TestMessageComposerStateStore => {
22
+ let value: MessageComposerState = {
23
+ draftId: null,
24
+ editedMessage: null,
25
+ id: 'composer-id',
26
+ pollId: null,
27
+ quotedMessage: null,
28
+ showReplyInChannel: false,
29
+ };
30
+ const subscribers = new Set<() => void>();
31
+
32
+ return {
33
+ getLatestValue: () => value,
34
+ partialNext: (nextValue) => {
35
+ value = { ...value, ...nextValue };
36
+ subscribers.forEach((subscriber) => subscriber());
37
+ },
38
+ subscribeWithSelector: (_selector, onStoreChange) => {
39
+ subscribers.add(onStoreChange);
40
+ return () => {
41
+ subscribers.delete(onStoreChange);
42
+ };
43
+ },
44
+ };
45
+ };
46
+
47
+ const command = {
48
+ id: 'ban',
49
+ name: 'ban',
50
+ set: 'moderation_set',
51
+ } as CommandSuggestion;
52
+
53
+ describe('useIsCommandDisabled', () => {
54
+ const mockUseMessageComposer = useMessageComposer as jest.MockedFunction<
55
+ typeof useMessageComposer
56
+ >;
57
+
58
+ afterEach(() => {
59
+ jest.resetAllMocks();
60
+ cleanup();
61
+ });
62
+
63
+ it('recalculates when quoted message existence changes', () => {
64
+ const state = createMessageComposerState();
65
+ const messageComposer = {
66
+ isCommandDisabled: jest.fn(() => !!state.getLatestValue().quotedMessage),
67
+ state,
68
+ } as unknown as ReturnType<typeof useMessageComposer>;
69
+
70
+ mockUseMessageComposer.mockReturnValue(messageComposer);
71
+
72
+ const { result } = renderHook(() => useIsCommandDisabled(command));
73
+
74
+ expect(result.current).toBe(false);
75
+ expect(messageComposer.isCommandDisabled).toHaveBeenCalledTimes(1);
76
+
77
+ act(() => {
78
+ state.partialNext({ quotedMessage: generateMessage({ id: 'quoted-message' }) });
79
+ });
80
+
81
+ expect(result.current).toBe(true);
82
+ expect(messageComposer.isCommandDisabled).toHaveBeenCalledTimes(2);
83
+ });
84
+
85
+ it('does not recalculate when quoted message changes but existence does not', () => {
86
+ const state = createMessageComposerState();
87
+ const messageComposer = {
88
+ isCommandDisabled: jest.fn(() => !!state.getLatestValue().quotedMessage),
89
+ state,
90
+ } as unknown as ReturnType<typeof useMessageComposer>;
91
+
92
+ mockUseMessageComposer.mockReturnValue(messageComposer);
93
+
94
+ const { result } = renderHook(() => useIsCommandDisabled(command));
95
+
96
+ act(() => {
97
+ state.partialNext({ quotedMessage: generateMessage({ id: 'first-quoted-message' }) });
98
+ });
99
+
100
+ expect(result.current).toBe(true);
101
+ expect(messageComposer.isCommandDisabled).toHaveBeenCalledTimes(2);
102
+
103
+ act(() => {
104
+ state.partialNext({ quotedMessage: generateMessage({ id: 'second-quoted-message' }) });
105
+ });
106
+
107
+ expect(result.current).toBe(true);
108
+ expect(messageComposer.isCommandDisabled).toHaveBeenCalledTimes(2);
109
+ });
110
+ });