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,13 @@ +// @flow + +import * as React from 'react'; + +export type MessageContextType = { + +messageID: string, +}; + +const MessageContext: React.Context = React.createContext( + null, +); + +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'; @@ -49,6 +50,8 @@ class TextMessage extends React.PureComponent { message: ?React.ElementRef; + static contextType = MarkdownContext; + render() { const { item, @@ -140,8 +143,18 @@ const { message, - props: { verticalBounds, linkIsBlockingPresses }, + props: { verticalBounds }, } = this; + + let { linkIsBlockingPresses } = this.props; + + /* We run an "updated check" against the Context to check whether the + specific message should be blocked from the tooltip appearing */ + const { messagesLinkModalActive, messagesLinkPressActive } = this.context; + linkIsBlockingPresses = + messagesLinkModalActive.get(this.props.item.messageInfo.id) || + messagesLinkPressActive.get(this.props.item.messageInfo.id); + if (!message || !verticalBounds || linkIsBlockingPresses) { return; } @@ -200,23 +213,65 @@ function ConnectedTextMessage(props: BaseProps) { const overlayContext = React.useContext(OverlayContext); - const [linkModalActive, setLinkModalActive] = React.useState(false); - const [linkPressActive, setLinkPressActive] = React.useState(false); - const markdownContext = React.useMemo( - () => ({ - setLinkModalActive, - setLinkPressActive, - }), - [setLinkModalActive, setLinkPressActive], - ); + /* We get the Markdown Context */ + const markdownContext = React.useContext(MarkdownContext); + invariant(markdownContext, 'should be set'); + + /* Extract the methods & maps */ + const { + setLinkModalActive, + setLinkPressActive, + clearMessageData, + messagesLinkModalActive, + messagesLinkPressActive, + } = markdownContext; + + /* Extract the messageID */ + const { id } = props.item.messageInfo; + invariant(id, 'should exist'); + + /* Set `linkModalActive` & `linkPressActive` to either the + value inside the map, or if it does + not exist, defualt to false There most likely + is not a value for the message if the thread has + just been opened */ + const linkModalActive = messagesLinkModalActive.get(id) ?? false; + const linkPressActive = messagesLinkPressActive.get(id) ?? false; + setLinkModalActive(id, linkModalActive); + setLinkPressActive(id, linkPressActive); + + /* Same logic is before to check if linkIsBlockingPresses */ + const linkIsBlockingPresses = linkModalActive || linkPressActive; + + /* Not relevant for Markdown refactoring */ 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], + ); + + /* This is how we want to check if the component is + unmounted (a specific message), + and thus we will clear the data for that message stored + in the Context maps */ + React.useEffect(() => { + return () => { + // Anything in here is fired on component unmount. + id ? clearMessageData(id) : null; + }; + }, [clearMessageData, id]); + 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,49 @@ +// @flow + +import * as React from 'react'; + +import { MarkdownContext } from './markdown-context.js'; + +type Props = { + +children: React.Node, +}; + +function MarkdownContextProvider(props: Props): React.Node { + const messagesLinkModalActive = React.useRef>(new Map()); + const setLinkModalActive = React.useCallback( + (messageID: string, active: boolean) => + messagesLinkModalActive.current.set(messageID, active), + [], + ); + + const messagesLinkPressActive = React.useRef>(new Map()); + const setLinkPressActive = React.useCallback( + (messageID: string, active: boolean) => + messagesLinkPressActive.current.set(messageID, active), + [], + ); + + const clearMessageData = React.useCallback((messageID: string) => { + messagesLinkModalActive.current.delete(messageID); + messagesLinkPressActive.current.delete(messageID); + }, []); + + const contextValue = React.useMemo( + () => ({ + setLinkModalActive, + setLinkPressActive, + clearMessageData, + messagesLinkModalActive: messagesLinkModalActive.current, + messagesLinkPressActive: messagesLinkPressActive.current, + }), + [setLinkModalActive, setLinkPressActive, 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 @@ -3,8 +3,11 @@ import * as React from 'react'; export type MarkdownContextType = { - +setLinkModalActive: boolean => void, - +setLinkPressActive: boolean => void, + +setLinkModalActive: (messageID: string, active: boolean) => mixed, + +setLinkPressActive: (messageID: string, active: boolean) => mixed, + +clearMessageData: (messageID: string) => void, + +messagesLinkModalActive: $ReadOnlyMap, + +messagesLinkPressActive: $ReadOnlyMap, }; 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,18 @@ 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 onDismiss = React.useCallback(() => { - setLinkModalActive?.(false); - }, [setLinkModalActive]); + setLinkModalActive?.(messageID, false); + }, [setLinkModalActive, messageID]); const url = normalizeURL(inputURL); const onConfirm = React.useCallback(() => { @@ -27,7 +29,7 @@ displayURL += '…'; } return React.useCallback(() => { - setLinkModalActive && setLinkModalActive(true); + setLinkModalActive && setLinkModalActive(messageID, true); Alert.alert( 'External link', `You sure you want to open this link?\n\n${displayURL}`, @@ -37,7 +39,7 @@ ], { cancelable: true, onDismiss }, ); - }, [setLinkModalActive, displayURL, onConfirm, onDismiss]); + }, [setLinkModalActive, messageID, displayURL, onConfirm, onDismiss]); } type TextProps = React.ElementConfig; @@ -48,15 +50,18 @@ }; 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 androidOnStartShouldSetResponderCapture = React.useCallback(() => { - setLinkPressActive?.(true); + setLinkPressActive?.(messageID, true); return true; - }, [setLinkPressActive]); + }, [setLinkPressActive, messageID]); const activePressHasMoved = React.useRef(false); const androidOnResponderMove = React.useCallback(() => { @@ -68,8 +73,8 @@ onPressLink(); } activePressHasMoved.current = false; - setLinkPressActive?.(false); - }, [onPressLink, setLinkPressActive]); + setLinkPressActive?.(messageID, false); + }, [onPressLink, setLinkPressActive, 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} );