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.
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -2
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -2
- package/lib/commonjs/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js +29 -1
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteSuggestionItem.js +3 -1
- package/lib/commonjs/components/AutoCompleteInput/AutoCompleteSuggestionItem.js.map +1 -1
- package/lib/commonjs/components/ui/GiphyChip.js +0 -1
- package/lib/commonjs/components/ui/GiphyChip.js.map +1 -1
- package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +16 -4
- package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
- package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
- package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
- package/lib/commonjs/version.json +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -2
- package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
- package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -2
- package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +29 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js +3 -1
- package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js.map +1 -1
- package/lib/module/components/ui/GiphyChip.js +0 -1
- package/lib/module/components/ui/GiphyChip.js.map +1 -1
- package/lib/module/contexts/messageInputContext/MessageInputContext.js +16 -4
- package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
- package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
- package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
- package/lib/module/version.json +1 -1
- package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerContent.d.ts.map +1 -1
- package/lib/typescript/components/AttachmentPicker/components/AttachmentTypePickerButton.d.ts.map +1 -1
- package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts +1 -1
- package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts.map +1 -1
- package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts +1 -1
- package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts.map +1 -1
- package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts +6 -2
- package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts.map +1 -1
- package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts +3 -0
- package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx +10 -2
- package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx +1 -3
- package/src/components/AttachmentPicker/components/__tests__/AttachmentPickerContent.test.tsx +104 -0
- package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +41 -5
- package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx +9 -2
- package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.tsx +47 -0
- package/src/components/ui/GiphyChip.tsx +1 -1
- package/src/contexts/messageInputContext/MessageInputContext.tsx +23 -8
- package/src/contexts/messageInputContext/__tests__/sendMessage.test.tsx +48 -0
- package/src/contexts/messageInputContext/__tests__/useIsCommandDisabled.test.tsx +110 -0
- package/src/contexts/messageInputContext/hooks/useIsCommandDisabled.ts +24 -0
- 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<
|
|
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<
|
|
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":"AAAA,OAAO,KAAK,EAAE,EACZ,iBAAiB,EACjB,GAAG,EAMJ,MAAM,OAAO,CAAC;AACf,OAAO,EAAkB,SAAS,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAGzE,OAAO,EACL,YAAY,EAEZ,kBAAkB,EAClB,UAAU,EACV,OAAO,IAAI,aAAa,EACxB,oBAAoB,EACpB,eAAe,EAChB,MAAM,aAAa,CAAC;AAerB,OAAO,EAA6B,UAAU,EAAkB,MAAM,cAAc,CAAC;AACrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,0CAA0C,CAAC;AAChF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8CAA8C,CAAC;AACvF,OAAO,EAAE,IAAI,EAAE,MAAM,mBAAmB,CAAC;AAazC,MAAM,MAAM,wBAAwB,GAAG;IACrC,qBAAqB,EAAE,MAAM,IAAI,CAAC;IAClC,WAAW,EAAE,KAAK,CAAC,SAAS,CAAC,
|
|
1
|
+
{"version":3,"file":"MessageInputContext.d.ts","sourceRoot":"","sources":["../../../../src/contexts/messageInputContext/MessageInputContext.tsx"],"names":[],"mappings":"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"}
|
|
@@ -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.
|
|
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.
|
|
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
|
-
}, [
|
|
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
|
-
}, [
|
|
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
|
|
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
|
-
|
|
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={
|
|
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={[
|
|
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
|
|
|
@@ -51,7 +51,7 @@ import { isTestEnvironment } from '../utils/isTestEnvironment';
|
|
|
51
51
|
|
|
52
52
|
export type LocalMessageInputContext = {
|
|
53
53
|
closeAttachmentPicker: () => void;
|
|
54
|
-
inputBoxRef: React.RefObject<
|
|
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<
|
|
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<
|
|
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
|
-
|
|
372
|
-
|
|
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)
|
|
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:
|
|
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
|
+
});
|