stream-chat-react-native-core 9.1.2-beta.2 → 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/Chat/Chat.js +1 -1
- package/lib/commonjs/components/Chat/Chat.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/Chat/Chat.js +1 -1
- package/lib/module/components/Chat/Chat.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/components/Chat/Chat.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/Chat/Chat.tsx +4 -5
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React, { PropsWithChildren, useEffect, useMemo, useState } from 'react';
|
|
2
2
|
import { Platform } from 'react-native';
|
|
3
3
|
|
|
4
|
-
import { Channel, OfflineDBState
|
|
4
|
+
import { Channel, OfflineDBState } from 'stream-chat';
|
|
5
5
|
|
|
6
6
|
import { useClientMutedUsers } from './hooks';
|
|
7
7
|
import { useAppSettings } from './hooks/useAppSettings';
|
|
@@ -187,10 +187,9 @@ const ChatWithContext = (props: PropsWithChildren<ChatProps>) => {
|
|
|
187
187
|
|
|
188
188
|
useEffect(() => {
|
|
189
189
|
if (client) {
|
|
190
|
-
const sdkName = (
|
|
191
|
-
? NativeHandlers.SDK.replace('stream-chat-', '')
|
|
192
|
-
|
|
193
|
-
`-${Platform.OS as 'ios' | 'android'}`) as SdkIdentifier['name'];
|
|
190
|
+
const sdkName = (
|
|
191
|
+
NativeHandlers.SDK ? NativeHandlers.SDK.replace('stream-chat-', '') : 'react-native'
|
|
192
|
+
) as 'react-native' | 'expo';
|
|
194
193
|
client.sdkIdentifier = {
|
|
195
194
|
name: sdkName,
|
|
196
195
|
version,
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useMemo } from 'react';
|
|
2
|
+
|
|
3
|
+
import type { CommandSuggestion, MessageComposerState } from 'stream-chat';
|
|
4
|
+
|
|
5
|
+
import { useMessageComposer } from './useMessageComposer';
|
|
6
|
+
|
|
7
|
+
import { useStateStore } from '../../../hooks/useStateStore';
|
|
8
|
+
|
|
9
|
+
const hasQuotedMessageSelector = (state: MessageComposerState) => ({
|
|
10
|
+
hasQuotedMessage: !!state.quotedMessage,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
export const useIsCommandDisabled = (command: CommandSuggestion) => {
|
|
14
|
+
const messageComposer = useMessageComposer();
|
|
15
|
+
const { hasQuotedMessage } = useStateStore(messageComposer.state, hasQuotedMessageSelector);
|
|
16
|
+
|
|
17
|
+
return useMemo(
|
|
18
|
+
() => messageComposer.isCommandDisabled(command),
|
|
19
|
+
// isCommandDisabled reads quotedMessage through the composer state getter.
|
|
20
|
+
// Keep this dependency scoped to quote presence, not quote object identity.
|
|
21
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
22
|
+
[command, hasQuotedMessage, messageComposer],
|
|
23
|
+
);
|
|
24
|
+
};
|
package/src/version.json
CHANGED