diff --git a/native/chat/inner-text-message.react.js b/native/chat/inner-text-message.react.js --- a/native/chat/inner-text-message.react.js +++ b/native/chat/inner-text-message.react.js @@ -5,6 +5,7 @@ import { View, StyleSheet, TouchableWithoutFeedback } from 'react-native'; import Animated from 'react-native-reanimated'; +import { messageKey } from 'lib/shared/message-utils'; import { colorIsDark } from 'lib/shared/thread-utils'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react'; @@ -13,6 +14,7 @@ import { useColors, colors } from '../themes/colors'; import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types'; import { useComposedMessageMaxWidth } from './composed-message-width'; +import { MessageContext } from './message-context.react'; import { MessageListContext } from './message-list-types'; import { allCorners, @@ -101,6 +103,16 @@ return [styles.text, textStyle]; }, [darkColor]); + // We use a MessageContext to allow MarkdownLink and MarkdownSpoiler + // to access the messageKey so it is 'self-aware' + const key = messageKey(item.messageInfo); + const messageContextValue = React.useMemo( + () => ({ + messageKey: key, + }), + [key], + ); + const message = ( @@ -111,9 +123,11 @@ style={[styles.message, cornerStyle]} animatedStyle={messageStyle} > - - {text} - + + + {text} + + 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 = { + +messageKey: string, +}; + +const MessageContext: React.Context = React.createContext( + { + messageKey: '', + }, +); + +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 @@ -46,7 +46,7 @@ // ChatContext +chatContext: ?ChatContextType, // MarkdownContext - +linkModalActive: boolean, + +isLinkModalActive: boolean, +linkIsBlockingPresses: boolean, }; class TextMessage extends React.PureComponent { @@ -62,7 +62,7 @@ verticalBounds, overlayContext, chatContext, - linkModalActive, + isLinkModalActive, linkIsBlockingPresses, canCreateSidebarFromMessage, ...viewProps @@ -71,7 +71,7 @@ let swipeOptions = 'none'; const canReply = this.canReply(); const canNavigateToSidebar = this.canNavigateToSidebar(); - if (linkModalActive) { + if (isLinkModalActive) { swipeOptions = 'none'; } else if (canReply && canNavigateToSidebar) { swipeOptions = 'both'; @@ -147,6 +147,7 @@ message, props: { verticalBounds, linkIsBlockingPresses }, } = this; + if (!message || !verticalBounds || linkIsBlockingPresses) { return; } @@ -208,33 +209,41 @@ function ConnectedTextMessage(props: BaseProps) { const overlayContext = React.useContext(OverlayContext); const chatContext = React.useContext(ChatContext); + const markdownContext = React.useContext(MarkdownContext); + invariant(markdownContext, 'markdownContext should be set'); + + const { + linkModalActive, + linkPressActive, + clearMarkdownContextData, + } = markdownContext; + + const key = messageKey(props.item.messageInfo); + + // 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[key] || linkPressActive[key]) ?? false; + + const isLinkModalActive = linkModalActive[key] ?? 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; + React.useEffect(() => 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, + messageKey: string, ) { const setLinkModalActive = markdownContext?.setLinkModalActive; + const linkModalActive = markdownContext?.linkModalActive; const onDismiss = React.useCallback(() => { - setLinkModalActive?.(false); - }, [setLinkModalActive]); + if (linkModalActive) { + setLinkModalActive?.({ ...linkModalActive, [messageKey]: false }); + } + }, [setLinkModalActive, linkModalActive, messageKey]); const url = normalizeURL(inputURL); const onConfirm = React.useCallback(() => { @@ -27,7 +32,9 @@ displayURL += '…'; } return React.useCallback(() => { - setLinkModalActive && setLinkModalActive(true); + setLinkModalActive && + linkModalActive && + setLinkModalActive({ ...linkModalActive, [messageKey]: 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, + messageKey, + 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 messageKey = messageContext?.messageKey; const { target, ...rest } = props; - const onPressLink = useDisplayLinkPrompt(target, markdownContext); + const onPressLink = useDisplayLinkPrompt(target, markdownContext, messageKey); const setLinkPressActive = markdownContext?.setLinkPressActive; + const linkPressActive = markdownContext?.linkPressActive; + const androidOnStartShouldSetResponderCapture = React.useCallback(() => { - setLinkPressActive?.(true); + if (linkPressActive) { + setLinkPressActive?.({ ...linkPressActive, [messageKey]: true }); + } return true; - }, [setLinkPressActive]); + }, [setLinkPressActive, linkPressActive, messageKey]); 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, [messageKey]: false }); + } + }, [onPressLink, setLinkPressActive, linkPressActive, messageKey]); if (Platform.OS !== 'android') { return ; diff --git a/native/root.react.js b/native/root.react.js --- a/native/root.react.js +++ b/native/root.react.js @@ -27,6 +27,7 @@ import ErrorBoundary from './error-boundary.react'; import InputStateContainer from './input/input-state-container.react'; import LifecycleHandler from './lifecycle/lifecycle-handler.react'; +import MarkdownContextProvider from './markdown/markdown-context-provider.react'; import { defaultNavigationState } from './navigation/default-state'; import DisconnectedBarVisibilityHandler from './navigation/disconnected-bar-visibility-handler.react'; import { setGlobalNavContext } from './navigation/icky-global'; @@ -248,23 +249,25 @@ - - - - - {gated} - - - - - {navigation} - - - + + + + + + {gated} + + + + + {navigation} + + + +