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.
Files changed (59) 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/Chat/Chat.js +1 -1
  10. package/lib/commonjs/components/Chat/Chat.js.map +1 -1
  11. package/lib/commonjs/components/ui/GiphyChip.js +0 -1
  12. package/lib/commonjs/components/ui/GiphyChip.js.map +1 -1
  13. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js +16 -4
  14. package/lib/commonjs/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  15. package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
  16. package/lib/commonjs/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
  17. package/lib/commonjs/version.json +1 -1
  18. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js +8 -2
  19. package/lib/module/components/AttachmentPicker/components/AttachmentPickerContent.js.map +1 -1
  20. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js +1 -2
  21. package/lib/module/components/AttachmentPicker/components/AttachmentTypePickerButton.js.map +1 -1
  22. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js +29 -1
  23. package/lib/module/components/AutoCompleteInput/AutoCompleteInput.js.map +1 -1
  24. package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js +3 -1
  25. package/lib/module/components/AutoCompleteInput/AutoCompleteSuggestionItem.js.map +1 -1
  26. package/lib/module/components/Chat/Chat.js +1 -1
  27. package/lib/module/components/Chat/Chat.js.map +1 -1
  28. package/lib/module/components/ui/GiphyChip.js +0 -1
  29. package/lib/module/components/ui/GiphyChip.js.map +1 -1
  30. package/lib/module/contexts/messageInputContext/MessageInputContext.js +16 -4
  31. package/lib/module/contexts/messageInputContext/MessageInputContext.js.map +1 -1
  32. package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js +21 -0
  33. package/lib/module/contexts/messageInputContext/hooks/useIsCommandDisabled.js.map +1 -0
  34. package/lib/module/version.json +1 -1
  35. package/lib/typescript/components/AttachmentPicker/components/AttachmentPickerContent.d.ts.map +1 -1
  36. package/lib/typescript/components/AttachmentPicker/components/AttachmentTypePickerButton.d.ts.map +1 -1
  37. package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts +1 -1
  38. package/lib/typescript/components/AutoCompleteInput/AutoCompleteInput.d.ts.map +1 -1
  39. package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts +1 -1
  40. package/lib/typescript/components/AutoCompleteInput/AutoCompleteSuggestionItem.d.ts.map +1 -1
  41. package/lib/typescript/components/Chat/Chat.d.ts.map +1 -1
  42. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts +6 -2
  43. package/lib/typescript/contexts/messageInputContext/MessageInputContext.d.ts.map +1 -1
  44. package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts +3 -0
  45. package/lib/typescript/contexts/messageInputContext/hooks/useIsCommandDisabled.d.ts.map +1 -0
  46. package/package.json +2 -2
  47. package/src/components/AttachmentPicker/components/AttachmentPickerContent.tsx +10 -2
  48. package/src/components/AttachmentPicker/components/AttachmentTypePickerButton.tsx +1 -3
  49. package/src/components/AttachmentPicker/components/__tests__/AttachmentPickerContent.test.tsx +104 -0
  50. package/src/components/AutoCompleteInput/AutoCompleteInput.tsx +41 -5
  51. package/src/components/AutoCompleteInput/AutoCompleteSuggestionItem.tsx +9 -2
  52. package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.tsx +47 -0
  53. package/src/components/Chat/Chat.tsx +4 -5
  54. package/src/components/ui/GiphyChip.tsx +1 -1
  55. package/src/contexts/messageInputContext/MessageInputContext.tsx +23 -8
  56. package/src/contexts/messageInputContext/__tests__/sendMessage.test.tsx +48 -0
  57. package/src/contexts/messageInputContext/__tests__/useIsCommandDisabled.test.tsx +110 -0
  58. package/src/contexts/messageInputContext/hooks/useIsCommandDisabled.ts +24 -0
  59. 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, SdkIdentifier } from 'stream-chat';
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 = (((NativeHandlers.SDK
191
- ? NativeHandlers.SDK.replace('stream-chat-', '')
192
- : 'react-native') as 'react-native' | 'expo') +
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,
@@ -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
+ });
@@ -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
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "9.1.2-beta.2"
2
+ "version": "9.1.2-beta.4"
3
3
  }