diff --git a/native/chat/message-context.react.js b/native/chat/message-context.react.js new file mode 100644 --- /dev/null +++ b/native/chat/message-context.react.js @@ -0,0 +1,15 @@ +// @flow + +import * as React from 'react'; + +export type MessageContextType = { + +messageID: string, +}; + +const MessageContext: React.Context = React.createContext( + { + messageID: '', + }, +); + +export { MessageContext }; diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -23,6 +23,7 @@ import type { ChatNavigationProp } from './chat.react'; import ComposedMessage from './composed-message.react'; import { InnerTextMessage } from './inner-text-message.react'; +import { MessageContext } from './message-context.react'; import textMessageSendFailed from './text-message-send-failed'; import { textMessageTooltipHeight } from './text-message-tooltip-modal.react'; import { getMessageTooltipKey } from './utils'; @@ -142,6 +143,7 @@ message, props: { verticalBounds, linkIsBlockingPresses }, } = this; + if (!message || !verticalBounds || linkIsBlockingPresses) { return; } @@ -199,24 +201,50 @@ const ConnectedTextMessage: React.ComponentType = React.memo( function ConnectedTextMessage(props: BaseProps) { const overlayContext = React.useContext(OverlayContext); + const markdownContext = React.useContext(MarkdownContext); + invariant(markdownContext, 'markdown context should be set'); + + const { + linkModalActive, + linkPressActive, + clearMessageData, + } = markdownContext; + + const { id } = props.item.messageInfo; + invariant(id, 'message ID should exist'); + + // We check if there is an ID in the respective + // objects - if not, we default to false. + // The likely situation where the former statement + // evaluates to null is when the thread is opened + // for the first time. + const linkIsBlockingPresses = + (linkModalActive[id] || linkPressActive[id]) ?? false; - const [linkModalActive, setLinkModalActive] = React.useState(false); - const [linkPressActive, setLinkPressActive] = React.useState(false); - const markdownContext = React.useMemo( - () => ({ - setLinkModalActive, - setLinkPressActive, - }), - [setLinkModalActive, setLinkPressActive], - ); const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( props.item.threadInfo, props.item.messageInfo, ); - const linkIsBlockingPresses = linkModalActive || linkPressActive; + // We use a MessageContext to allow MarkdownLink + // (and MarkdownSpoiler soon) to access + // the messageID so it is 'self-aware' + const contextValue = React.useMemo( + () => ({ + messageID: id, + }), + [id], + ); + + React.useEffect(() => { + return () => { + // Anything in here is fired on component unmount. + clearMessageData(); + }; + }, [clearMessageData]); + return ( - + - + ); }, ); diff --git a/native/markdown/markdown-context-provider.react.js b/native/markdown/markdown-context-provider.react.js new file mode 100644 --- /dev/null +++ b/native/markdown/markdown-context-provider.react.js @@ -0,0 +1,44 @@ +// @flow + +import * as React from 'react'; + +import { MarkdownContext } from './markdown-context.js'; + +type Props = { + +children: React.Node, +}; + +function MarkdownContextProvider(props: Props): React.Node { + const [linkModalActive, setLinkModalActive] = React.useState({}); + const [linkPressActive, setLinkPressActive] = React.useState({}); + + const clearMessageData = React.useCallback(() => { + setLinkModalActive({}); + setLinkPressActive({}); + }, []); + + const contextValue = React.useMemo( + () => ({ + setLinkModalActive, + linkModalActive, + setLinkPressActive, + linkPressActive, + clearMessageData, + }), + [ + setLinkModalActive, + linkModalActive, + setLinkPressActive, + linkPressActive, + clearMessageData, + ], + ); + + return ( + + {props.children} + + ); +} + +export default MarkdownContextProvider; diff --git a/native/markdown/markdown-context.js b/native/markdown/markdown-context.js --- a/native/markdown/markdown-context.js +++ b/native/markdown/markdown-context.js @@ -2,9 +2,14 @@ import * as React from 'react'; +import type { SetState } from 'lib/types/hook-types'; + export type MarkdownContextType = { - +setLinkModalActive: boolean => void, - +setLinkPressActive: boolean => void, + +setLinkModalActive: SetState, + +linkModalActive: Object, + +setLinkPressActive: SetState, + +linkPressActive: Object, + +clearMessageData: () => void, }; const MarkdownContext: React.Context = React.createContext( diff --git a/native/markdown/markdown-link.react.js b/native/markdown/markdown-link.react.js --- a/native/markdown/markdown-link.react.js +++ b/native/markdown/markdown-link.react.js @@ -5,16 +5,19 @@ import { normalizeURL } from 'lib/utils/url-utils'; +import { MessageContext } from '../chat/message-context.react'; import { MarkdownContext, type MarkdownContextType } from './markdown-context'; function useDisplayLinkPrompt( inputURL: string, markdownContext: ?MarkdownContextType, + messageID: string, ) { const setLinkModalActive = markdownContext?.setLinkModalActive; + const linkModalActive = markdownContext?.linkModalActive; const onDismiss = React.useCallback(() => { - setLinkModalActive?.(false); - }, [setLinkModalActive]); + setLinkModalActive?.({ ...linkModalActive, [messageID]: false }); + }, [setLinkModalActive, linkModalActive, messageID]); const url = normalizeURL(inputURL); const onConfirm = React.useCallback(() => { @@ -27,7 +30,8 @@ displayURL += '…'; } return React.useCallback(() => { - setLinkModalActive && setLinkModalActive(true); + setLinkModalActive && + setLinkModalActive({ ...linkModalActive, [messageID]: true }); Alert.alert( 'External link', `You sure you want to open this link?\n\n${displayURL}`, @@ -37,7 +41,14 @@ ], { cancelable: true, onDismiss }, ); - }, [setLinkModalActive, displayURL, onConfirm, onDismiss]); + }, [ + setLinkModalActive, + linkModalActive, + messageID, + displayURL, + onConfirm, + onDismiss, + ]); } type TextProps = React.ElementConfig; @@ -48,15 +59,20 @@ }; function MarkdownLink(props: Props): React.Node { const markdownContext = React.useContext(MarkdownContext); + const messageContext = React.useContext(MessageContext); + + const messageID = messageContext?.messageID; const { target, ...rest } = props; - const onPressLink = useDisplayLinkPrompt(target, markdownContext); + const onPressLink = useDisplayLinkPrompt(target, markdownContext, messageID); const setLinkPressActive = markdownContext?.setLinkPressActive; + const linkPressActive = markdownContext?.linkPressActive; + const androidOnStartShouldSetResponderCapture = React.useCallback(() => { - setLinkPressActive?.(true); + setLinkPressActive?.({ ...linkPressActive, [messageID]: true }); return true; - }, [setLinkPressActive]); + }, [setLinkPressActive, linkPressActive, messageID]); const activePressHasMoved = React.useRef(false); const androidOnResponderMove = React.useCallback(() => { @@ -68,8 +84,8 @@ onPressLink(); } activePressHasMoved.current = false; - setLinkPressActive?.(false); - }, [onPressLink, setLinkPressActive]); + setLinkPressActive?.({ ...linkPressActive, [messageID]: false }); + }, [onPressLink, setLinkPressActive, linkPressActive, messageID]); if (Platform.OS !== 'android') { return ; diff --git a/native/navigation/app-navigator.react.js b/native/navigation/app-navigator.react.js --- a/native/navigation/app-navigator.react.js +++ b/native/navigation/app-navigator.react.js @@ -21,6 +21,7 @@ import SWMansionIcon from '../components/swmansion-icon.react'; import { type SQLiteContextType, SQLiteContext } from '../data/sqlite-context'; import KeyboardStateContainer from '../keyboard/keyboard-state-container.react'; +import MarkdownContextProvider from '../markdown/markdown-context-provider.react'; import CameraModal from '../media/camera-modal.react'; import ImageModal from '../media/image-modal.react'; import VideoPlaybackModal from '../media/video-playback-modal.react'; @@ -212,39 +213,41 @@ } return ( - - - - - - - - - - - - + + + + + + + + + + + + + + {pushHandler} );