diff --git a/native/chat/chat-input-bar.react.js b/native/chat/chat-input-bar.react.js --- a/native/chat/chat-input-bar.react.js +++ b/native/chat/chat-input-bar.react.js @@ -57,6 +57,7 @@ threadActualMembers, checkIfDefaultMembersAreVoiced, draftKeyFromThreadID, + useThreadChatMentionCandidates, } from 'lib/shared/thread-utils.js'; import type { CalendarQuery } from 'lib/types/entry-types.js'; import type { LoadingStatus } from 'lib/types/loading-types.js'; @@ -1258,13 +1259,17 @@ parentThreadInfo, ); + const chatMentionCandidates = useThreadChatMentionCandidates( + props.threadInfo, + ); + const messageEditingContext = React.useContext(MessageEditingContext); const editedMessageInfo = messageEditingContext?.editState.editedMessage; const editedMessagePreview = useMessagePreview( editedMessageInfo, props.threadInfo, - getDefaultTextMessageRules().simpleMarkdownRules, + getDefaultTextMessageRules(chatMentionCandidates).simpleMarkdownRules, ); const editMessage = useEditMessage(); diff --git a/native/chat/message-list-types.js b/native/chat/message-list-types.js --- a/native/chat/message-list-types.js +++ b/native/chat/message-list-types.js @@ -1,16 +1,21 @@ // @flow -import { useNavigation } from '@react-navigation/native'; +import { useNavigation, useNavigationState } from '@react-navigation/native'; import invariant from 'invariant'; import * as React from 'react'; +import { useThreadChatMentionCandidates } from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { type UserInfo } from 'lib/types/user-types.js'; +import { ChatContext } from './chat-context.js'; import type { ChatRouterNavigationAction } from './chat-router.js'; import type { MarkdownRules } from '../markdown/rules.react.js'; import { useTextMessageRulesFunc } from '../markdown/rules.react.js'; -import { MessageListRouteName } from '../navigation/route-names.js'; +import { + MessageListRouteName, + TextMessageTooltipModalRouteName, +} from '../navigation/route-names.js'; export type MessageListParams = { +threadInfo: ThreadInfo, @@ -31,7 +36,11 @@ React.createContext(); function useMessageListContext(threadInfo: ThreadInfo) { - const getTextMessageMarkdownRules = useTextMessageRulesFunc(threadInfo); + const chatMentionCandidates = useThreadChatMentionCandidates(threadInfo); + const getTextMessageMarkdownRules = useTextMessageRulesFunc( + threadInfo, + chatMentionCandidates, + ); return React.useMemo( () => ({ getTextMessageMarkdownRules, @@ -84,9 +93,40 @@ return messageListContext.getTextMessageMarkdownRules(useDarkStyle); } +function useNavigateToThreadWithFadeAnimation( + threadInfo: ThreadInfo, + messageKey: ?string, +): () => mixed { + const chatContext = React.useContext(ChatContext); + invariant(chatContext, 'ChatContext should be set'); + const setSidebarSourceID = chatContext?.setCurrentTransitionSidebarSourceID; + const setSidebarAnimationType = chatContext?.setSidebarAnimationType; + const navigateToThread = useNavigateToThread(); + const navigationStack = useNavigationState(state => state.routes); + + return React.useCallback(() => { + if ( + navigationStack[navigationStack.length - 1].name === + TextMessageTooltipModalRouteName + ) { + setSidebarSourceID && setSidebarSourceID(messageKey); + setSidebarAnimationType && setSidebarAnimationType('fade_source_message'); + } + navigateToThread({ threadInfo }); + }, [ + messageKey, + navigateToThread, + navigationStack, + setSidebarAnimationType, + setSidebarSourceID, + threadInfo, + ]); +} + export { MessageListContextProvider, createNavigateToThreadAction, useNavigateToThread, useTextMessageMarkdownRules, + useNavigateToThreadWithFadeAnimation, }; diff --git a/native/chat/message-preview.react.js b/native/chat/message-preview.react.js --- a/native/chat/message-preview.react.js +++ b/native/chat/message-preview.react.js @@ -5,6 +5,7 @@ import { Text } from 'react-native'; import { useMessagePreview } from 'lib/shared/message-utils.js'; +import { useThreadChatMentionCandidates } from 'lib/shared/thread-utils.js'; import { type MessageInfo } from 'lib/types/message-types.js'; import { type ThreadInfo } from 'lib/types/thread-types.js'; @@ -18,10 +19,12 @@ }; function MessagePreview(props: Props): React.Node { const { messageInfo, threadInfo } = props; + + const chatMentionCandidates = useThreadChatMentionCandidates(threadInfo); const messagePreviewResult = useMessagePreview( messageInfo, threadInfo, - getDefaultTextMessageRules().simpleMarkdownRules, + getDefaultTextMessageRules(chatMentionCandidates).simpleMarkdownRules, ); invariant( messagePreviewResult, diff --git a/native/chat/sidebar-input-bar-height-measurer.react.js b/native/chat/sidebar-input-bar-height-measurer.react.js --- a/native/chat/sidebar-input-bar-height-measurer.react.js +++ b/native/chat/sidebar-input-bar-height-measurer.react.js @@ -4,6 +4,7 @@ import { View, StyleSheet } from 'react-native'; import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; +import { useChatMentionCandidatesObj } from 'lib/shared/thread-utils.js'; import { DummyChatInputBar } from './chat-input-bar.react.js'; import { useMessageListScreenWidth } from './composed-message-width.js'; @@ -21,9 +22,15 @@ const width = useMessageListScreenWidth(); const loggedInUserInfo = useLoggedInUserInfo(); + const chatMentionCandidatesObj = useChatMentionCandidatesObj(); const sidebarThreadInfo = React.useMemo( - () => getUnresolvedSidebarThreadInfo({ sourceMessage, loggedInUserInfo }), - [sourceMessage, loggedInUserInfo], + () => + getUnresolvedSidebarThreadInfo({ + sourceMessage, + loggedInUserInfo, + chatMentionCandidatesObj, + }), + [sourceMessage, loggedInUserInfo, chatMentionCandidatesObj], ); if (!sidebarThreadInfo) { return null; diff --git a/native/chat/sidebar-navigation.js b/native/chat/sidebar-navigation.js --- a/native/chat/sidebar-navigation.js +++ b/native/chat/sidebar-navigation.js @@ -8,8 +8,13 @@ import { createPendingSidebar, createUnresolvedPendingSidebar, + useChatMentionCandidatesObj, + threadChatMentionCandidates, } from 'lib/shared/thread-utils.js'; -import type { ThreadInfo } from 'lib/types/thread-types.js'; +import type { + ThreadInfo, + ChatMentionCandidatesObj, +} from 'lib/types/thread-types.js'; import type { LoggedInUserInfo } from 'lib/types/user-types.js'; import type { GetENSNames } from 'lib/utils/ens-helpers.js'; @@ -21,11 +26,12 @@ type GetUnresolvedSidebarThreadInfoInput = { +sourceMessage: ChatMessageInfoItemWithHeight, +loggedInUserInfo: ?LoggedInUserInfo, + +chatMentionCandidatesObj: ChatMentionCandidatesObj, }; function getUnresolvedSidebarThreadInfo( input: GetUnresolvedSidebarThreadInfoInput, ): ?ThreadInfo { - const { sourceMessage, loggedInUserInfo } = input; + const { sourceMessage, loggedInUserInfo, chatMentionCandidatesObj } = input; const threadCreatedFromMessage = sourceMessage.threadCreatedFromMessage; if (threadCreatedFromMessage) { return threadCreatedFromMessage; @@ -36,22 +42,35 @@ } const { messageInfo, threadInfo } = sourceMessage; + const chatMentionCandidates = threadChatMentionCandidates( + threadInfo, + chatMentionCandidatesObj, + ); + return createUnresolvedPendingSidebar({ sourceMessageInfo: messageInfo, parentThreadInfo: threadInfo, loggedInUserInfo, - markdownRules: getDefaultTextMessageRules().simpleMarkdownRules, + markdownRules: getDefaultTextMessageRules(chatMentionCandidates) + .simpleMarkdownRules, }); } type GetSidebarThreadInfoInput = { - ...GetUnresolvedSidebarThreadInfoInput, + +sourceMessage: ChatMessageInfoItemWithHeight, + +loggedInUserInfo: ?LoggedInUserInfo, +getENSNames: ?GetENSNames, + +chatMentionCandidatesObj: ChatMentionCandidatesObj, }; async function getSidebarThreadInfo( input: GetSidebarThreadInfoInput, ): Promise { - const { sourceMessage, loggedInUserInfo, getENSNames } = input; + const { + sourceMessage, + loggedInUserInfo, + getENSNames, + chatMentionCandidatesObj, + } = input; const threadCreatedFromMessage = sourceMessage.threadCreatedFromMessage; if (threadCreatedFromMessage) { return threadCreatedFromMessage; @@ -62,11 +81,17 @@ } const { messageInfo, threadInfo } = sourceMessage; + const chatMentionCandidates = threadChatMentionCandidates( + threadInfo, + chatMentionCandidatesObj, + ); + return await createPendingSidebar({ sourceMessageInfo: messageInfo, parentThreadInfo: threadInfo, loggedInUserInfo, - markdownRules: getDefaultTextMessageRules().simpleMarkdownRules, + markdownRules: getDefaultTextMessageRules(chatMentionCandidates) + .simpleMarkdownRules, getENSNames, }); } @@ -77,16 +102,24 @@ const loggedInUserInfo = useLoggedInUserInfo(); const navigateToThread = useNavigateToThread(); const cacheContext = React.useContext(ENSCacheContext); + const chatMentionCandidatesObj = useChatMentionCandidatesObj(); const { getENSNames } = cacheContext; return React.useCallback(async () => { const threadInfo = await getSidebarThreadInfo({ sourceMessage: item, loggedInUserInfo, getENSNames, + chatMentionCandidatesObj, }); invariant(threadInfo, 'threadInfo should be set'); navigateToThread({ threadInfo }); - }, [navigateToThread, item, loggedInUserInfo, getENSNames]); + }, [ + item, + loggedInUserInfo, + getENSNames, + chatMentionCandidatesObj, + navigateToThread, + ]); } function useAnimatedNavigateToSidebar( diff --git a/native/chat/utils.js b/native/chat/utils.js --- a/native/chat/utils.js +++ b/native/chat/utils.js @@ -7,7 +7,10 @@ import { useLoggedInUserInfo } from 'lib/hooks/account-hooks.js'; import { colorIsDark } from 'lib/shared/color-utils.js'; import { messageKey } from 'lib/shared/message-utils.js'; -import { viewerIsMember } from 'lib/shared/thread-utils.js'; +import { + viewerIsMember, + useChatMentionCandidatesObj, +} from 'lib/shared/thread-utils.js'; import type { ThreadInfo } from 'lib/types/thread-types.js'; import { clusterEndHeight } from './chat-constants.js'; @@ -206,9 +209,15 @@ } = chatContext; const loggedInUserInfo = useLoggedInUserInfo(); + const chatMentionCandidatesObj = useChatMentionCandidatesObj(); const sidebarThreadInfo = React.useMemo( - () => getUnresolvedSidebarThreadInfo({ sourceMessage, loggedInUserInfo }), - [sourceMessage, loggedInUserInfo], + () => + getUnresolvedSidebarThreadInfo({ + sourceMessage, + loggedInUserInfo, + chatMentionCandidatesObj, + }), + [sourceMessage, loggedInUserInfo, chatMentionCandidatesObj], ); const currentInputBarHeight = diff --git a/native/markdown/markdown-chat-mention.react.js b/native/markdown/markdown-chat-mention.react.js new file mode 100644 --- /dev/null +++ b/native/markdown/markdown-chat-mention.react.js @@ -0,0 +1,35 @@ +// @flow + +import * as React from 'react'; +import { Text } from 'react-native'; + +import type { ResolvedThreadInfo } from 'lib/types/thread-types.js'; + +import { + useMarkdownOnPressUtils, + useHandleChatMentionClick, +} from './markdown-utils.js'; + +type TextProps = React.ElementConfig; +type Props = { + +threadInfo: ResolvedThreadInfo, + +hasAccessToChat: boolean, + +children: React.Node, + ...TextProps, +}; +function MarkdownChatMention(props: Props): React.Node { + const { threadInfo, hasAccessToChat, style, ...rest } = props; + const { messageKey, shouldBePressable, onLongPressHandler } = + useMarkdownOnPressUtils(!hasAccessToChat); + const onPressHandler = useHandleChatMentionClick(threadInfo, messageKey); + return ( + + ); +} + +export default MarkdownChatMention; diff --git a/native/markdown/markdown-utils.js b/native/markdown/markdown-utils.js --- a/native/markdown/markdown-utils.js +++ b/native/markdown/markdown-utils.js @@ -5,12 +5,14 @@ import { Linking } from 'react-native'; import { inviteLinkUrl } from 'lib/facts/links.js'; +import type { ThreadInfo } from 'lib/types/thread-types.js'; import { MarkdownContext, type MarkdownContextType, } from './markdown-context.js'; import { MarkdownSpoilerContext } from './markdown-spoiler-context.js'; +import { useNavigateToThreadWithFadeAnimation } from '../chat/message-list-types.js'; import { MessagePressResponderContext } from '../chat/message-press-responder-context.js'; import { TextMessageMarkdownContext } from '../chat/text-message-markdown-context.js'; import { InviteLinksContext } from '../invite-links/invite-links-context-provider.react.js'; @@ -101,4 +103,19 @@ ]); } -export { useMarkdownOnPressUtils, useHandleLinkClick }; +function useHandleChatMentionClick( + threadInfo: ThreadInfo, + messageKey: ?string, +): () => mixed { + const navigateToThreadWithFadeAnimation = + useNavigateToThreadWithFadeAnimation(threadInfo, messageKey); + return React.useCallback(() => { + navigateToThreadWithFadeAnimation(); + }, [navigateToThreadWithFadeAnimation]); +} + +export { + useMarkdownOnPressUtils, + useHandleLinkClick, + useHandleChatMentionClick, +}; diff --git a/native/markdown/rules.react.js b/native/markdown/rules.react.js --- a/native/markdown/rules.react.js +++ b/native/markdown/rules.react.js @@ -6,7 +6,11 @@ import * as SimpleMarkdown from 'simple-markdown'; import * as SharedMarkdown from 'lib/shared/markdown.js'; -import type { RelativeMemberInfo, ThreadInfo } from 'lib/types/thread-types.js'; +import type { + RelativeMemberInfo, + ThreadInfo, + ChatMentionCandidates, +} from 'lib/types/thread-types.js'; import MarkdownLink from './markdown-link.react.js'; import MarkdownParagraph from './markdown-paragraph.react.js'; @@ -353,19 +357,21 @@ function useTextMessageRulesFunc( threadInfo: ThreadInfo, + chatMentionCandidates: ChatMentionCandidates, ): (useDarkStyle: boolean) => MarkdownRules { const { members } = threadInfo; return React.useMemo( () => _memoize<[boolean], MarkdownRules>((useDarkStyle: boolean) => - textMessageRules(members, useDarkStyle), + textMessageRules(members, chatMentionCandidates, useDarkStyle), ), - [members], + [members, chatMentionCandidates], ); } function textMessageRules( members: $ReadOnlyArray, + chatMentionCandidates: ChatMentionCandidates, useDarkStyle: boolean, ): MarkdownRules { const styles = getMarkdownStyles(useDarkStyle ? 'dark' : 'light'); @@ -395,8 +401,10 @@ }; } -const getDefaultTextMessageRules: () => MarkdownRules = _memoize(() => - textMessageRules([], false), +const getDefaultTextMessageRules: ( + overrideDefaultChatMentionCandidates?: ChatMentionCandidates, +) => MarkdownRules = _memoize((overrideDefaultChatMentionCandidates = {}) => + textMessageRules([], overrideDefaultChatMentionCandidates, false), ); export {