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'; @@ -43,7 +44,7 @@ // withOverlayContext +overlayContext: ?OverlayContextType, // MarkdownContext - +linkModalActive: boolean, + +isLinkModalActive: boolean, +linkIsBlockingPresses: boolean, }; class TextMessage extends React.PureComponent { @@ -58,7 +59,7 @@ toggleFocus, verticalBounds, overlayContext, - linkModalActive, + isLinkModalActive, linkIsBlockingPresses, canCreateSidebarFromMessage, ...viewProps @@ -67,7 +68,7 @@ let swipeOptions = 'none'; const canReply = this.canReply(); const canNavigateToSidebar = this.canNavigateToSidebar(); - if (linkModalActive) { + if (isLinkModalActive) { swipeOptions = 'none'; } else if (canReply && canNavigateToSidebar) { swipeOptions = 'both'; @@ -142,6 +143,7 @@ message, props: { verticalBounds, linkIsBlockingPresses }, } = this; + if (!message || !verticalBounds || linkIsBlockingPresses) { return; } @@ -199,32 +201,56 @@ const ConnectedTextMessage: React.ComponentType = React.memo( function ConnectedTextMessage(props: BaseProps) { const overlayContext = React.useContext(OverlayContext); + const markdownContext = React.useContext(MarkdownContext); + invariant(markdownContext, 'markdownContext should be set'); + + const { + linkModalActive, + linkPressActive, + clearMarkdownContextData, + } = markdownContext; + + const { id } = props.item.messageInfo; + invariant(id, 'messageInfo.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 isLinkModalActive = linkModalActive[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 () => { + clearMarkdownContextData(); + }; + }, [clearMarkdownContextData]); + 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,48 @@ +// @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<{ + [key: string]: boolean, + }>({}); + const [linkPressActive, setLinkPressActive] = React.useState<{ + [key: string]: boolean, + }>({}); + + const clearMarkdownContextData = React.useCallback(() => { + setLinkModalActive({}); + setLinkPressActive({}); + }, []); + + const contextValue = React.useMemo( + () => ({ + setLinkModalActive, + linkModalActive, + setLinkPressActive, + linkPressActive, + clearMarkdownContextData, + }), + [ + setLinkModalActive, + linkModalActive, + setLinkPressActive, + linkPressActive, + clearMarkdownContextData, + ], + ); + + 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<{ [key: string]: boolean }>, + +linkModalActive: { [key: string]: boolean }, + +setLinkPressActive: SetState<{ [key: string]: boolean }>, + +linkPressActive: { [key: string]: boolean }, + +clearMarkdownContextData: () => 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,21 @@ 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]); + if (linkModalActive) { + setLinkModalActive?.({ ...linkModalActive, [messageID]: false }); + } + }, [setLinkModalActive, linkModalActive, messageID]); const url = normalizeURL(inputURL); const onConfirm = React.useCallback(() => { @@ -27,7 +32,9 @@ displayURL += '…'; } return React.useCallback(() => { - setLinkModalActive && setLinkModalActive(true); + setLinkModalActive && + linkModalActive && + setLinkModalActive({ ...linkModalActive, [messageID]: true }); Alert.alert( 'External link', `You sure you want to open this link?\n\n${displayURL}`, @@ -37,7 +44,14 @@ ], { cancelable: true, onDismiss }, ); - }, [setLinkModalActive, displayURL, onConfirm, onDismiss]); + }, [ + setLinkModalActive, + linkModalActive, + messageID, + displayURL, + onConfirm, + onDismiss, + ]); } type TextProps = React.ElementConfig; @@ -48,15 +62,22 @@ }; 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); + if (linkPressActive) { + setLinkPressActive?.({ ...linkPressActive, [messageID]: true }); + } return true; - }, [setLinkPressActive]); + }, [setLinkPressActive, linkPressActive, messageID]); const activePressHasMoved = React.useRef(false); const androidOnResponderMove = React.useCallback(() => { @@ -68,8 +89,10 @@ onPressLink(); } activePressHasMoved.current = false; - setLinkPressActive?.(false); - }, [onPressLink, setLinkPressActive]); + if (linkPressActive) { + 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} );