Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[MentionsV2] Room mentions suggestions #39697

Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 28 additions & 16 deletions src/components/MentionSuggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
* 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
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
* 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 = {
Expand Down Expand Up @@ -67,16 +77,18 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe

return (
<View style={[styles.autoCompleteSuggestionContainer, styles.ph2]}>
<View style={styles.mentionSuggestionsAvatarContainer}>
<Avatar
source={item.icons[0].source}
size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER}
name={item.icons[0].name}
type={item.icons[0].type}
fill={isIcon ? theme.success : undefined}
fallbackIcon={item.icons[0].fallbackIcon}
/>
</View>
{item.icons && !!item.icons.length && (
<View style={styles.mentionSuggestionsAvatarContainer}>
<Avatar
source={item.icons[0].source}
size={isIcon ? CONST.AVATAR_SIZE.MENTION_ICON : CONST.AVATAR_SIZE.SMALLER}
name={item.icons[0].name}
type={item.icons[0].type}
fill={isIcon ? theme.success : undefined}
fallbackIcon={item.icons[0].fallbackIcon}
/>
</View>
)}
<Text
style={[styles.mentionSuggestionsText, styles.flexShrink1]}
numberOfLines={1}
Expand Down
9 changes: 9 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5805,6 +5805,14 @@ function getOutstandingChildRequest(iouReport: OnyxEntry<Report> | EmptyObject):
return {};
}

function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyID: string): boolean {
if (report?.policyID !== policyID) {
return false;
}

return isChatRoom(report) && !isThread(report);
}

export {
getReportParticipantsTitle,
isReportMessageAttachment,
Expand Down Expand Up @@ -6032,6 +6040,7 @@ export {
getReportActionActorAccountID,
getGroupChatName,
getOutstandingChildRequest,
canReportBeMentionedWithinPolicy,
getAllHeldTransactions,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -217,6 +223,8 @@ function ComposerWithSuggestions(
isEmptyChat,
lastReportAction,
parentReportActionID,
isGroupPolicyReport,
policyID,

// Focus
onFocus,
Expand Down Expand Up @@ -769,6 +777,8 @@ function ComposerWithSuggestions(
composerHeight={composerHeight}
measureParentContainer={measureParentContainer}
isAutoSuggestionPickerLarge={isAutoSuggestionPickerLarge}
isGroupPolicyReport={isGroupPolicyReport}
policyID={policyID}
// Input
value={value}
setValue={setValue}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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}
Expand Down
104 changes: 90 additions & 14 deletions src/pages/home/report/ReportActionCompose/SuggestionMention.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';

Expand All @@ -23,6 +28,12 @@ type SuggestionValues = {
atSignIndex: number;
shouldShowSuggestionMenu: boolean;
mentionPrefix: string;
prefixType: string;
};

type RoomMentionOnyxProps = {
/** All reports shared with the user */
reports: OnyxCollection<Report>;
};

/**
Expand All @@ -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<SuggestionsRef>,
) {
const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT;
Expand Down Expand Up @@ -83,17 +106,25 @@ 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
*/
const insertSelectedMention = useCallback(
(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);
Expand All @@ -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,
],
);

/**
Expand Down Expand Up @@ -146,7 +186,7 @@ function SuggestionMention(
[highlightedMentionIndex, insertSelectedMention, resetSuggestions, suggestionValues.suggestedMentions.length],
);

const getMentionOptions = useCallback(
const getUserMentionOptions = useCallback(
(personalDetailsParam: PersonalDetailsList, searchValue = ''): Mention[] => {
const suggestions = [];

Expand Down Expand Up @@ -211,6 +251,27 @@ function SuggestionMention(
[translate, formatPhoneNumber, formatLoginPrivateDomain],
);

const getRoomMentionOptions = useCallback(
(searchTerm: string, reportBatch: OnyxCollection<Report>): 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) {
Expand Down Expand Up @@ -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}`;
Expand All @@ -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(() => {
Expand Down Expand Up @@ -330,4 +402,8 @@ function SuggestionMention(

SuggestionMention.displayName = 'SuggestionMention';

export default forwardRef(SuggestionMention);
export default withOnyx<SuggestionProps & RoomMentionOnyxProps & RefAttributes<SuggestionsRef>, RoomMentionOnyxProps>({
reports: {
key: ONYXKEYS.COLLECTION.REPORT,
},
})(forwardRef(SuggestionMention));
10 changes: 10 additions & 0 deletions src/pages/home/report/ReportActionCompose/Suggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand All @@ -66,6 +72,8 @@ function Suggestions(
measureParentContainer,
isAutoSuggestionPickerLarge = true,
isComposerFocused,
isGroupPolicyReport,
policyID,
}: SuggestionProps,
ref: ForwardedRef<SuggestionsRef>,
) {
Expand Down Expand Up @@ -155,6 +163,8 @@ function Suggestions(
isAutoSuggestionPickerLarge,
measureParentContainer,
isComposerFocused,
isGroupPolicyReport,
policyID,
};

return (
Expand Down
Loading