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 6 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
26 changes: 14 additions & 12 deletions src/components/MentionSuggestions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ type Mention = {
/** Email/phone number of the user */
login?: string;

/** Array of icons of the user. We use the first element of this array */
icons: Icon[];
/** Array of icons of the user. 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 +67,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 @@ -5752,6 +5752,14 @@ function getOutstandingChildRequest(iouReport: OnyxEntry<Report> | EmptyObject):
return {};
}

function canReportBeMentionedWithinPolicy(report: OnyxEntry<Report>, policyId: string): boolean {
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
if (report?.policyID !== policyId) {
return false;
}

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

export {
getReportParticipantsTitle,
isReportMessageAttachment,
Expand Down Expand Up @@ -5978,6 +5986,7 @@ export {
hasActionsWithErrors,
getGroupChatName,
getOutstandingChildRequest,
canReportBeMentionedWithinPolicy,
};

export type {
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 chat is a reoprt from group policy */
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/** Whether chat is a reoprt from group policy */
/** Whether chat is a report from group policy */

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should say "chat" here. I think it's just:

Suggested change
/** Whether chat is a reoprt from group policy */
/** 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
103 changes: 89 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.login ?? '';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we returning a login in the room mention case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before Mention type was only used for users, thats why key names are named with users in mind. I didn't change it, because I didn't want to make this PR bigger. So I just follow the Mention type - in the line 264 we are setting login value to roomName.

LEt me know If I should change the Mention type naming convencion so it would be more universal

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh I see. I feel like we should change it. The way it is right now feels confusing to me. What do you think @rlinoz?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I missed this one, sorry.

Yeah, I agree we should change it, mention.handle maybe?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I like that. Or maybe mention.displayText

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On it 👀

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@puneetlath @rlinoz Went with handle as with displayText we would end up with three different fields named around text: text, alternateText, displayText

}
return mention.text === CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT ? CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT : `@${formatLoginPrivateDomain(mention.login, mention.login)}`;
},
[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,26 @@ 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,
login: report.reportName,
alternateText: report.reportName,
});
}
});
return filteredRoomMentions.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 +299,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 +321,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 shouldDisplayMenetionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

typo

Suggested change
const shouldDisplayMenetionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === '');
const shouldDisplayMentionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === '');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const shouldDisplayMenetionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === '');
const shouldDisplayRoomMentionsSuggestions = isGroupPolicyReport && (isValidRoomName(suggestionWord.toLowerCase()) || prefix === '');

This variable is only for room mentions, right?

if (!isCursorBeforeTheMention && prefixType === '#' && shouldDisplayMenetionsSuggestions) {
// 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 +401,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 */
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
isGroupPolicyReport: boolean;

/** policy ID connected to current composer */
robertKozik marked this conversation as resolved.
Show resolved Hide resolved
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