diff --git a/examples/SampleApp/ios/Podfile.lock b/examples/SampleApp/ios/Podfile.lock index beaeacb61d..9deda9f191 100644 --- a/examples/SampleApp/ios/Podfile.lock +++ b/examples/SampleApp/ios/Podfile.lock @@ -3327,7 +3327,7 @@ PODS: - ReactCommon/turbomodule/core - SocketRocket - Yoga - - Teleport (0.5.4): + - Teleport (1.1.2): - boost - DoubleConversion - fast_float @@ -3354,9 +3354,9 @@ PODS: - ReactCommon/turbomodule/bridging - ReactCommon/turbomodule/core - SocketRocket - - Teleport/common (= 0.5.4) + - Teleport/common (= 1.1.2) - Yoga - - Teleport/common (0.5.4): + - Teleport/common (1.1.2): - boost - DoubleConversion - fast_float @@ -3827,7 +3827,7 @@ SPEC CHECKSUMS: RNGestureHandler: 6bc8f2f56c8a68f3380cd159f3a1ae06defcfabb RNNotifee: 5e3b271e8ea7456a36eec994085543c9adca9168 RNReactNativeHapticFeedback: 5f1542065f0b24c9252bd8cf3e83bc9c548182e4 - RNReanimated: 0e779d4d219b01331bf5ad620d30c5b993d18856 + RNReanimated: a1e0ce339c1d8f9164b7499920d8787d6a7f7a23 RNScreens: e902eba58a27d3ad399a495d578e8aba3ea0f490 RNShare: c0f25f3d0ec275239c35cadbc98c94053118bee7 RNSVG: b1cb00d54dbc3066a3e98732e5418c8361335124 @@ -3836,7 +3836,7 @@ SPEC CHECKSUMS: SDWebImageWebPCoder: 908b83b6adda48effe7667cd2b7f78c897e5111d SocketRocket: d4aabe649be1e368d1318fdf28a022d714d65748 stream-chat-react-native: d15df89b47c1a08bc7db90c316d34b8ac4e13900 - Teleport: c5c5d9ac843d3024fd5776a7fcba22d37080f86b + Teleport: ed828b19e62ca8b9ec101d991bf0594b1c1c8812 Yoga: ff16d80456ce825ffc9400eeccc645a0dfcccdf5 PODFILE CHECKSUM: 4f662370295f8f9cee909f1a4c59a614999a209d diff --git a/examples/SampleApp/package.json b/examples/SampleApp/package.json index 5cacc802f0..228b0e2334 100644 --- a/examples/SampleApp/package.json +++ b/examples/SampleApp/package.json @@ -66,7 +66,7 @@ "react-native-screens": "^4.24.0", "react-native-share": "^12.0.11", "react-native-svg": "^15.15.4", - "react-native-teleport": "^0.5.4", + "react-native-teleport": "^1.1.2", "react-native-video": "^6.16.1", "react-native-worklets": "^0.8.1", "stream-chat-react-native": "link:../../package/native-package", diff --git a/examples/SampleApp/yarn.lock b/examples/SampleApp/yarn.lock index 70af5c4a61..5793eccca9 100644 --- a/examples/SampleApp/yarn.lock +++ b/examples/SampleApp/yarn.lock @@ -7674,10 +7674,10 @@ react-native-svg@^15.15.4: css-tree "^1.1.3" warn-once "0.1.1" -react-native-teleport@^0.5.4: - version "0.5.4" - resolved "https://registry.yarnpkg.com/react-native-teleport/-/react-native-teleport-0.5.4.tgz#6af16e3984ee0fc002e5d6c6dae0ad40f22699c2" - integrity sha512-kiNbB27lJHAO7DdG7LlPi6DaFnYusVQV9rqp0q5WoRpnqKNckIvzXULox6QpZBstaTF/jkO+xmlkU+pnQqKSAQ== +react-native-teleport@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/react-native-teleport/-/react-native-teleport-1.1.2.tgz#23deea2a34f6b1bb378e0305d44deeb93d51d490" + integrity sha512-64dcEkxlVKzxIts2FAVhzI2tDExcD23T13c2yDC/E+1dA1vP9UlDwPYUEkHvnoTOFtMDGrKLH03RJahIWfQC1g== react-native-url-polyfill@^2.0.0: version "2.0.0" diff --git a/package/src/components/Channel/Channel.tsx b/package/src/components/Channel/Channel.tsx index 2a2893b67a..d66cdf3d5a 100644 --- a/package/src/components/Channel/Channel.tsx +++ b/package/src/components/Channel/Channel.tsx @@ -261,6 +261,7 @@ export type ChannelPropsWithContext = Pick & | 'markdownRules' | 'messageActions' | 'messageContentOrder' + | 'messageOverlayTargetId' | 'messageTextNumberOfLines' | 'messageSwipeToReplyHitSlop' | 'myMessageTheme' @@ -464,6 +465,7 @@ const ChannelWithContext = (props: PropsWithChildren) = 'text', 'location', ], + messageOverlayTargetId, messageInputFloating = false, messageId, messageSwipeToReplyHitSlop, @@ -1665,6 +1667,7 @@ const ChannelWithContext = (props: PropsWithChildren) = markdownRules, messageActions, messageContentOrder, + messageOverlayTargetId, messageSwipeToReplyHitSlop, messageTextNumberOfLines, myMessageTheme, diff --git a/package/src/components/Channel/hooks/useCreateMessagesContext.ts b/package/src/components/Channel/hooks/useCreateMessagesContext.ts index e90b13ef3b..a237709a95 100644 --- a/package/src/components/Channel/hooks/useCreateMessagesContext.ts +++ b/package/src/components/Channel/hooks/useCreateMessagesContext.ts @@ -37,6 +37,7 @@ export const useCreateMessagesContext = ({ markdownRules, messageActions, messageContentOrder, + messageOverlayTargetId, messageSwipeToReplyHitSlop, messageTextNumberOfLines, myMessageTheme, @@ -100,6 +101,7 @@ export const useCreateMessagesContext = ({ markdownRules, messageActions, messageContentOrder, + messageOverlayTargetId, messageSwipeToReplyHitSlop, messageTextNumberOfLines, myMessageTheme, @@ -127,6 +129,7 @@ export const useCreateMessagesContext = ({ initialScrollToFirstUnreadMessage, markdownRulesLength, messageContentOrderValue, + messageOverlayTargetId, supportedReactionsLength, myMessageTheme, targetedMessage, diff --git a/package/src/components/Message/Message.tsx b/package/src/components/Message/Message.tsx index 327fff2648..316979be3f 100644 --- a/package/src/components/Message/Message.tsx +++ b/package/src/components/Message/Message.tsx @@ -19,6 +19,8 @@ import { useMessageActions } from './hooks/useMessageActions'; import { useMessageDeliveredData } from './hooks/useMessageDeliveryData'; import { useMessageReadData } from './hooks/useMessageReadData'; import { useProcessReactions } from './hooks/useProcessReactions'; +import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from './messageOverlayConstants'; +import { MessageOverlayWrapper } from './MessageOverlayWrapper'; import { measureInWindow } from './utils/measureInWindow'; import { messageActions as defaultMessageActions } from './utils/messageActions'; @@ -36,7 +38,11 @@ import { MessageComposerAPIContextValue, useMessageComposerAPIContext, } from '../../contexts/messageComposerContext/MessageComposerAPIContext'; -import { MessageContextValue, MessageProvider } from '../../contexts/messageContext/MessageContext'; +import { + MessageContextValue, + MessageOverlayRuntimeProvider, + MessageProvider, +} from '../../contexts/messageContext/MessageContext'; import { useMessageListItemContext } from '../../contexts/messageListItemContext/MessageListItemContext'; import { MessagesContextValue, @@ -207,6 +213,7 @@ export type MessagePropsWithContext = Pick< | 'handleBlockUser' | 'isAttachmentEqual' | 'messageActions' + | 'messageOverlayTargetId' | 'messageContentOrder' | 'onLongPressMessage' | 'onPressInMessage' @@ -278,6 +285,7 @@ const MessageWithContext = (props: MessagePropsWithContext) => { members, message, messageActions: messageActionsProp = defaultMessageActions, + messageOverlayTargetId = DEFAULT_MESSAGE_OVERLAY_TARGET_ID, messageContentOrder: messageContentOrderProp, messagesContext, onLongPressMessage: onLongPressMessageProp, @@ -323,11 +331,28 @@ const MessageWithContext = (props: MessagePropsWithContext) => { const rectRef = useRef(undefined); const bubbleRect = useRef(undefined); const contextMenuAnchorRef = useRef(null); + const messageOverlayTargetsRef = useRef>({}); + const registerMessageOverlayTarget = useStableCallback( + ({ id, view }: { id: string; view: View | null }) => { + messageOverlayTargetsRef.current[id] = view; + }, + ); + const unregisterMessageOverlayTarget = useStableCallback((id: string) => { + delete messageOverlayTargetsRef.current[id]; + }); const showMessageOverlay = useStableCallback(async () => { dismissKeyboard(); try { - const layout = await measureInWindow(messageWrapperRef, insets); + const activeTargetView = messageOverlayTargetsRef.current[messageOverlayTargetId]; + + if (!activeTargetView) { + throw new Error( + `No message overlay target is registered for target id "${messageOverlayTargetId}".`, + ); + } + + const layout = await measureInWindow({ current: activeTargetView }, insets); const bubbleLayout = await measureInWindow(contextMenuAnchorRef, insets).catch(() => layout); rectRef.current = layout; @@ -655,8 +680,6 @@ const MessageWithContext = (props: MessagePropsWithContext) => { unpinMessage: handleTogglePinMessage, }; - const messageWrapperRef = useRef(null); - const onLongPress = () => { setNativeScrollability(false); if (hasAttachmentActions || isBlockedMessage(message) || !enableLongPress) { @@ -771,6 +794,8 @@ const MessageWithContext = (props: MessagePropsWithContext) => { onThreadSelect, otherAttachments: attachments.other, preventPress: overlayActive ? true : preventPress, + registerMessageOverlayTarget, + unregisterMessageOverlayTarget, reactions, readBy, setQuotedMessage, @@ -781,6 +806,14 @@ const MessageWithContext = (props: MessagePropsWithContext) => { threadList, videos: attachments.videos, }); + const messageOverlayRuntimeContext = useMemo( + () => ({ + overlayTargetRectRef: rectRef, + messageOverlayTargetId, + overlayActive, + }), + [messageOverlayTargetId, overlayActive], + ); const prevActive = useRef(overlayActive); @@ -817,82 +850,74 @@ const MessageWithContext = (props: MessagePropsWithContext) => { return ( - - {overlayActive && rect ? ( - - ) : null} - {/*TODO: V9: Find a way to separate these in a dedicated file*/} - - {overlayActive && rect && overlayItemsAnchorRect ? ( - { - const { width: w, height: h } = e.nativeEvent.layout; - - setOverlayTopH({ - h, - w, - x: - overlayItemAlignment === 'right' - ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w - : overlayItemsAnchorRect.x, - y: rect.y - h, - }); - }} - > - - - ) : null} - - - + + + {/*TODO: V9: Find a way to separate these in a dedicated file*/} + + {overlayActive && rect && overlayItemsAnchorRect ? ( + { + const { width: w, height: h } = e.nativeEvent.layout; + + setOverlayTopH({ + h, + w, + x: + overlayItemAlignment === 'right' + ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w + : overlayItemsAnchorRect.x, + y: rect.y - h, + }); + }} + > + + + ) : null} + + - - - {showMessageReactions ? ( - setShowMessageReactions(false)} - visible={showMessageReactions} - height={424} - > - - - ) : null} - - {overlayActive && rect && overlayItemsAnchorRect ? ( - { - const { width: w, height: h } = e.nativeEvent.layout; - setOverlayBottomH({ - h, - w, - x: - overlayItemAlignment === 'right' - ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w - : overlayItemsAnchorRect.x, - y: rect.y + rect.h, - }); - }} + + {showMessageReactions ? ( + setShowMessageReactions(false)} + visible={showMessageReactions} + height={424} > - - + + ) : null} - - {isBounceDialogOpen ? ( - - ) : null} - + + {overlayActive && rect && overlayItemsAnchorRect ? ( + { + const { width: w, height: h } = e.nativeEvent.layout; + setOverlayBottomH({ + h, + w, + x: + overlayItemAlignment === 'right' + ? overlayItemsAnchorRect.x + overlayItemsAnchorRect.w - w + : overlayItemsAnchorRect.x, + y: rect.y + rect.h, + }); + }} + > + + + ) : null} + + {isBounceDialogOpen ? ( + + ) : null} + + ); }; @@ -905,6 +930,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit groupStyles: prevGroupStyles, isAttachmentEqual, isTargetedMessage: prevIsTargetedMessage, + messageOverlayTargetId: prevMessageOverlayTargetId, members: prevMembers, message: prevMessage, messagesContext: prevMessagesContext, @@ -918,6 +944,7 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit goToMessage: nextGoToMessage, groupStyles: nextGroupStyles, isTargetedMessage: nextIsTargetedMessage, + messageOverlayTargetId: nextMessageOverlayTargetId, members: nextMembers, message: nextMessage, messagesContext: nextMessagesContext, @@ -958,6 +985,11 @@ const areEqual = (prevProps: MessagePropsWithContext, nextProps: MessagePropsWit return false; } + const messageOverlayTargetIdEqual = prevMessageOverlayTargetId === nextMessageOverlayTargetId; + if (!messageOverlayTargetIdEqual) { + return false; + } + const messageEqual = checkMessageEquality(prevMessage, nextMessage); if (!messageEqual) { diff --git a/package/src/components/Message/MessageItemView/__tests__/Message.test.js b/package/src/components/Message/MessageItemView/__tests__/Message.test.js index ebafc968c9..9a87d7f7b8 100644 --- a/package/src/components/Message/MessageItemView/__tests__/Message.test.js +++ b/package/src/components/Message/MessageItemView/__tests__/Message.test.js @@ -1,9 +1,13 @@ import React from 'react'; +import { Pressable, Text, View } from 'react-native'; import { SafeAreaProvider } from 'react-native-safe-area-context'; import { cleanup, fireEvent, render, waitFor } from '@testing-library/react-native'; +import { WithComponents } from '../../../../contexts/componentsContext/ComponentsContext'; +import { useMessageContext } from '../../../../contexts/messageContext/MessageContext'; +import { MessageListItemProvider } from '../../../../contexts/messageListItemContext/MessageListItemContext'; import { OverlayProvider } from '../../../../contexts/overlayContext/OverlayProvider'; import { getOrCreateChannelApi } from '../../../../mock-builders/api/getOrCreateChannel'; @@ -16,7 +20,38 @@ import { getTestClientWithUser } from '../../../../mock-builders/mock'; import { Channel } from '../../../Channel/Channel'; import { Chat } from '../../../Chat/Chat'; import { MessageComposer } from '../../../MessageInput/MessageComposer'; +import { useShouldUseOverlayStyles } from '../../hooks/useShouldUseOverlayStyles'; import { Message } from '../../Message'; +import { MessageOverlayWrapper } from '../../MessageOverlayWrapper'; + +const OverlayStateText = ({ label }) => { + const shouldUseOverlayStyles = useShouldUseOverlayStyles(); + + return {`${label}:${shouldUseOverlayStyles ? 'overlay' : 'normal'}`}; +}; + +const OverlayTrigger = () => { + const { onLongPress } = useMessageContext(); + + return ( + onLongPress({ emitter: 'message' })} + testID='custom-overlay-trigger' + > + Open overlay + + ); +}; + +const CustomMessageItemView = () => ( + + + + + + + +); describe('Message', () => { let channel; @@ -37,16 +72,34 @@ describe('Message', () => { useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]); channel = chatClient.channel('messaging', mockedChannel.id); - renderMessage = (options) => + renderMessage = (options, channelProps, componentOverrides) => render( - - - - - - + + + {componentOverrides ? ( + + + + + + + ) : ( + + + + + )} + + , ); @@ -88,4 +141,31 @@ describe('Message', () => { expect(onLongPressMessage).toHaveBeenCalledTimes(1); }); }); + + it('teleports a custom overlay target without applying overlay styles to siblings', async () => { + const message = generateMessage({ user }); + const { getByTestId, getByText } = renderMessage( + { message }, + { + messageOverlayTargetId: 'custom-overlay-target', + }, + { + MessageItemView: CustomMessageItemView, + }, + ); + + await waitFor(() => { + expect(getByTestId('custom-message-item-view')).toBeTruthy(); + expect(getByText('outside:normal')).toBeTruthy(); + expect(getByText('inside:normal')).toBeTruthy(); + }); + + fireEvent(getByTestId('custom-overlay-trigger'), 'longPress'); + + await waitFor(() => { + expect(getByText('outside:normal')).toBeTruthy(); + expect(getByText('inside:overlay')).toBeTruthy(); + expect(getByTestId('custom-overlay-target-placeholder')).toBeTruthy(); + }); + }); }); diff --git a/package/src/components/Message/MessageOverlayWrapper.tsx b/package/src/components/Message/MessageOverlayWrapper.tsx new file mode 100644 index 0000000000..a27f2eb7af --- /dev/null +++ b/package/src/components/Message/MessageOverlayWrapper.tsx @@ -0,0 +1,81 @@ +import React, { PropsWithChildren, useCallback, useEffect } from 'react'; +import { View } from 'react-native'; + +import { Portal } from 'react-native-teleport'; + +import { + MessageOverlayTargetProvider, + useMessageContext, + useMessageOverlayRuntimeContext, +} from '../../contexts/messageContext/MessageContext'; + +export type MessageOverlayWrapperProps = PropsWithChildren<{ + /** + * Stable identifier for this overlay target. Must match `messageOverlayTargetId` + * when this subtree should be teleported into the overlay. + */ + targetId: string; + /** + * Optional test id attached to the wrapped target container. + */ + testID?: string; +}>; + +/** + * Wraps the primary message overlay target so the active message can be teleported + * into the overlay host while a placeholder preserves its original layout space. + */ +export const MessageOverlayWrapper = ({ + children, + targetId, + testID, +}: MessageOverlayWrapperProps) => { + const { registerMessageOverlayTarget, unregisterMessageOverlayTarget } = useMessageContext(); + const { messageOverlayTargetId, overlayActive, overlayTargetRectRef } = + useMessageOverlayRuntimeContext(); + const isActiveTarget = messageOverlayTargetId === targetId; + const placeholderLayout = overlayTargetRectRef.current; + + const handleTargetRef = useCallback( + (view: View | null) => { + registerMessageOverlayTarget({ + id: targetId, + view, + }); + }, + [registerMessageOverlayTarget, targetId], + ); + + useEffect( + () => () => { + unregisterMessageOverlayTarget(targetId); + }, + [targetId, unregisterMessageOverlayTarget], + ); + + if (!isActiveTarget) { + return children; + } + + return ( + <> + + + + {children} + + + + {overlayActive ? ( + 0 ? placeholderLayout.w : '100%', + }} + testID={testID ? `${testID}-placeholder` : 'message-overlay-wrapper-placeholder'} + /> + ) : null} + + ); +}; diff --git a/package/src/components/Message/hooks/__tests__/useShouldUseOverlayStyles.test.tsx b/package/src/components/Message/hooks/__tests__/useShouldUseOverlayStyles.test.tsx index 7ae7133225..a8173e45f9 100644 --- a/package/src/components/Message/hooks/__tests__/useShouldUseOverlayStyles.test.tsx +++ b/package/src/components/Message/hooks/__tests__/useShouldUseOverlayStyles.test.tsx @@ -4,10 +4,12 @@ import { act, cleanup, renderHook } from '@testing-library/react-native'; import { MessageContextValue, + MessageOverlayRuntimeProvider, MessageProvider, } from '../../../../contexts/messageContext/MessageContext'; import { generateMessage } from '../../../../mock-builders/generator/message'; import { finalizeCloseOverlay, openOverlay, overlayStore } from '../../../../state-store'; +import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from '../../messageOverlayConstants'; import { useShouldUseOverlayStyles } from '../useShouldUseOverlayStyles'; @@ -22,6 +24,7 @@ const createMessageContextValue = (overrides: Partial): Mes groupStyles: [], handleAction: jest.fn(), handleToggleReaction: jest.fn(), + hasAttachmentActions: false, hasReactions: false, images: [], isMessageAIGenerated: jest.fn(), @@ -29,6 +32,7 @@ const createMessageContextValue = (overrides: Partial): Mes lastGroupMessage: false, members: {}, message: generateMessage({ id: 'shared-message-id' }), + contextMenuAnchorRef: { current: null }, messageContentOrder: [], messageHasOnlySingleAttachment: false, messageOverlayId: 'message-overlay-default', @@ -38,6 +42,7 @@ const createMessageContextValue = (overrides: Partial): Mes onPress: jest.fn(), onPressIn: null, otherAttachments: [], + registerMessageOverlayTarget: jest.fn(), reactions: [], readBy: false, setQuotedMessage: jest.fn(), @@ -46,13 +51,23 @@ const createMessageContextValue = (overrides: Partial): Mes showReactionsOverlay: jest.fn(), showMessageStatus: false, threadList: false, + unregisterMessageOverlayTarget: jest.fn(), videos: [], ...overrides, }) as MessageContextValue; -const createWrapper = (value: MessageContextValue) => { +const createWrapper = ( + value: MessageContextValue, + runtimeValue = { + overlayTargetRectRef: { current: undefined }, + messageOverlayTargetId: DEFAULT_MESSAGE_OVERLAY_TARGET_ID, + overlayActive: false, + }, +) => { const Wrapper = ({ children }: PropsWithChildren) => ( - {children} + + {children} + ); return Wrapper; diff --git a/package/src/components/Message/hooks/useCreateMessageContext.ts b/package/src/components/Message/hooks/useCreateMessageContext.ts index 55e176e95d..3c2141699e 100644 --- a/package/src/components/Message/hooks/useCreateMessageContext.ts +++ b/package/src/components/Message/hooks/useCreateMessageContext.ts @@ -47,6 +47,8 @@ export const useCreateMessageContext = ({ onThreadSelect, otherAttachments, preventPress, + registerMessageOverlayTarget, + unregisterMessageOverlayTarget, reactions, readBy, showAvatar, @@ -102,6 +104,8 @@ export const useCreateMessageContext = ({ onThreadSelect, otherAttachments, preventPress, + registerMessageOverlayTarget, + unregisterMessageOverlayTarget, reactions, readBy, setQuotedMessage, @@ -134,6 +138,7 @@ export const useCreateMessageContext = ({ showMessageStatus, threadList, preventPress, + unregisterMessageOverlayTarget, ], ); diff --git a/package/src/components/Message/hooks/useShouldUseOverlayStyles.ts b/package/src/components/Message/hooks/useShouldUseOverlayStyles.ts index 9baa0f2046..e030113325 100644 --- a/package/src/components/Message/hooks/useShouldUseOverlayStyles.ts +++ b/package/src/components/Message/hooks/useShouldUseOverlayStyles.ts @@ -1,9 +1,22 @@ -import { useMessageContext } from '../../../contexts'; +import { + useMessageContext, + useMessageOverlayRuntimeContext, + useMessageOverlayTargetContext, +} from '../../../contexts'; import { useIsOverlayActive } from '../../../state-store'; +import { DEFAULT_MESSAGE_OVERLAY_TARGET_ID } from '../messageOverlayConstants'; export const useShouldUseOverlayStyles = () => { const { messageOverlayId } = useMessageContext(); + const { messageOverlayTargetId } = useMessageOverlayRuntimeContext(); + const isWithinMessageOverlayTarget = useMessageOverlayTargetContext(); const { active, closing } = useIsOverlayActive(messageOverlayId); - return active && !closing; + if (!active || closing) { + return false; + } + + return messageOverlayTargetId === DEFAULT_MESSAGE_OVERLAY_TARGET_ID + ? true + : isWithinMessageOverlayTarget; }; diff --git a/package/src/components/Message/messageOverlayConstants.ts b/package/src/components/Message/messageOverlayConstants.ts new file mode 100644 index 0000000000..3080d4cd12 --- /dev/null +++ b/package/src/components/Message/messageOverlayConstants.ts @@ -0,0 +1 @@ +export const DEFAULT_MESSAGE_OVERLAY_TARGET_ID = '@stream-io/message-root'; diff --git a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap index 0987d6c577..049ff2af71 100644 --- a/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap +++ b/package/src/components/Thread/__tests__/__snapshots__/Thread.test.js.snap @@ -368,7 +368,9 @@ exports[`Thread should match thread snapshot 1`] = ` > - + - + - + - + void) | null; /** The images attached to a message */ otherAttachments: Attachment[]; + /** + * Registers the subtree that should be measured and portaled into the message overlay. + * Custom message renderers typically interact with this via `MessageOverlayWrapper`. + */ + registerMessageOverlayTarget: (params: { id: string; view: View | null }) => void; + unregisterMessageOverlayTarget: (id: string) => void; reactions: ReactionSummary[]; /** Read count of the message */ readBy: number | boolean; @@ -164,3 +172,39 @@ export const useMessageContext = () => { return contextValue; }; + +type MessageOverlayRuntimeContextValue = { + overlayTargetRectRef: { current: Rect }; + messageOverlayTargetId: string; + overlayActive: boolean; +}; + +const MessageOverlayRuntimeContext = React.createContext({ + overlayTargetRectRef: { current: undefined }, + messageOverlayTargetId: DEFAULT_MESSAGE_OVERLAY_TARGET_ID, + overlayActive: false, +}); + +export const MessageOverlayRuntimeProvider = ({ + children, + value, +}: PropsWithChildren<{ value: MessageOverlayRuntimeContextValue }>) => ( + + {children} + +); + +export const useMessageOverlayRuntimeContext = () => useContext(MessageOverlayRuntimeContext); + +const MessageOverlayTargetContext = React.createContext(false); + +export const MessageOverlayTargetProvider = ({ + children, + value, +}: PropsWithChildren<{ value: boolean }>) => ( + + {children} + +); + +export const useMessageOverlayTargetContext = () => useContext(MessageOverlayTargetContext); diff --git a/package/src/contexts/messagesContext/MessagesContext.tsx b/package/src/contexts/messagesContext/MessagesContext.tsx index badfc58d94..b4846b074b 100644 --- a/package/src/contexts/messagesContext/MessagesContext.tsx +++ b/package/src/contexts/messagesContext/MessagesContext.tsx @@ -75,6 +75,11 @@ export type MessagesContextValue = Pick Promise; /** * Override the api request for retry message functionality.