Skip to content

Commit

Permalink
Merge pull request #39697 from software-mansion-labs/39532-mentionsV2…
Browse files Browse the repository at this point in the history
…-roomMentionsSuggestions

[MentionsV2] Room mentions suggestions
  • Loading branch information
rlinoz authored Apr 12, 2024
2 parents e8c57de + 8b64eb2 commit 5f04270
Show file tree
Hide file tree
Showing 6 changed files with 150 additions and 30 deletions.
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
* 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 = {
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

0 comments on commit 5f04270

Please sign in to comment.