stream-chat-react-native-core 9.0.0-beta.22 → 9.0.0-beta.24

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 (45) hide show
  1. package/lib/commonjs/components/Channel/Channel.js +11 -10
  2. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  3. package/lib/commonjs/components/Message/hooks/useMessageActionHandlers.js +5 -1
  4. package/lib/commonjs/components/Message/hooks/useMessageActionHandlers.js.map +1 -1
  5. package/lib/commonjs/components/MessageInput/MessageComposer.js +0 -6
  6. package/lib/commonjs/components/MessageInput/MessageComposer.js.map +1 -1
  7. package/lib/commonjs/hooks/index.js +22 -0
  8. package/lib/commonjs/hooks/index.js.map +1 -1
  9. package/lib/commonjs/hooks/useAfterKeyboardOpenCallback.js +48 -0
  10. package/lib/commonjs/hooks/useAfterKeyboardOpenCallback.js.map +1 -0
  11. package/lib/commonjs/hooks/usePortalSettledCallback.js +41 -0
  12. package/lib/commonjs/hooks/usePortalSettledCallback.js.map +1 -0
  13. package/lib/commonjs/version.json +1 -1
  14. package/lib/module/components/Channel/Channel.js +11 -10
  15. package/lib/module/components/Channel/Channel.js.map +1 -1
  16. package/lib/module/components/Message/hooks/useMessageActionHandlers.js +5 -1
  17. package/lib/module/components/Message/hooks/useMessageActionHandlers.js.map +1 -1
  18. package/lib/module/components/MessageInput/MessageComposer.js +0 -6
  19. package/lib/module/components/MessageInput/MessageComposer.js.map +1 -1
  20. package/lib/module/hooks/index.js +22 -0
  21. package/lib/module/hooks/index.js.map +1 -1
  22. package/lib/module/hooks/useAfterKeyboardOpenCallback.js +48 -0
  23. package/lib/module/hooks/useAfterKeyboardOpenCallback.js.map +1 -0
  24. package/lib/module/hooks/usePortalSettledCallback.js +41 -0
  25. package/lib/module/hooks/usePortalSettledCallback.js.map +1 -0
  26. package/lib/module/version.json +1 -1
  27. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  28. package/lib/typescript/components/Message/hooks/useMessageActionHandlers.d.ts.map +1 -1
  29. package/lib/typescript/components/MessageInput/MessageComposer.d.ts.map +1 -1
  30. package/lib/typescript/hooks/index.d.ts +2 -0
  31. package/lib/typescript/hooks/index.d.ts.map +1 -1
  32. package/lib/typescript/hooks/useAfterKeyboardOpenCallback.d.ts +9 -0
  33. package/lib/typescript/hooks/useAfterKeyboardOpenCallback.d.ts.map +1 -0
  34. package/lib/typescript/hooks/usePortalSettledCallback.d.ts +32 -0
  35. package/lib/typescript/hooks/usePortalSettledCallback.d.ts.map +1 -0
  36. package/package.json +1 -1
  37. package/src/__tests__/offline-support/offline-feature.js +7 -4
  38. package/src/__tests__/offline-support/optimistic-update.js +125 -0
  39. package/src/components/Channel/Channel.tsx +11 -8
  40. package/src/components/Message/hooks/useMessageActionHandlers.ts +12 -2
  41. package/src/components/MessageInput/MessageComposer.tsx +1 -7
  42. package/src/hooks/index.ts +2 -0
  43. package/src/hooks/useAfterKeyboardOpenCallback.ts +62 -0
  44. package/src/hooks/usePortalSettledCallback.ts +78 -0
  45. package/src/version.json +1 -1
@@ -1167,6 +1167,15 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
1167
1167
  updated_at: message.updated_at?.toString(),
1168
1168
  }) as unknown as MessageResponse;
1169
1169
 
1170
+ const getRecoverableFailedMessages = (messages: LocalMessage[] = []) =>
1171
+ messages
1172
+ .filter(
1173
+ (message) =>
1174
+ message.status === MessageStatusTypes.FAILED &&
1175
+ !channel.state.findMessage(message.id, message.parent_id),
1176
+ )
1177
+ .map(parseMessage);
1178
+
1170
1179
  try {
1171
1180
  if (channelMessagesState?.messages) {
1172
1181
  await channel?.watch({
@@ -1181,9 +1190,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
1181
1190
  if (!thread) {
1182
1191
  copyChannelState();
1183
1192
 
1184
- const failedMessages = channelMessagesState.messages
1185
- ?.filter((message) => message.status === MessageStatusTypes.FAILED)
1186
- .map(parseMessage);
1193
+ const failedMessages = getRecoverableFailedMessages(channelMessagesState.messages);
1187
1194
  if (failedMessages?.length) {
1188
1195
  channel.state.addMessagesSorted(failedMessages);
1189
1196
  }
@@ -1192,11 +1199,7 @@ const ChannelWithContext = (props: PropsWithChildren<ChannelPropsWithContext>) =
1192
1199
  } else {
1193
1200
  await reloadThread();
1194
1201
 
1195
- const failedThreadMessages = thread
1196
- ? threadMessages
1197
- .filter((message) => message.status === MessageStatusTypes.FAILED)
1198
- .map(parseMessage)
1199
- : [];
1202
+ const failedThreadMessages = thread ? getRecoverableFailedMessages(threadMessages) : [];
1200
1203
  if (failedThreadMessages.length) {
1201
1204
  channel.state.addMessagesSorted(failedThreadMessages);
1202
1205
  setThreadMessages([...channel.state.threads[thread.id]]);
@@ -12,10 +12,20 @@ import type { MessageContextValue } from '../../../contexts/messageContext/Messa
12
12
  import type { MessagesContextValue } from '../../../contexts/messagesContext/MessagesContext';
13
13
 
14
14
  import { useTranslationContext } from '../../../contexts/translationContext/TranslationContext';
15
- import { useStableCallback } from '../../../hooks';
15
+ import {
16
+ useAfterKeyboardOpenCallback,
17
+ usePortalSettledCallback,
18
+ useStableCallback,
19
+ } from '../../../hooks';
16
20
  import { useTranslatedMessage } from '../../../hooks/useTranslatedMessage';
17
21
  import { NativeHandlers } from '../../../native';
18
22
 
23
+ const useWithPortalKeyboardSafety = <T extends unknown[]>(callback: (...args: T) => void) => {
24
+ const callbackAfterKeyboardOpen = useAfterKeyboardOpenCallback(callback);
25
+
26
+ return usePortalSettledCallback(callbackAfterKeyboardOpen);
27
+ };
28
+
19
29
  export const useMessageActionHandlers = ({
20
30
  channel,
21
31
  client,
@@ -114,7 +124,7 @@ export const useMessageActionHandlers = ({
114
124
  }
115
125
  });
116
126
 
117
- const handleEditMessage = useStableCallback(() => {
127
+ const handleEditMessage = useWithPortalKeyboardSafety(() => {
118
128
  setEditingState(message);
119
129
  });
120
130
 
@@ -219,7 +219,6 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => {
219
219
  closePollCreationDialog,
220
220
  CreatePollContent,
221
221
  createPollOptionGap,
222
- editing,
223
222
  InputView,
224
223
  MessageComposerLeadingView,
225
224
  MessageComposerTrailingView,
@@ -272,12 +271,6 @@ const MessageComposerWithContext = (props: MessageComposerPropsWithContext) => {
272
271
  [closeAttachmentPicker],
273
272
  );
274
273
 
275
- useEffect(() => {
276
- if (editing && inputBoxRef.current) {
277
- inputBoxRef.current.focus();
278
- }
279
- }, [editing, inputBoxRef]);
280
-
281
274
  /**
282
275
  * Effect to get the draft data for legacy thread composer and set it to message composer.
283
276
  * TODO: This can be removed once we remove legacy thread composer.
@@ -746,6 +739,7 @@ export const MessageComposer = (props: MessageComposerProps) => {
746
739
  closePollCreationDialog,
747
740
  compressImageQuality,
748
741
  CreatePollContent,
742
+ // TODO: probably not needed anymore, please check
749
743
  editing,
750
744
  Input,
751
745
  InputView,
@@ -7,8 +7,10 @@ export * from './useStableCallback';
7
7
  export * from './useLoadingImage';
8
8
  export * from './useMessageReminder';
9
9
  export * from './useQueryReminders';
10
+ export * from './useAfterKeyboardOpenCallback';
10
11
  export * from './useClientNotifications';
11
12
  export * from './useInAppNotificationsState';
13
+ export * from './usePortalSettledCallback';
12
14
  export * from './useRAFCoalescedValue';
13
15
  export * from './useAudioPlayerControl';
14
16
  export * from './useAttachmentPickerState';
@@ -0,0 +1,62 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { EventSubscription, Keyboard, Platform } from 'react-native';
3
+
4
+ import { useKeyboardVisibility } from './useKeyboardVisibility';
5
+
6
+ import { useStableCallback } from './useStableCallback';
7
+
8
+ import { KeyboardControllerPackage } from '../components/KeyboardCompatibleView/KeyboardControllerAvoidingView';
9
+ import { useMessageInputContext } from '../contexts/messageInputContext/MessageInputContext';
10
+
11
+ /**
12
+ * A utility hook that returns a stable callback which focuses the message input
13
+ * and invokes the callback once the keyboard is open.
14
+ *
15
+ * @param callback - callback we want to run once the keyboard is ready
16
+ * @returns A stable callback that will wait for the keyboard to be open before executing.
17
+ */
18
+ export const useAfterKeyboardOpenCallback = <T extends unknown[]>(
19
+ callback: (...args: T) => void,
20
+ ) => {
21
+ const isKeyboardVisible = useKeyboardVisibility();
22
+ const { inputBoxRef } = useMessageInputContext();
23
+ const keyboardSubscriptionRef = useRef<EventSubscription | undefined>(undefined);
24
+ // This callback runs from a keyboard event listener, so it must stay fresh across rerenders.
25
+ const stableCallback = useStableCallback(callback);
26
+
27
+ /** Clears the pending keyboard listener, if any. */
28
+ const clearKeyboardSubscription = useStableCallback(() => {
29
+ keyboardSubscriptionRef.current?.remove();
30
+ keyboardSubscriptionRef.current = undefined;
31
+ });
32
+
33
+ useEffect(() => clearKeyboardSubscription, [clearKeyboardSubscription]);
34
+
35
+ return useStableCallback((...args: T) => {
36
+ clearKeyboardSubscription();
37
+
38
+ const runCallback = () => {
39
+ clearKeyboardSubscription();
40
+ stableCallback(...args);
41
+ };
42
+
43
+ if (!inputBoxRef.current) {
44
+ runCallback();
45
+ return;
46
+ }
47
+
48
+ if (isKeyboardVisible) {
49
+ inputBoxRef.current.focus();
50
+ runCallback();
51
+ return;
52
+ }
53
+
54
+ const keyboardEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
55
+
56
+ keyboardSubscriptionRef.current = KeyboardControllerPackage?.KeyboardEvents
57
+ ? KeyboardControllerPackage.KeyboardEvents.addListener(keyboardEvent, runCallback)
58
+ : Keyboard.addListener(keyboardEvent, runCallback);
59
+
60
+ inputBoxRef.current.focus();
61
+ });
62
+ };
@@ -0,0 +1,78 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { Platform } from 'react-native';
3
+
4
+ import { useStableCallback } from './useStableCallback';
5
+
6
+ /**
7
+ * Number of frames we wait before invoking input focus sensitive work after the
8
+ * overlay closes.
9
+ */
10
+ const SETTLE_FRAMES = Platform.OS === 'android' ? 2 : 0;
11
+
12
+ /**
13
+ * Runs a callback after a fixed number of animation frames.
14
+ *
15
+ * We use RAFs here because the settling work we care about is tied to the next
16
+ * rendered frames after the overlay close transition.
17
+ *
18
+ * @param callback - callback to run once the frame budget has elapsed
19
+ * @param frames - number of frames to wait
20
+ * @param rafIds - accumulator used for later cancellation/cleanup
21
+ */
22
+ const scheduleAfterFrames = (callback: () => void, frames: number, rafIds: number[]) => {
23
+ if (frames <= 0) {
24
+ callback();
25
+ return;
26
+ }
27
+
28
+ const rafId = requestAnimationFrame(() => scheduleAfterFrames(callback, frames - 1, rafIds));
29
+ rafIds.push(rafId);
30
+ };
31
+
32
+ /**
33
+ * Returns a stable callback that is safe to run after a `PortalWhileClosingView`
34
+ * has settled back into its original tree.
35
+ *
36
+ * Some followup actions are sensitive to that handoff window. If they run
37
+ * while a view is still being returned from a portal host to its in place host,
38
+ * they can target a node that is about to be reattached. On Android, that is
39
+ * especially noticeable with focus sensitive work, where the target can lose
40
+ * focus again mid keyboard animation.
41
+ *
42
+ * Two frames are intentional here:
43
+ * - frame 1 lets the portal retarget and React commit the component tree
44
+ * - frame 2 lets the native view hierarchy settle in its final host
45
+ *
46
+ * iOS does not currently need this settle window for this flow.
47
+ *
48
+ * A good example is the message composer edit action: after closing the message
49
+ * overlay, we wait for the portal handoff to settle before focusing the input
50
+ * and opening the keyboard. Doing this prematurely will result in the keyboard
51
+ * being immediately closed.
52
+ *
53
+ * Another good example would be having a button wrapped in a `PortalWhileClosingView`,
54
+ * that possibly renders (or morphs into) something when pressed. Handling `onPress`
55
+ * prematurely here may lead to the morphed button rendering into a completely different
56
+ * part of the UI hierarchy, causing unknown behaviour. This hook prevents that from
57
+ * happening.
58
+ *
59
+ * @param callback - callback we want to invoke once the portal handoff has settled
60
+ * @returns A stable callback gated behind the portal settle window.
61
+ */
62
+ export const usePortalSettledCallback = <T extends unknown[]>(callback: (...args: T) => void) => {
63
+ const rafIdsRef = useRef<number[]>([]);
64
+ // This callback runs from deferred RAF work, so it must stay fresh across rerenders.
65
+ const stableCallback = useStableCallback(callback);
66
+
67
+ const clearScheduledFrames = useStableCallback(() => {
68
+ rafIdsRef.current.forEach((rafId) => cancelAnimationFrame(rafId));
69
+ rafIdsRef.current = [];
70
+ });
71
+
72
+ useEffect(() => clearScheduledFrames, [clearScheduledFrames]);
73
+
74
+ return useStableCallback((...args: T) => {
75
+ clearScheduledFrames();
76
+ scheduleAfterFrames(() => stableCallback(...args), SETTLE_FRAMES, rafIdsRef.current);
77
+ });
78
+ };
package/src/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "9.0.0-beta.22"
2
+ "version": "9.0.0-beta.24"
3
3
  }