diff --git a/native/chat/text-message.react.js b/native/chat/text-message.react.js index c2ed095b5..381848038 100644 --- a/native/chat/text-message.react.js +++ b/native/chat/text-message.react.js @@ -1,296 +1,315 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View } from 'react-native'; import { messageKey } from 'lib/shared/message-utils.js'; import { threadHasPermission, useCanCreateSidebarFromMessage, } from 'lib/shared/thread-utils.js'; import { threadPermissions } from 'lib/types/thread-permission-types.js'; import type { ChatNavigationProp } from './chat.react.js'; import ComposedMessage from './composed-message.react.js'; import { InnerTextMessage } from './inner-text-message.react.js'; import { MessageEditingContext } from './message-editing-context.react.js'; import { MessagePressResponderContext, type MessagePressResponderContextType, } from './message-press-responder-context.js'; import textMessageSendFailed from './text-message-send-failed.js'; import { getMessageTooltipKey } from './utils.js'; import { ChatContext, type ChatContextType } from '../chat/chat-context.js'; import { MarkdownContext } from '../markdown/markdown-context.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { useCanEditMessageNative } from '../navigation/nav-selectors.js'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; import { TextMessageTooltipModalRouteName } from '../navigation/route-names.js'; import type { NavigationRoute } from '../navigation/route-names.js'; import { fixedTooltipHeight } from '../tooltip/tooltip.react.js'; import type { ChatTextMessageInfoItemWithHeight } from '../types/chat-types.js'; import type { VerticalBounds } from '../types/layout-types.js'; type BaseProps = { ...React.ElementConfig, +item: ChatTextMessageInfoItemWithHeight, +navigation: | ChatNavigationProp<'MessageList'> | AppNavigationProp<'TogglePinModal'> | ChatNavigationProp<'MessageResultsScreen'> | ChatNavigationProp<'MessageSearch'>, +route: | NavigationRoute<'MessageList'> | NavigationRoute<'TogglePinModal'> | NavigationRoute<'MessageResultsScreen'> | NavigationRoute<'MessageSearch'>, +focused: boolean, +toggleFocus: (messageKey: string) => void, +verticalBounds: ?VerticalBounds, +shouldDisplayPinIndicator: boolean, }; type Props = { ...BaseProps, // Redux state +canCreateSidebarFromMessage: boolean, // withOverlayContext +overlayContext: ?OverlayContextType, // ChatContext +chatContext: ?ChatContextType, // MarkdownContext +isLinkModalActive: boolean, + +isUserProfileBottomSheetActive: boolean, +canEditMessage: boolean, +canTogglePins: boolean, }; class TextMessage extends React.PureComponent { message: ?React.ElementRef; messagePressResponderContext: MessagePressResponderContextType; constructor(props: Props) { super(props); this.messagePressResponderContext = { onPressMessage: this.onPress, }; } render() { const { item, navigation, route, focused, toggleFocus, verticalBounds, shouldDisplayPinIndicator, overlayContext, chatContext, isLinkModalActive, + isUserProfileBottomSheetActive, canCreateSidebarFromMessage, canEditMessage, canTogglePins, ...viewProps } = this.props; let swipeOptions = 'none'; const canReply = this.canReply(); const canNavigateToSidebar = this.canNavigateToSidebar(); if (isLinkModalActive) { swipeOptions = 'none'; } else if (canReply && canNavigateToSidebar) { swipeOptions = 'both'; } else if (canReply) { swipeOptions = 'reply'; } else if (canNavigateToSidebar) { swipeOptions = 'sidebar'; } return ( ); } messageRef = (message: ?React.ElementRef) => { this.message = message; }; canReply() { return threadHasPermission( this.props.item.threadInfo, threadPermissions.VOICED, ); } canNavigateToSidebar() { return ( this.props.item.threadCreatedFromMessage || this.props.canCreateSidebarFromMessage ); } visibleEntryIDs() { const result = ['copy']; if (this.canReply()) { result.push('reply'); } if (this.props.canEditMessage) { result.push('edit'); } if (this.props.canTogglePins) { this.props.item.isPinned ? result.push('unpin') : result.push('pin'); } if ( this.props.item.threadCreatedFromMessage || this.props.canCreateSidebarFromMessage ) { result.push('sidebar'); } if (!this.props.item.messageInfo.creator.isViewer) { result.push('report'); } return result; } onPress = () => { const visibleEntryIDs = this.visibleEntryIDs(); if (visibleEntryIDs.length === 0) { return; } const { message, - props: { verticalBounds, isLinkModalActive }, + props: { + verticalBounds, + isLinkModalActive, + isUserProfileBottomSheetActive, + }, } = this; - if (!message || !verticalBounds || isLinkModalActive) { + + if ( + !message || + !verticalBounds || + isLinkModalActive || + isUserProfileBottomSheetActive + ) { return; } const { focused, toggleFocus, item } = this.props; if (!focused) { toggleFocus(messageKey(item.messageInfo)); } const { overlayContext } = this.props; invariant(overlayContext, 'TextMessage should have OverlayContext'); overlayContext.setScrollBlockingModalStatus('open'); message.measure((x, y, width, height, pageX, pageY) => { const coordinates = { x: pageX, y: pageY, width, height }; const messageTop = pageY; const messageBottom = pageY + height; const boundsTop = verticalBounds.y; const boundsBottom = verticalBounds.y + verticalBounds.height; const belowMargin = 20; const belowSpace = fixedTooltipHeight + belowMargin; const { isViewer } = item.messageInfo.creator; const aboveMargin = isViewer ? 30 : 50; const aboveSpace = fixedTooltipHeight + aboveMargin; let margin = belowMargin; if ( messageBottom + belowSpace > boundsBottom && messageTop - aboveSpace > boundsTop ) { margin = aboveMargin; } const currentInputBarHeight = this.props.chatContext?.chatInputBarHeights.get(item.threadInfo.id) ?? 0; this.props.navigation.navigate<'TextMessageTooltipModal'>({ name: TextMessageTooltipModalRouteName, params: { presentedFrom: this.props.route.key, initialCoordinates: coordinates, verticalBounds, visibleEntryIDs, tooltipLocation: 'fixed', margin, item, chatInputBarHeight: currentInputBarHeight, }, key: getMessageTooltipKey(item), }); }); }; } const ConnectedTextMessage: React.ComponentType = React.memo(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, clearMarkdownContextData } = markdownContext; + const { + linkModalActive, + userProfileBottomSheetActive, + clearMarkdownContextData, + } = markdownContext; const key = messageKey(props.item.messageInfo); // We check if there is an key in the object - 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 isLinkModalActive = linkModalActive[key] ?? false; + const isUserProfileBottomSheetActive = + userProfileBottomSheetActive[key] ?? false; const canCreateSidebarFromMessage = useCanCreateSidebarFromMessage( props.item.threadInfo, props.item.messageInfo, ); const messageEditingContext = React.useContext(MessageEditingContext); const editMessageID = messageEditingContext?.editState.editedMessage?.id; const isThisMessageEdited = editMessageID === props.item.messageInfo.id; const canEditMessage = useCanEditMessageNative(props.item.threadInfo, props.item.messageInfo) && !isThisMessageEdited; const canTogglePins = threadHasPermission( props.item.threadInfo, threadPermissions.MANAGE_PINS, ); React.useEffect(() => clearMarkdownContextData, [clearMarkdownContextData]); return ( ); }); export { ConnectedTextMessage as TextMessage }; diff --git a/native/markdown/markdown-context-provider.react.js b/native/markdown/markdown-context-provider.react.js index 4dcc59dce..44b04b057 100644 --- a/native/markdown/markdown-context-provider.react.js +++ b/native/markdown/markdown-context-provider.react.js @@ -1,51 +1,61 @@ // @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 [userProfileBottomSheetActive, setUserProfileBottomSheetActive] = + React.useState<{ + [key: string]: boolean, + }>({}); + const [spoilerRevealed, setSpoilerRevealed] = React.useState<{ [key: string]: { [key: number]: boolean, }, }>({}); const clearMarkdownContextData = React.useCallback(() => { setLinkModalActive({}); + setUserProfileBottomSheetActive({}); setSpoilerRevealed({}); }, []); const contextValue = React.useMemo( () => ({ setLinkModalActive, linkModalActive, + userProfileBottomSheetActive, + setUserProfileBottomSheetActive, setSpoilerRevealed, spoilerRevealed, clearMarkdownContextData, }), [ setLinkModalActive, linkModalActive, + setUserProfileBottomSheetActive, + userProfileBottomSheetActive, setSpoilerRevealed, spoilerRevealed, clearMarkdownContextData, ], ); return ( {props.children} ); } export default MarkdownContextProvider; diff --git a/native/markdown/markdown-context.js b/native/markdown/markdown-context.js index 550c6fee0..bc18e4f62 100644 --- a/native/markdown/markdown-context.js +++ b/native/markdown/markdown-context.js @@ -1,18 +1,20 @@ // @flow import * as React from 'react'; import type { SetState } from 'lib/types/hook-types.js'; export type MarkdownContextType = { +setLinkModalActive: SetState<{ [key: string]: boolean }>, +linkModalActive: { [key: string]: boolean }, + +setUserProfileBottomSheetActive: SetState<{ [key: string]: boolean }>, + +userProfileBottomSheetActive: { [key: string]: boolean }, +setSpoilerRevealed: SetState<{ [key: string]: { [key: number]: boolean } }>, +spoilerRevealed: { [key: string]: { [key: number]: boolean } }, +clearMarkdownContextData: () => void, }; const MarkdownContext: React.Context = React.createContext(null); export { MarkdownContext };