stream-chat-react-native-core 9.0.1-beta.1 → 9.0.1-beta.3

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 (102) hide show
  1. package/lib/commonjs/components/Channel/Channel.js +2 -0
  2. package/lib/commonjs/components/Channel/Channel.js.map +1 -1
  3. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js +3 -1
  4. package/lib/commonjs/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  5. package/lib/commonjs/components/Message/Message.js +99 -74
  6. package/lib/commonjs/components/Message/Message.js.map +1 -1
  7. package/lib/commonjs/components/Message/MessageOverlayWrapper.js +64 -0
  8. package/lib/commonjs/components/Message/MessageOverlayWrapper.js.map +1 -0
  9. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js +5 -1
  10. package/lib/commonjs/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  11. package/lib/commonjs/components/Message/hooks/useShouldUseOverlayStyles.js +8 -1
  12. package/lib/commonjs/components/Message/hooks/useShouldUseOverlayStyles.js.map +1 -1
  13. package/lib/commonjs/components/Message/messageOverlayConstants.js +6 -0
  14. package/lib/commonjs/components/Message/messageOverlayConstants.js.map +1 -0
  15. package/lib/commonjs/components/index.js +11 -11
  16. package/lib/commonjs/components/index.js.map +1 -1
  17. package/lib/commonjs/contexts/componentsContext/defaultComponents.js +0 -2
  18. package/lib/commonjs/contexts/componentsContext/defaultComponents.js.map +1 -1
  19. package/lib/commonjs/contexts/messageContext/MessageContext.js +32 -1
  20. package/lib/commonjs/contexts/messageContext/MessageContext.js.map +1 -1
  21. package/lib/commonjs/contexts/messagesContext/MessagesContext.js.map +1 -1
  22. package/lib/commonjs/hooks/index.js +4 -4
  23. package/lib/commonjs/hooks/index.js.map +1 -1
  24. package/lib/commonjs/version.json +1 -1
  25. package/lib/module/components/Channel/Channel.js +2 -0
  26. package/lib/module/components/Channel/Channel.js.map +1 -1
  27. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js +3 -1
  28. package/lib/module/components/Channel/hooks/useCreateMessagesContext.js.map +1 -1
  29. package/lib/module/components/Message/Message.js +99 -74
  30. package/lib/module/components/Message/Message.js.map +1 -1
  31. package/lib/module/components/Message/MessageOverlayWrapper.js +64 -0
  32. package/lib/module/components/Message/MessageOverlayWrapper.js.map +1 -0
  33. package/lib/module/components/Message/hooks/useCreateMessageContext.js +5 -1
  34. package/lib/module/components/Message/hooks/useCreateMessageContext.js.map +1 -1
  35. package/lib/module/components/Message/hooks/useShouldUseOverlayStyles.js +8 -1
  36. package/lib/module/components/Message/hooks/useShouldUseOverlayStyles.js.map +1 -1
  37. package/lib/module/components/Message/messageOverlayConstants.js +6 -0
  38. package/lib/module/components/Message/messageOverlayConstants.js.map +1 -0
  39. package/lib/module/components/index.js +11 -11
  40. package/lib/module/components/index.js.map +1 -1
  41. package/lib/module/contexts/componentsContext/defaultComponents.js +0 -2
  42. package/lib/module/contexts/componentsContext/defaultComponents.js.map +1 -1
  43. package/lib/module/contexts/messageContext/MessageContext.js +32 -1
  44. package/lib/module/contexts/messageContext/MessageContext.js.map +1 -1
  45. package/lib/module/contexts/messagesContext/MessagesContext.js.map +1 -1
  46. package/lib/module/hooks/index.js +4 -4
  47. package/lib/module/hooks/index.js.map +1 -1
  48. package/lib/module/version.json +1 -1
  49. package/lib/typescript/components/Channel/Channel.d.ts +1 -1
  50. package/lib/typescript/components/Channel/Channel.d.ts.map +1 -1
  51. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts +1 -1
  52. package/lib/typescript/components/Channel/hooks/useCreateMessagesContext.d.ts.map +1 -1
  53. package/lib/typescript/components/Message/Message.d.ts +1 -1
  54. package/lib/typescript/components/Message/Message.d.ts.map +1 -1
  55. package/lib/typescript/components/Message/MessageOverlayWrapper.d.ts +18 -0
  56. package/lib/typescript/components/Message/MessageOverlayWrapper.d.ts.map +1 -0
  57. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts +1 -1
  58. package/lib/typescript/components/Message/hooks/useCreateMessageContext.d.ts.map +1 -1
  59. package/lib/typescript/components/Message/hooks/useShouldUseOverlayStyles.d.ts.map +1 -1
  60. package/lib/typescript/components/Message/messageOverlayConstants.d.ts +2 -0
  61. package/lib/typescript/components/Message/messageOverlayConstants.d.ts.map +1 -0
  62. package/lib/typescript/components/index.d.ts +1 -1
  63. package/lib/typescript/components/index.d.ts.map +1 -1
  64. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts +0 -1
  65. package/lib/typescript/contexts/componentsContext/defaultComponents.d.ts.map +1 -1
  66. package/lib/typescript/contexts/messageContext/MessageContext.d.ts +26 -0
  67. package/lib/typescript/contexts/messageContext/MessageContext.d.ts.map +1 -1
  68. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts +5 -0
  69. package/lib/typescript/contexts/messagesContext/MessagesContext.d.ts.map +1 -1
  70. package/lib/typescript/hooks/index.d.ts +1 -1
  71. package/lib/typescript/hooks/index.d.ts.map +1 -1
  72. package/package.json +1 -1
  73. package/src/components/Channel/Channel.tsx +3 -0
  74. package/src/components/Channel/hooks/useCreateMessagesContext.ts +3 -0
  75. package/src/components/Message/Message.tsx +109 -77
  76. package/src/components/Message/MessageItemView/__tests__/Message.test.js +87 -7
  77. package/src/components/Message/MessageOverlayWrapper.tsx +81 -0
  78. package/src/components/Message/hooks/__tests__/useShouldUseOverlayStyles.test.tsx +17 -2
  79. package/src/components/Message/hooks/useCreateMessageContext.ts +5 -0
  80. package/src/components/Message/hooks/useShouldUseOverlayStyles.ts +15 -2
  81. package/src/components/Message/messageOverlayConstants.ts +1 -0
  82. package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +12 -4
  83. package/src/components/index.ts +1 -1
  84. package/src/contexts/componentsContext/defaultComponents.ts +0 -2
  85. package/src/contexts/messageContext/MessageContext.tsx +44 -0
  86. package/src/contexts/messagesContext/MessagesContext.tsx +5 -0
  87. package/src/hooks/index.ts +1 -1
  88. package/src/version.json +1 -1
  89. package/lib/commonjs/components/MessageMenu/MessageMenu.js +0 -29
  90. package/lib/commonjs/components/MessageMenu/MessageMenu.js.map +0 -1
  91. package/lib/commonjs/hooks/useAudioPlayerControl.js +0 -43
  92. package/lib/commonjs/hooks/useAudioPlayerControl.js.map +0 -1
  93. package/lib/module/components/MessageMenu/MessageMenu.js +0 -29
  94. package/lib/module/components/MessageMenu/MessageMenu.js.map +0 -1
  95. package/lib/module/hooks/useAudioPlayerControl.js +0 -43
  96. package/lib/module/hooks/useAudioPlayerControl.js.map +0 -1
  97. package/lib/typescript/components/MessageMenu/MessageMenu.d.ts +0 -40
  98. package/lib/typescript/components/MessageMenu/MessageMenu.d.ts.map +0 -1
  99. package/lib/typescript/hooks/useAudioPlayerControl.d.ts +0 -18
  100. package/lib/typescript/hooks/useAudioPlayerControl.d.ts.map +0 -1
  101. package/src/components/MessageMenu/MessageMenu.tsx +0 -107
  102. package/src/hooks/useAudioPlayerControl.ts +0 -59
@@ -19,6 +19,8 @@ import { useMessageActions } from './hooks/useMessageActions';
19
19
  import { useMessageDeliveredData } from './hooks/useMessageDeliveryData';
20
20
  import { useMessageReadData } from './hooks/useMessageReadData';
21
21
  import { useProcessReactions } from './hooks/useProcessReactions';
22
+ import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from './messageOverlayConstants';
23
+ import { MessageOverlayWrapper } from './MessageOverlayWrapper';
22
24
  import { measureInWindow } from './utils/measureInWindow';
23
25
  import { messageActions as defaultMessageActions } from './utils/messageActions';
24
26
 
@@ -36,7 +38,11 @@ import {
36
38
  MessageComposerAPIContextValue,
37
39
  useMessageComposerAPIContext,
38
40
  } from '../../contexts/messageComposerContext/MessageComposerAPIContext';
39
- import { MessageContextValue, MessageProvider } from '../../contexts/messageContext/MessageContext';
41
+ import {
42
+ MessageContextValue,
43
+ MessageOverlayRuntimeProvider,
44
+ MessageProvider,
45
+ } from '../../contexts/messageContext/MessageContext';
40
46
  import { useMessageListItemContext } from '../../contexts/messageListItemContext/MessageListItemContext';
41
47
  import {
42
48
  MessagesContextValue,
@@ -207,6 +213,7 @@ export type MessagePropsWithContext = Pick<
207
213
  | 'handleBlockUser'
208
214
  | 'isAttachmentEqual'
209
215
  | 'messageActions'
216
+ | 'messageOverlayTargetId'
210
217
  | 'messageContentOrder'
211
218
  | 'onLongPressMessage'
212
219
  | 'onPressInMessage'
@@ -278,6 +285,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
278
285
  members,
279
286
  message,
280
287
  messageActions: messageActionsProp = defaultMessageActions,
288
+ messageOverlayTargetId = DEFAULT_MESSAGE_OVERLAY_TARGET_ID,
281
289
  messageContentOrder: messageContentOrderProp,
282
290
  messagesContext,
283
291
  onLongPressMessage: onLongPressMessageProp,
@@ -323,11 +331,28 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
323
331
  const rectRef = useRef<Rect>(undefined);
324
332
  const bubbleRect = useRef<Rect>(undefined);
325
333
  const contextMenuAnchorRef = useRef<View>(null);
334
+ const messageOverlayTargetsRef = useRef<Record<string, View | null>>({});
335
+ const registerMessageOverlayTarget = useStableCallback(
336
+ ({ id, view }: { id: string; view: View | null }) => {
337
+ messageOverlayTargetsRef.current[id] = view;
338
+ },
339
+ );
340
+ const unregisterMessageOverlayTarget = useStableCallback((id: string) => {
341
+ delete messageOverlayTargetsRef.current[id];
342
+ });
326
343
 
327
344
  const showMessageOverlay = useStableCallback(async () => {
328
345
  dismissKeyboard();
329
346
  try {
330
- const layout = await measureInWindow(messageWrapperRef, insets);
347
+ const activeTargetView = messageOverlayTargetsRef.current[messageOverlayTargetId];
348
+
349
+ if (!activeTargetView) {
350
+ throw new Error(
351
+ `No message overlay target is registered for target id "${messageOverlayTargetId}".`,
352
+ );
353
+ }
354
+
355
+ const layout = await measureInWindow({ current: activeTargetView }, insets);
331
356
  const bubbleLayout = await measureInWindow(contextMenuAnchorRef, insets).catch(() => layout);
332
357
 
333
358
  rectRef.current = layout;
@@ -655,8 +680,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
655
680
  unpinMessage: handleTogglePinMessage,
656
681
  };
657
682
 
658
- const messageWrapperRef = useRef<View>(null);
659
-
660
683
  const onLongPress = () => {
661
684
  setNativeScrollability(false);
662
685
  if (hasAttachmentActions || isBlockedMessage(message) || !enableLongPress) {
@@ -771,6 +794,8 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
771
794
  onThreadSelect,
772
795
  otherAttachments: attachments.other,
773
796
  preventPress: overlayActive ? true : preventPress,
797
+ registerMessageOverlayTarget,
798
+ unregisterMessageOverlayTarget,
774
799
  reactions,
775
800
  readBy,
776
801
  setQuotedMessage,
@@ -781,6 +806,14 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
781
806
  threadList,
782
807
  videos: attachments.videos,
783
808
  });
809
+ const messageOverlayRuntimeContext = useMemo(
810
+ () => ({
811
+ overlayTargetRectRef: rectRef,
812
+ messageOverlayTargetId,
813
+ overlayActive,
814
+ }),
815
+ [messageOverlayTargetId, overlayActive],
816
+ );
784
817
 
785
818
  const prevActive = useRef<boolean>(overlayActive);
786
819
 
@@ -817,82 +850,74 @@ const MessageWithContext = (props: MessagePropsWithContext) => {
817
850
 
818
851
  return (
819
852
  <MessageProvider value={messageContext}>
820
- <View style={[style, styles.wrapper]} testID='message-wrapper'>
821
- {overlayActive && rect ? (
822
- <View
823
- style={{
824
- height: rect.h,
825
- width: rect.w,
826
- }}
827
- />
828
- ) : null}
829
- {/*TODO: V9: Find a way to separate these in a dedicated file*/}
830
- <Portal hostName={overlayActive && rect ? 'top-item' : undefined}>
831
- {overlayActive && rect && overlayItemsAnchorRect ? (
832
- <View
833
- onLayout={(e) => {
834
- const { width: w, height: h } = e.nativeEvent.layout;
835
-
836
- setOverlayTopH({
837
- h,
838
- w,
839
- x:
840
- overlayItemAlignment === 'right'
841
- ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w
842
- : overlayItemsAnchorRect.x,
843
- y: rect.y - h,
844
- });
845
- }}
846
- >
847
- <MessageReactionPicker
848
- dismissOverlay={dismissOverlay}
849
- handleReaction={ownCapabilities.sendReaction ? handleReaction : undefined}
850
- />
851
- </View>
852
- ) : null}
853
- </Portal>
854
- <Portal
855
- hostName={overlayActive ? 'message-overlay' : undefined}
856
- style={overlayActive && rect ? { width: rect.w } : undefined}
857
- >
858
- <View ref={messageWrapperRef}>
853
+ <MessageOverlayRuntimeProvider value={messageOverlayRuntimeContext}>
854
+ <View style={[style, styles.wrapper]} testID='message-wrapper'>
855
+ {/*TODO: V9: Find a way to separate these in a dedicated file*/}
856
+ <Portal hostName={overlayActive && rect ? 'top-item' : undefined}>
857
+ {overlayActive && rect && overlayItemsAnchorRect ? (
858
+ <View
859
+ onLayout={(e) => {
860
+ const { width: w, height: h } = e.nativeEvent.layout;
861
+
862
+ setOverlayTopH({
863
+ h,
864
+ w,
865
+ x:
866
+ overlayItemAlignment === 'right'
867
+ ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w
868
+ : overlayItemsAnchorRect.x,
869
+ y: rect.y - h,
870
+ });
871
+ }}
872
+ >
873
+ <MessageReactionPicker
874
+ dismissOverlay={dismissOverlay}
875
+ handleReaction={ownCapabilities.sendReaction ? handleReaction : undefined}
876
+ />
877
+ </View>
878
+ ) : null}
879
+ </Portal>
880
+ <MessageOverlayWrapper targetId={DEFAULT_MESSAGE_OVERLAY_TARGET_ID}>
859
881
  <MessageItemView />
860
- </View>
861
- </Portal>
862
- {showMessageReactions ? (
863
- <BottomSheetModal
864
- lazy={true}
865
- onClose={() => setShowMessageReactions(false)}
866
- visible={showMessageReactions}
867
- height={424}
868
- >
869
- <MessageUserReactions message={message} selectedReaction={selectedReaction} />
870
- </BottomSheetModal>
871
- ) : null}
872
- <Portal hostName={overlayActive && rect ? 'bottom-item' : undefined}>
873
- {overlayActive && rect && overlayItemsAnchorRect ? (
874
- <View
875
- onLayout={(e) => {
876
- const { width: w, height: h } = e.nativeEvent.layout;
877
- setOverlayBottomH({
878
- h,
879
- w,
880
- x:
881
- overlayItemAlignment === 'right'
882
- ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w
883
- : overlayItemsAnchorRect.x,
884
- y: rect.y + rect.h,
885
- });
886
- }}
882
+ </MessageOverlayWrapper>
883
+ {showMessageReactions ? (
884
+ <BottomSheetModal
885
+ lazy={true}
886
+ onClose={() => setShowMessageReactions(false)}
887
+ visible={showMessageReactions}
888
+ height={424}
887
889
  >
888
- <MessageActionList dismissOverlay={dismissOverlay} messageActions={messageActions} />
889
- </View>
890
+ <MessageUserReactions message={message} selectedReaction={selectedReaction} />
891
+ </BottomSheetModal>
890
892
  ) : null}
891
- </Portal>
892
- {isBounceDialogOpen ? (
893
- <MessageBounce setIsBounceDialogOpen={setIsBounceDialogOpen} />
894
- ) : null}
895
- </View>
893
+ <Portal hostName={overlayActive && rect ? 'bottom-item' : undefined}>
894
+ {overlayActive && rect && overlayItemsAnchorRect ? (
895
+ <View
896
+ onLayout={(e) => {
897
+ const { width: w, height: h } = e.nativeEvent.layout;
898
+ setOverlayBottomH({
899
+ h,
900
+ w,
901
+ x:
902
+ overlayItemAlignment === 'right'
903
+ ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w
904
+ : overlayItemsAnchorRect.x,
905
+ y: rect.y + rect.h,
906
+ });
907
+ }}
908
+ >
909
+ <MessageActionList
910
+ dismissOverlay={dismissOverlay}
911
+ messageActions={messageActions}
912
+ />
913
+ </View>
914
+ ) : null}
915
+ </Portal>
916
+ {isBounceDialogOpen ? (
917
+ <MessageBounce setIsBounceDialogOpen={setIsBounceDialogOpen} />
918
+ ) : null}
919
+ </View>
920
+ </MessageOverlayRuntimeProvider>
896
921
  </MessageProvider>
897
922
  );
898
923
  };
@@ -905,6 +930,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
905
930
  groupStyles: prevGroupStyles,
906
931
  isAttachmentEqual,
907
932
  isTargetedMessage: prevIsTargetedMessage,
933
+ messageOverlayTargetId: prevMessageOverlayTargetId,
908
934
  members: prevMembers,
909
935
  message: prevMessage,
910
936
  messagesContext: prevMessagesContext,
@@ -918,6 +944,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
918
944
  goToMessage: nextGoToMessage,
919
945
  groupStyles: nextGroupStyles,
920
946
  isTargetedMessage: nextIsTargetedMessage,
947
+ messageOverlayTargetId: nextMessageOverlayTargetId,
921
948
  members: nextMembers,
922
949
  message: nextMessage,
923
950
  messagesContext: nextMessagesContext,
@@ -958,6 +985,11 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit
958
985
  return false;
959
986
  }
960
987
 
988
+ const messageOverlayTargetIdEqual = prevMessageOverlayTargetId === nextMessageOverlayTargetId;
989
+ if (!messageOverlayTargetIdEqual) {
990
+ return false;
991
+ }
992
+
961
993
  const messageEqual = checkMessageEquality(prevMessage, nextMessage);
962
994
 
963
995
  if (!messageEqual) {
@@ -1,9 +1,13 @@
1
1
  import React from 'react';
2
2
 
3
+ import { Pressable, Text, View } from 'react-native';
3
4
  import { SafeAreaProvider } from 'react-native-safe-area-context';
4
5
 
5
6
  import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native';
6
7
 
8
+ import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext';
9
+ import { useMessageContext } from '../../../../contexts/messageContext/MessageContext';
10
+ import { MessageListItemProvider } from '../../../../contexts/messageListItemContext/MessageListItemContext';
7
11
  import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider';
8
12
  import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel';
9
13
 
@@ -16,7 +20,38 @@ import { getTestClientWithUser } from '../../../../mock-builders/mock';
16
20
  import { Channel } from '../../../Channel/Channel';
17
21
  import { Chat } from '../../../Chat/Chat';
18
22
  import { MessageComposer } from '../../../MessageInput/MessageComposer';
23
+ import { useShouldUseOverlayStyles } from '../../hooks/useShouldUseOverlayStyles';
19
24
  import { Message } from '../../Message';
25
+ import { MessageOverlayWrapper } from '../../MessageOverlayWrapper';
26
+
27
+ const OverlayStateText = ({ label }) => {
28
+ const shouldUseOverlayStyles = useShouldUseOverlayStyles();
29
+
30
+ return <Text>{`${label}:${shouldUseOverlayStyles ? 'overlay' : 'normal'}`}</Text>;
31
+ };
32
+
33
+ const OverlayTrigger = () => {
34
+ const { onLongPress } = useMessageContext();
35
+
36
+ return (
37
+ <Pressable
38
+ onLongPress={() => onLongPress({ emitter: 'message' })}
39
+ testID='custom-overlay-trigger'
40
+ >
41
+ <Text>Open overlay</Text>
42
+ </Pressable>
43
+ );
44
+ };
45
+
46
+ const CustomMessageItemView = () => (
47
+ <View testID='custom-message-item-view'>
48
+ <OverlayStateText label='outside' />
49
+ <MessageOverlayWrapper targetId='custom-overlay-target' testID='custom-overlay-target'>
50
+ <OverlayStateText label='inside' />
51
+ <OverlayTrigger />
52
+ </MessageOverlayWrapper>
53
+ </View>
54
+ );
20
55
 
21
56
  describe('Message', () => {
22
57
  let channel;
@@ -37,16 +72,34 @@ describe('Message', () => {
37
72
  useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
38
73
  channel = chatClient.channel('messaging', mockedChannel.id);
39
74
 
40
- renderMessage = (options) =>
75
+ renderMessage = (options, channelProps, componentOverrides) =>
41
76
  render(
42
77
  <SafeAreaProvider>
43
78
  <OverlayProvider>
44
- <Chat client={chatClient}>
45
- <Channel channel={channel}>
46
- <Message groupStyles={['bottom']} {...options} />
47
- <MessageComposer />
48
- </Channel>
49
- </Chat>
79
+ <MessageListItemProvider
80
+ value={{
81
+ goToMessage: jest.fn(),
82
+ modifiedTheme: {},
83
+ onThreadSelect: jest.fn(),
84
+ setNativeScrollability: jest.fn(),
85
+ }}
86
+ >
87
+ <Chat client={chatClient}>
88
+ {componentOverrides ? (
89
+ <WithComponents overrides={componentOverrides}>
90
+ <Channel channel={channel} {...channelProps}>
91
+ <Message groupStyles={['bottom']} {...options} />
92
+ <MessageComposer />
93
+ </Channel>
94
+ </WithComponents>
95
+ ) : (
96
+ <Channel channel={channel} {...channelProps}>
97
+ <Message groupStyles={['bottom']} {...options} />
98
+ <MessageComposer />
99
+ </Channel>
100
+ )}
101
+ </Chat>
102
+ </MessageListItemProvider>
50
103
  </OverlayProvider>
51
104
  </SafeAreaProvider>,
52
105
  );
@@ -88,4 +141,31 @@ describe('Message', () => {
88
141
  expect(onLongPressMessage).toHaveBeenCalledTimes(1);
89
142
  });
90
143
  });
144
+
145
+ it('teleports a custom overlay target without applying overlay styles to siblings', async () => {
146
+ const message = generateMessage({ user });
147
+ const { getByTestId, getByText } = renderMessage(
148
+ { message },
149
+ {
150
+ messageOverlayTargetId: 'custom-overlay-target',
151
+ },
152
+ {
153
+ MessageItemView: CustomMessageItemView,
154
+ },
155
+ );
156
+
157
+ await waitFor(() => {
158
+ expect(getByTestId('custom-message-item-view')).toBeTruthy();
159
+ expect(getByText('outside:normal')).toBeTruthy();
160
+ expect(getByText('inside:normal')).toBeTruthy();
161
+ });
162
+
163
+ fireEvent(getByTestId('custom-overlay-trigger'), 'longPress');
164
+
165
+ await waitFor(() => {
166
+ expect(getByText('outside:normal')).toBeTruthy();
167
+ expect(getByText('inside:overlay')).toBeTruthy();
168
+ expect(getByTestId('custom-overlay-target-placeholder')).toBeTruthy();
169
+ });
170
+ });
91
171
  });
@@ -0,0 +1,81 @@
1
+ import React, { PropsWithChildren, useCallback, useEffect } from 'react';
2
+ import { View } from 'react-native';
3
+
4
+ import { Portal } from 'react-native-teleport';
5
+
6
+ import {
7
+ MessageOverlayTargetProvider,
8
+ useMessageContext,
9
+ useMessageOverlayRuntimeContext,
10
+ } from '../../contexts/messageContext/MessageContext';
11
+
12
+ export type MessageOverlayWrapperProps = PropsWithChildren<{
13
+ /**
14
+ * Stable identifier for this overlay target. Must match `messageOverlayTargetId`
15
+ * when this subtree should be teleported into the overlay.
16
+ */
17
+ targetId: string;
18
+ /**
19
+ * Optional test id attached to the wrapped target container.
20
+ */
21
+ testID?: string;
22
+ }>;
23
+
24
+ /**
25
+ * Wraps the primary message overlay target so the active message can be teleported
26
+ * into the overlay host while a placeholder preserves its original layout space.
27
+ */
28
+ export const MessageOverlayWrapper = ({
29
+ children,
30
+ targetId,
31
+ testID,
32
+ }: MessageOverlayWrapperProps) => {
33
+ const { registerMessageOverlayTarget, unregisterMessageOverlayTarget } = useMessageContext();
34
+ const { messageOverlayTargetId, overlayActive, overlayTargetRectRef } =
35
+ useMessageOverlayRuntimeContext();
36
+ const isActiveTarget = messageOverlayTargetId === targetId;
37
+ const placeholderLayout = overlayTargetRectRef.current;
38
+
39
+ const handleTargetRef = useCallback(
40
+ (view: View | null) => {
41
+ registerMessageOverlayTarget({
42
+ id: targetId,
43
+ view,
44
+ });
45
+ },
46
+ [registerMessageOverlayTarget, targetId],
47
+ );
48
+
49
+ useEffect(
50
+ () => () => {
51
+ unregisterMessageOverlayTarget(targetId);
52
+ },
53
+ [targetId, unregisterMessageOverlayTarget],
54
+ );
55
+
56
+ if (!isActiveTarget) {
57
+ return children;
58
+ }
59
+
60
+ return (
61
+ <>
62
+ <Portal hostName={overlayActive ? 'message-overlay' : undefined}>
63
+ <View collapsable={false} ref={handleTargetRef} testID={testID}>
64
+ <MessageOverlayTargetProvider value={isActiveTarget}>
65
+ {children}
66
+ </MessageOverlayTargetProvider>
67
+ </View>
68
+ </Portal>
69
+ {overlayActive ? (
70
+ <View
71
+ pointerEvents='none'
72
+ style={{
73
+ height: placeholderLayout?.h ?? 0,
74
+ width: placeholderLayout?.w && placeholderLayout.w > 0 ? placeholderLayout.w : '100%',
75
+ }}
76
+ testID={testID ? `${testID}-placeholder` : 'message-overlay-wrapper-placeholder'}
77
+ />
78
+ ) : null}
79
+ </>
80
+ );
81
+ };
@@ -4,10 +4,12 @@ import { act, cleanup, renderHook } from '@testing-library/react-native';
4
4
 
5
5
  import {
6
6
  MessageContextValue,
7
+ MessageOverlayRuntimeProvider,
7
8
  MessageProvider,
8
9
  } from '../../../../contexts/messageContext/MessageContext';
9
10
  import { generateMessage } from '../../../../mock-builders/generator/message';
10
11
  import { finalizeCloseOverlay, openOverlay, overlayStore } from '../../../../state-store';
12
+ import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from '../../messageOverlayConstants';
11
13
 
12
14
  import { useShouldUseOverlayStyles } from '../useShouldUseOverlayStyles';
13
15
 
@@ -22,6 +24,7 @@ const createMessageContextValue = (overrides: Partial<MessageContextValue>): Mes
22
24
  groupStyles: [],
23
25
  handleAction: jest.fn(),
24
26
  handleToggleReaction: jest.fn(),
27
+ hasAttachmentActions: false,
25
28
  hasReactions: false,
26
29
  images: [],
27
30
  isMessageAIGenerated: jest.fn(),
@@ -29,6 +32,7 @@ const createMessageContextValue = (overrides: Partial<MessageContextValue>): Mes
29
32
  lastGroupMessage: false,
30
33
  members: {},
31
34
  message: generateMessage({ id: 'shared-message-id' }),
35
+ contextMenuAnchorRef: { current: null },
32
36
  messageContentOrder: [],
33
37
  messageHasOnlySingleAttachment: false,
34
38
  messageOverlayId: 'message-overlay-default',
@@ -38,6 +42,7 @@ const createMessageContextValue = (overrides: Partial<MessageContextValue>): Mes
38
42
  onPress: jest.fn(),
39
43
  onPressIn: null,
40
44
  otherAttachments: [],
45
+ registerMessageOverlayTarget: jest.fn(),
41
46
  reactions: [],
42
47
  readBy: false,
43
48
  setQuotedMessage: jest.fn(),
@@ -46,13 +51,23 @@ const createMessageContextValue = (overrides: Partial<MessageContextValue>): Mes
46
51
  showReactionsOverlay: jest.fn(),
47
52
  showMessageStatus: false,
48
53
  threadList: false,
54
+ unregisterMessageOverlayTarget: jest.fn(),
49
55
  videos: [],
50
56
  ...overrides,
51
57
  }) as MessageContextValue;
52
58
 
53
- const createWrapper = (value: MessageContextValue) => {
59
+ const createWrapper = (
60
+ value: MessageContextValue,
61
+ runtimeValue = {
62
+ overlayTargetRectRef: { current: undefined },
63
+ messageOverlayTargetId: DEFAULT_MESSAGE_OVERLAY_TARGET_ID,
64
+ overlayActive: false,
65
+ },
66
+ ) => {
54
67
  const Wrapper = ({ children }: PropsWithChildren) => (
55
- <MessageProvider value={value}>{children}</MessageProvider>
68
+ <MessageProvider value={value}>
69
+ <MessageOverlayRuntimeProvider value={runtimeValue}>{children}</MessageOverlayRuntimeProvider>
70
+ </MessageProvider>
56
71
  );
57
72
 
58
73
  return Wrapper;
@@ -47,6 +47,8 @@ export const useCreateMessageContext = ({
47
47
  onThreadSelect,
48
48
  otherAttachments,
49
49
  preventPress,
50
+ registerMessageOverlayTarget,
51
+ unregisterMessageOverlayTarget,
50
52
  reactions,
51
53
  readBy,
52
54
  showAvatar,
@@ -102,6 +104,8 @@ export const useCreateMessageContext = ({
102
104
  onThreadSelect,
103
105
  otherAttachments,
104
106
  preventPress,
107
+ registerMessageOverlayTarget,
108
+ unregisterMessageOverlayTarget,
105
109
  reactions,
106
110
  readBy,
107
111
  setQuotedMessage,
@@ -134,6 +138,7 @@ export const useCreateMessageContext = ({
134
138
  showMessageStatus,
135
139
  threadList,
136
140
  preventPress,
141
+ unregisterMessageOverlayTarget,
137
142
  ],
138
143
  );
139
144
 
@@ -1,9 +1,22 @@
1
- import { useMessageContext } from '../../../contexts';
1
+ import {
2
+ useMessageContext,
3
+ useMessageOverlayRuntimeContext,
4
+ useMessageOverlayTargetContext,
5
+ } from '../../../contexts';
2
6
  import { useIsOverlayActive } from '../../../state-store';
7
+ import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from '../messageOverlayConstants';
3
8
 
4
9
  export const useShouldUseOverlayStyles = () => {
5
10
  const { messageOverlayId } = useMessageContext();
11
+ const { messageOverlayTargetId } = useMessageOverlayRuntimeContext();
12
+ const isWithinMessageOverlayTarget = useMessageOverlayTargetContext();
6
13
  const { active, closing } = useIsOverlayActive(messageOverlayId);
7
14
 
8
- return active && !closing;
15
+ if (!active || closing) {
16
+ return false;
17
+ }
18
+
19
+ return messageOverlayTargetId === DEFAULT_MESSAGE_OVERLAY_TARGET_ID
20
+ ? true
21
+ : isWithinMessageOverlayTarget;
9
22
  };
@@ -0,0 +1 @@
1
+ export const DEFAULT_MESSAGE_OVERLAY_TARGET_ID = '@stream-io/message-root';
@@ -368,7 +368,9 @@ exports[`Thread should match thread snapshot 1`] = `
368
368
  >
369
369
  <View />
370
370
  <View>
371
- <View>
371
+ <View
372
+ collapsable={false}
373
+ >
372
374
  <View
373
375
  collapsable={false}
374
376
  hitSlop={
@@ -707,7 +709,9 @@ exports[`Thread should match thread snapshot 1`] = `
707
709
  >
708
710
  <View />
709
711
  <View>
710
- <View>
712
+ <View
713
+ collapsable={false}
714
+ >
711
715
  <View
712
716
  collapsable={false}
713
717
  hitSlop={
@@ -1079,7 +1083,9 @@ exports[`Thread should match thread snapshot 1`] = `
1079
1083
  >
1080
1084
  <View />
1081
1085
  <View>
1082
- <View>
1086
+ <View
1087
+ collapsable={false}
1088
+ >
1083
1089
  <View
1084
1090
  collapsable={false}
1085
1091
  hitSlop={
@@ -1408,7 +1414,9 @@ exports[`Thread should match thread snapshot 1`] = `
1408
1414
  >
1409
1415
  <View />
1410
1416
  <View>
1411
- <View>
1417
+ <View
1418
+ collapsable={false}
1419
+ >
1412
1420
  <View
1413
1421
  collapsable={false}
1414
1422
  hitSlop={
@@ -87,6 +87,7 @@ export * from './Message/hooks/useStreamingMessage';
87
87
  export * from './Message/hooks/useMessageDeliveryData';
88
88
  export * from './Message/hooks/useMessageReadData';
89
89
  export * from './Message/Message';
90
+ export * from './Message/MessageOverlayWrapper';
90
91
  export * from './Message/MessageItemView/MessageAuthor';
91
92
  export * from './Message/MessageItemView/MessageBounce';
92
93
  export * from './Message/MessageItemView/MessageBlocked';
@@ -163,7 +164,6 @@ export * from './MessageList/hooks/useMessageGroupStyles';
163
164
 
164
165
  export * from './MessageMenu/MessageActionList';
165
166
  export * from './MessageMenu/MessageActionListItem';
166
- export * from './MessageMenu/MessageMenu';
167
167
  export * from './MessageMenu/MessageUserReactions';
168
168
  export * from './MessageMenu/MessageUserReactionsAvatar';
169
169
  export * from './MessageMenu/MessageReactionPicker';
@@ -123,7 +123,6 @@ import { TypingIndicatorContainer } from '../../components/MessageList/TypingInd
123
123
  import { UnreadMessagesNotification } from '../../components/MessageList/UnreadMessagesNotification';
124
124
  import { MessageActionList } from '../../components/MessageMenu/MessageActionList';
125
125
  import { MessageActionListItem } from '../../components/MessageMenu/MessageActionListItem';
126
- import { MessageMenu } from '../../components/MessageMenu/MessageMenu';
127
126
  import { MessageReactionPicker } from '../../components/MessageMenu/MessageReactionPicker';
128
127
  import { MessageUserReactions } from '../../components/MessageMenu/MessageUserReactions';
129
128
  import { MessageUserReactionsAvatar } from '../../components/MessageMenu/MessageUserReactionsAvatar';
@@ -226,7 +225,6 @@ const components = {
226
225
  MessageInputTrailingView,
227
226
  MessageItemView,
228
227
  MessageList,
229
- MessageMenu,
230
228
  MessagePinnedHeader,
231
229
  MessageReactionPicker,
232
230
  MessageReminderHeader,