diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 23040a242807..3866911fff9e 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -12,17 +12,27 @@ import Avatar from './Avatar'; import Text from './Text'; type Mention = { - /** Display name of the user */ + /** + * Main display text of the mention + * always visible right after icon (if present) + */ text: string; - /** The formatted email/phone number of the user */ + /** + * Additional text for the mention + * visible if it's value is different than Mention.text value + * rendered after Mention.text + */ alternateText: string; - /** Email/phone number of the user */ - login?: string; + /** + * Handle of the mention + * used as a value for the mention (e.g. in for the filtering or putting the mention in the message) + */ + handle?: string; - /** Array of icons of the user. We use the first element of this array */ - icons: Icon[]; + /** Array of icons of the mention. If present, we use the first element of this array. For room suggestions, the icons are not used */ + icons?: Icon[]; }; type MentionSuggestionsProps = { @@ -67,16 +77,18 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe return ( - - - + {item.icons && !!item.icons.length && ( + + + + )} | EmptyObject): return {}; } +function canReportBeMentionedWithinPolicy(report: OnyxEntry, policyID: string): boolean { + if (report?.policyID !== policyID) { + return false; + } + + return isChatRoom(report) && !isThread(report); +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -6032,6 +6040,7 @@ export { getReportActionActorAccountID, getGroupChatName, getOutstandingChildRequest, + canReportBeMentionedWithinPolicy, getAllHeldTransactions, }; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index f8147dfda81d..8f42da5a1575 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -171,6 +171,12 @@ type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & /** The parent report ID */ // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC parentReportID: string | undefined; + + /** Whether report is from group policy */ + isGroupPolicyReport: boolean; + + /** policy ID of the report */ + policyID: string; }; const {RNTextInputReset} = NativeModules; @@ -217,6 +223,8 @@ function ComposerWithSuggestions( isEmptyChat, lastReportAction, parentReportActionID, + isGroupPolicyReport, + policyID, // Focus onFocus, @@ -769,6 +777,8 @@ function ComposerWithSuggestions( composerHeight={composerHeight} measureParentContainer={measureParentContainer} isAutoSuggestionPickerLarge={isAutoSuggestionPickerLarge} + isGroupPolicyReport={isGroupPolicyReport} + policyID={policyID} // Input value={value} setValue={setValue} diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 5f7f5adebfc9..b9b9025bb02b 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -342,6 +342,7 @@ function ReportActionCompose({ [], ); + const isGroupPolicyReport = useMemo(() => ReportUtils.isGroupPolicy(report), [report]); const reportRecipientAcountIDs = ReportUtils.getReportRecipientAccountIDs(report, currentUserPersonalDetails.accountID); const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; @@ -431,9 +432,11 @@ function ReportActionCompose({ isScrollLikelyLayoutTriggered={isScrollLikelyLayoutTriggered} raiseIsScrollLikelyLayoutTriggered={raiseIsScrollLikelyLayoutTriggered} reportID={reportID} + policyID={report?.policyID ?? ''} parentReportID={report?.parentReportID} parentReportActionID={report?.parentReportActionID} includeChronos={ReportUtils.chatIncludesChronos(report)} + isGroupPolicyReport={isGroupPolicyReport} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 5bf172e82388..147799ee118b 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,7 +1,9 @@ import Str from 'expensify-common/lib/str'; import lodashSortBy from 'lodash/sortBy'; -import type {ForwardedRef} from 'react'; +import type {ForwardedRef, RefAttributes} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import * as Expensicons from '@components/Icon/Expensicons'; import type {Mention} from '@components/MentionSuggestions'; import MentionSuggestions from '@components/MentionSuggestions'; @@ -11,10 +13,13 @@ import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails' import useLocalize from '@hooks/useLocalize'; import * as LoginUtils from '@libs/LoginUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; +import {isValidRoomName} from '@libs/ValidationUtils'; import CONST from '@src/CONST'; -import type {PersonalDetailsList} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList, Report} from '@src/types/onyx'; import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; @@ -23,6 +28,12 @@ type SuggestionValues = { atSignIndex: number; shouldShowSuggestionMenu: boolean; mentionPrefix: string; + prefixType: string; +}; + +type RoomMentionOnyxProps = { + /** All reports shared with the user */ + reports: OnyxCollection; }; /** @@ -35,10 +46,22 @@ const defaultSuggestionsValues: SuggestionValues = { atSignIndex: -1, shouldShowSuggestionMenu: false, mentionPrefix: '', + prefixType: '', }; function SuggestionMention( - {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionProps, + { + value, + selection, + setSelection, + updateComment, + isAutoSuggestionPickerLarge, + measureParentContainer, + isComposerFocused, + reports, + isGroupPolicyReport, + policyID, + }: SuggestionProps & RoomMentionOnyxProps, ref: ForwardedRef, ) { const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; @@ -83,6 +106,17 @@ function SuggestionMention( [currentUserPersonalDetails.login], ); + const getMentionCode = useCallback( + (mention: Mention, mentionType: string): string => { + if (mentionType === '#') { + // room mention case + return mention.handle ?? ''; + } + return mention.text === CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT ? CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT : `@${formatLoginPrivateDomain(mention.handle, mention.handle)}`; + }, + [formatLoginPrivateDomain], + ); + /** * Replace the code of mention and update selection */ @@ -90,10 +124,7 @@ function SuggestionMention( (highlightedMentionIndexInner: number) => { const commentBeforeAtSign = value.slice(0, suggestionValues.atSignIndex); const mentionObject = suggestionValues.suggestedMentions[highlightedMentionIndexInner]; - const mentionCode = - mentionObject.text === CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT - ? CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT - : `@${formatLoginPrivateDomain(mentionObject.login, mentionObject.login)}`; + const mentionCode = getMentionCode(mentionObject, suggestionValues.prefixType); const commentAfterMention = value.slice(suggestionValues.atSignIndex + suggestionValues.mentionPrefix.length + 1); updateComment(`${commentBeforeAtSign}${mentionCode} ${SuggestionsUtils.trimLeadingSpace(commentAfterMention)}`, true); @@ -108,7 +139,16 @@ function SuggestionMention( suggestedMentions: [], })); }, - [value, suggestionValues.atSignIndex, suggestionValues.suggestedMentions, suggestionValues.mentionPrefix, updateComment, setSelection, formatLoginPrivateDomain], + [ + value, + suggestionValues.atSignIndex, + suggestionValues.suggestedMentions, + suggestionValues.prefixType, + suggestionValues.mentionPrefix.length, + getMentionCode, + updateComment, + setSelection, + ], ); /** @@ -146,7 +186,7 @@ function SuggestionMention( [highlightedMentionIndex, insertSelectedMention, resetSuggestions, suggestionValues.suggestedMentions.length], ); - const getMentionOptions = useCallback( + const getUserMentionOptions = useCallback( (personalDetailsParam: PersonalDetailsList, searchValue = ''): Mention[] => { const suggestions = []; @@ -211,6 +251,27 @@ function SuggestionMention( [translate, formatPhoneNumber, formatLoginPrivateDomain], ); + const getRoomMentionOptions = useCallback( + (searchTerm: string, reportBatch: OnyxCollection): Mention[] => { + const filteredRoomMentions: Mention[] = []; + Object.values(reportBatch ?? {}).forEach((report) => { + if (!ReportUtils.canReportBeMentionedWithinPolicy(report, policyID ?? '')) { + return; + } + if (report?.reportName?.toLowerCase().includes(searchTerm.toLowerCase())) { + filteredRoomMentions.push({ + text: report.reportName, + handle: report.reportName, + alternateText: report.reportName, + }); + } + }); + + return lodashSortBy(filteredRoomMentions, 'handle').slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS); + }, + [policyID], + ); + const calculateMentionSuggestion = useCallback( (selectionEnd: number) => { if (shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) { @@ -239,13 +300,15 @@ function SuggestionMention( let atSignIndex: number | undefined; let suggestionWord = ''; let prefix: string; + let prefixType = ''; // Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it) - if (lastWord.startsWith('@')) { + if (lastWord.startsWith('@') || lastWord.startsWith('#')) { atSignIndex = leftString.lastIndexOf(lastWord) + afterLastBreakLineIndex; suggestionWord = lastWord; prefix = suggestionWord.substring(1); + prefixType = suggestionWord.substring(0, 1); } else if (secondToLastWord && secondToLastWord.startsWith('@') && secondToLastWord.length > 1) { atSignIndex = leftString.lastIndexOf(secondToLastWord) + afterLastBreakLineIndex; suggestionWord = `${secondToLastWord} ${lastWord}`; @@ -259,23 +322,32 @@ function SuggestionMention( suggestedMentions: [], atSignIndex, mentionPrefix: prefix, + prefixType, }; const isCursorBeforeTheMention = valueAfterTheCursor.startsWith(suggestionWord); - if (!isCursorBeforeTheMention && isMentionCode(suggestionWord)) { - const suggestions = getMentionOptions(personalDetails, prefix); + if (!isCursorBeforeTheMention && isMentionCode(suggestionWord) && prefixType === '@') { + const suggestions = getUserMentionOptions(personalDetails, prefix); nextState.suggestedMentions = suggestions; nextState.shouldShowSuggestionMenu = !!suggestions.length; } + const shouldDisplayRoomMentionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === ''); + if (!isCursorBeforeTheMention && prefixType === '#' && shouldDisplayRoomMentionsSuggestions) { + // filter reports by room name and current policy + const filteredRoomMentions = getRoomMentionOptions(prefix, reports); + nextState.suggestedMentions = filteredRoomMentions; + nextState.shouldShowSuggestionMenu = !!filteredRoomMentions.length; + } + setSuggestionValues((prevState) => ({ ...prevState, ...nextState, })); setHighlightedMentionIndex(0); }, - [getMentionOptions, personalDetails, resetSuggestions, setHighlightedMentionIndex, value, isComposerFocused], + [isComposerFocused, value, isGroupPolicyReport, setHighlightedMentionIndex, resetSuggestions, getUserMentionOptions, personalDetails, getRoomMentionOptions, reports], ); useEffect(() => { @@ -330,4 +402,8 @@ function SuggestionMention( SuggestionMention.displayName = 'SuggestionMention'; -export default forwardRef(SuggestionMention); +export default withOnyx, RoomMentionOnyxProps>({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, +})(forwardRef(SuggestionMention)); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 61026a792919..8ebd52f62428 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -46,6 +46,12 @@ type SuggestionProps = { /** The height of the composer */ composerHeight?: number; + + /** If current composer is connected with report from group policy */ + isGroupPolicyReport: boolean; + + /** The policyID of the report connected to current composer */ + policyID?: string; }; /** @@ -66,6 +72,8 @@ function Suggestions( measureParentContainer, isAutoSuggestionPickerLarge = true, isComposerFocused, + isGroupPolicyReport, + policyID, }: SuggestionProps, ref: ForwardedRef, ) { @@ -155,6 +163,8 @@ function Suggestions( isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused, + isGroupPolicyReport, + policyID, }; return (