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

[Mentions v2] Support mentions in editing comments #40565

Open
wants to merge 20 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 2 additions & 4 deletions src/components/LHNOptionsList/OptionRowLHN.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {useFocusEffect} from '@react-navigation/native';
import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useRef, useState} from 'react';
import type {GestureResponderEvent, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
Expand All @@ -20,6 +19,7 @@ import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import DateUtils from '@libs/DateUtils';
import DomUtils from '@libs/DomUtils';
import {parseHtmlToText} from '@libs/OnyxAwareParser';
import * as OptionsListUtils from '@libs/OptionsListUtils';
import Performance from '@libs/Performance';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
Expand All @@ -29,8 +29,6 @@ import CONST from '@src/CONST';
import {isEmptyObject} from '@src/types/utils/EmptyObject';
import type {OptionRowLHNProps} from './types';

const parser = new ExpensiMark();

function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, optionItem, viewMode = 'default', style, onLayout = () => {}, hasDraftComment}: OptionRowLHNProps) {
const theme = useTheme();
const styles = useThemeStyles();
Expand Down Expand Up @@ -239,7 +237,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti
numberOfLines={1}
accessibilityLabel={translate('accessibilityHints.lastChatMessagePreview')}
>
{parser.htmlToText(optionItem.alternateText)}
{parseHtmlToText(optionItem.alternateText)}
</Text>
) : null}
</View>
Expand Down
7 changes: 3 additions & 4 deletions src/hooks/useCopySelectionHelper.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {ExpensiMark} from 'expensify-common';
import {useEffect} from 'react';
import Clipboard from '@libs/Clipboard';
import KeyboardShortcut from '@libs/KeyboardShortcut';
import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
import SelectionScraper from '@libs/SelectionScraper';
import CONST from '@src/CONST';

Expand All @@ -10,12 +10,11 @@ function copySelectionToClipboard() {
if (!selection) {
return;
}
const parser = new ExpensiMark();
if (!Clipboard.canSetHtml()) {
Clipboard.setString(parser.htmlToMarkdown(selection));
Clipboard.setString(parseHtmlToMarkdown(selection));
return;
}
Clipboard.setHtml(selection, parser.htmlToText(selection));
Clipboard.setHtml(selection, parseHtmlToText(selection));
}

export default function useCopySelectionHelper() {
Expand Down
5 changes: 2 additions & 3 deletions src/hooks/useHtmlPaste/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useNavigation} from '@react-navigation/native';
import {ExpensiMark} from 'expensify-common';
import {useCallback, useEffect} from 'react';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import type UseHtmlPaste from './types';

const insertByCommand = (text: string) => {
Expand Down Expand Up @@ -62,8 +62,7 @@ const useHtmlPaste: UseHtmlPaste = (textInputRef, preHtmlPasteCallback, removeLi
*/
const handlePastedHTML = useCallback(
(html: string) => {
const parser = new ExpensiMark();
paste(parser.htmlToMarkdown(html));
paste(parseHtmlToMarkdown(html));
},
[paste],
);
Expand Down
42 changes: 42 additions & 0 deletions src/libs/OnyxAwareParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import {ExpensiMark} from 'expensify-common';
import Onyx from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

const parser = new ExpensiMark();

const reportIDToNameMap: Record<string, string> = {};
const accountIDToNameMap: Record<string, string> = {};

Onyx.connect({
key: ONYXKEYS.COLLECTION.REPORT,
callback: (report) => {
if (!report) {
return;
}

reportIDToNameMap[report.reportID] = report.reportName ?? report.displayName ?? report.reportID;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Not sure if we should fallback to id, but we did it before so should be fine

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, I think it makes sense to fallback to reportID. And if we would prefer reportID to empty string "", then we can use logical OR || and add comment to disable lint check

// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing

Copy link
Contributor

Choose a reason for hiding this comment

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

Out of curiosity, do you know when displayName is filled?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

tbh I don't know 😅 It's useful for the users but I'm not sure about the rooms

},
});

Onyx.connect({
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (personalDetailsList) => {
Object.values(personalDetailsList ?? {}).forEach((personalDetails) => {
if (!personalDetails) {
return;
}

accountIDToNameMap[personalDetails.accountID] = personalDetails.login ?? String(personalDetails.accountID);
});
},
});

function parseHtmlToMarkdown(html: string, reportIDToName?: Record<string, string>, accountIDToName?: Record<string, string>): string {
return parser.htmlToMarkdown(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap});
}

function parseHtmlToText(html: string, reportIDToName?: Record<string, string>, accountIDToName?: Record<string, string>): string {
return parser.htmlToText(html, {reportIDToName: reportIDToName ?? reportIDToNameMap, accountIDToName: accountIDToName ?? accountIDToNameMap});
}

export {parseHtmlToMarkdown, parseHtmlToText};
15 changes: 6 additions & 9 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import ModifiedExpenseMessage from './ModifiedExpenseMessage';
import linkingConfig from './Navigation/linkingConfig';
import Navigation from './Navigation/Navigation';
import * as NumberUtils from './NumberUtils';
import {parseHtmlToText} from './OnyxAwareParser';
import Permissions from './Permissions';
import * as PersonalDetailsUtils from './PersonalDetailsUtils';
import * as PhoneNumber from './PhoneNumber';
Expand Down Expand Up @@ -3186,8 +3187,7 @@ function parseReportActionHtmlToText(reportAction: OnyxEntry<ReportAction>, repo
const logins = PersonalDetailsUtils.getLoginsByAccountIDs(accountIDs);
accountIDs.forEach((id, index) => (accountIDToName[id] = logins[index]));

const parser = new ExpensiMark();
const textMessage = Str.removeSMSDomain(parser.htmlToText(html, {reportIDToName, accountIDToName}));
const textMessage = Str.removeSMSDomain(parseHtmlToText(html, reportIDToName, accountIDToName));
parsedReportActionMessageCache[key] = textMessage;

return textMessage;
Expand Down Expand Up @@ -3557,17 +3557,15 @@ function getReportDescriptionText(report: Report): string {
return '';
}

const parser = new ExpensiMark();
return parser.htmlToText(report.description);
return parseHtmlToText(report.description);
}

function getPolicyDescriptionText(policy: OnyxEntry<Policy>): string {
if (!policy?.description) {
return '';
}

const parser = new ExpensiMark();
return parser.htmlToText(policy.description);
return parseHtmlToText(policy.description);
}

function buildOptimisticAddCommentReportAction(
Expand All @@ -3578,7 +3576,6 @@ function buildOptimisticAddCommentReportAction(
shouldEscapeText?: boolean,
reportID?: string,
): OptimisticReportAction {
const parser = new ExpensiMark();
const commentText = getParsedComment(text ?? '', {shouldEscapeText, reportID});
const isAttachmentOnly = file && !text;
const isTextOnly = text && !file;
Expand All @@ -3590,10 +3587,10 @@ function buildOptimisticAddCommentReportAction(
textForNewComment = CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML;
} else if (isTextOnly) {
htmlForNewComment = commentText;
textForNewComment = parser.htmlToText(htmlForNewComment);
textForNewComment = parseHtmlToText(htmlForNewComment);
} else {
htmlForNewComment = `${commentText}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
textForNewComment = `${parser.htmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
textForNewComment = `${parseHtmlToText(commentText)}\n${CONST.ATTACHMENT_UPLOADING_MESSAGE_HTML}`;
}

const isAttachment = !text && file !== undefined;
Expand Down
5 changes: 3 additions & 2 deletions src/libs/actions/Report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import Log from '@libs/Log';
import Navigation from '@libs/Navigation/Navigation';
import type {NetworkStatus} from '@libs/NetworkConnection';
import LocalNotification from '@libs/Notification/LocalNotification';
import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils';
import * as PhoneNumber from '@libs/PhoneNumber';
import getPolicyEmployeeAccountIDs from '@libs/PolicyEmployeeListUtils';
Expand Down Expand Up @@ -1482,15 +1483,15 @@ function editReportComment(reportID: string, originalReportAction: OnyxEntry<Rep
// https://github.com/Expensify/App/issues/9090
// https://github.com/Expensify/App/issues/13221
const originalCommentHTML = originalReportAction.message?.[0]?.html;
const originalCommentMarkdown = parser.htmlToMarkdown(originalCommentHTML ?? '').trim();
const originalCommentMarkdown = parseHtmlToMarkdown(originalCommentHTML ?? '').trim();

// Skip the Edit if draft is not changed
if (originalCommentMarkdown === textForNewComment) {
return;
}

const htmlForNewComment = handleUserDeletedLinksInHtml(textForNewComment, originalCommentMarkdown);
const reportComment = parser.htmlToText(htmlForNewComment);
const reportComment = parseHtmlToText(htmlForNewComment);

// For comments shorter than or equal to 10k chars, convert the comment from MD into HTML because that's how it is stored in the database
// For longer comments, skip parsing and display plaintext for performance reasons. It takes over 40s to parse a 100k long string!!
Expand Down
8 changes: 4 additions & 4 deletions src/pages/PrivateNotes/PrivateNotesEditPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {useFocusEffect} from '@react-navigation/native';
import type {StackScreenProps} from '@react-navigation/stack';
import {ExpensiMark, Str} from 'expensify-common';
import {Str} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {Keyboard} from 'react-native';
Expand All @@ -19,6 +19,7 @@ import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import * as ReportUtils from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import type {WithReportAndPrivateNotesOrNotFoundProps} from '@pages/home/report/withReportAndPrivateNotesOrNotFound';
Expand Down Expand Up @@ -50,9 +51,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri
const {translate} = useLocalize();

// We need to edit the note in markdown format, but display it in HTML format
const parser = new ExpensiMark();
const [privateNote, setPrivateNote] = useState(
() => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(),
() => ReportActions.getDraftPrivateNote(report.reportID).trim() || parseHtmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(),
);

/**
Expand Down Expand Up @@ -93,7 +93,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report, session}: Pri
const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '';
let editedNote = '';
if (privateNote.trim() !== originalNote.trim()) {
editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim());
editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parseHtmlToMarkdown(originalNote).trim());
ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote);
}

Expand Down
5 changes: 2 additions & 3 deletions src/pages/RoomDescriptionPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {useFocusEffect} from '@react-navigation/native';
import {ExpensiMark} from 'expensify-common';
import React, {useCallback, useRef, useState} from 'react';
import {View} from 'react-native';
import type {OnyxCollection} from 'react-native-onyx';
Expand All @@ -13,6 +12,7 @@ import TextInput from '@components/TextInput';
import type {BaseTextInputRef} from '@components/TextInput/BaseTextInput/types';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import * as ReportUtils from '@libs/ReportUtils';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import variables from '@styles/variables';
Expand All @@ -32,8 +32,7 @@ type RoomDescriptionPageProps = {

function RoomDescriptionPage({report, policies}: RoomDescriptionPageProps) {
const styles = useThemeStyles();
const parser = new ExpensiMark();
const [description, setDescription] = useState(() => parser.htmlToMarkdown(report?.description ?? ''));
const [description, setDescription] = useState(() => parseHtmlToMarkdown(report?.description ?? ''));
const reportDescriptionInputRef = useRef<BaseTextInputRef | null>(null);
const focusTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const {translate} = useLocalize();
Expand Down
11 changes: 5 additions & 6 deletions src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ExpensiMark, Str} from 'expensify-common';
import {Str} from 'expensify-common';
import type {MutableRefObject} from 'react';
import React from 'react';
import {InteractionManager} from 'react-native';
Expand All @@ -18,6 +18,7 @@ import getAttachmentDetails from '@libs/fileDownload/getAttachmentDetails';
import * as Localize from '@libs/Localize';
import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage';
import Navigation from '@libs/Navigation/Navigation';
import {parseHtmlToMarkdown, parseHtmlToText} from '@libs/OnyxAwareParser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
Expand All @@ -40,13 +41,12 @@ function getActionHtml(reportAction: OnyxEntry<ReportAction>): string {

/** Sets the HTML string to Clipboard */
function setClipboardMessage(content: string) {
const parser = new ExpensiMark();
if (!Clipboard.canSetHtml()) {
Clipboard.setString(parser.htmlToMarkdown(content));
Clipboard.setString(parseHtmlToMarkdown(content));
} else {
const anchorRegex = CONST.REGEX_LINK_IN_ANCHOR;
const isAnchorTag = anchorRegex.test(content);
const plainText = isAnchorTag ? parser.htmlToMarkdown(content) : parser.htmlToText(content);
const plainText = isAnchorTag ? parseHtmlToMarkdown(content) : parseHtmlToText(content);
Clipboard.setHtml(content, plainText);
}
}
Expand Down Expand Up @@ -238,8 +238,7 @@ const ContextMenuActions: ContextMenuAction[] = [
}
const editAction = () => {
if (!draftMessage) {
const parser = new ExpensiMark();
Report.saveReportActionDraft(reportID, reportAction, parser.htmlToMarkdown(getActionHtml(reportAction)));
Report.saveReportActionDraft(reportID, reportAction, parseHtmlToMarkdown(getActionHtml(reportAction)));
} else {
Report.deleteReportActionDraft(reportID, reportAction);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import {useIsFocused, useNavigation} from '@react-navigation/native';
import {ExpensiMark} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react';
import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react';
Expand Down Expand Up @@ -35,6 +34,7 @@ import * as EmojiUtils from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import getPlatform from '@libs/getPlatform';
import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
import * as ReportUtils from '@libs/ReportUtils';
Expand Down Expand Up @@ -542,8 +542,7 @@ function ComposerWithSuggestions(
) {
event.preventDefault();
if (lastReportAction) {
const parser = new ExpensiMark();
Report.saveReportActionDraft(reportID, lastReportAction, parser.htmlToMarkdown(lastReportAction.message?.at(-1)?.html ?? ''));
Report.saveReportActionDraft(reportID, lastReportAction, parseHtmlToMarkdown(lastReportAction.message?.at(-1)?.html ?? ''));
}
}
},
Expand Down
5 changes: 2 additions & 3 deletions src/pages/home/report/ReportActionItemMessageEdit.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {ExpensiMark} from 'expensify-common';
import lodashDebounce from 'lodash/debounce';
import type {ForwardedRef} from 'react';
import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react';
Expand Down Expand Up @@ -27,6 +26,7 @@ import * as EmojiUtils from '@libs/EmojiUtils';
import focusComposerWithDelay from '@libs/focusComposerWithDelay';
import type {Selection} from '@libs/focusComposerWithDelay/types';
import focusEditAfterCancelDelete from '@libs/focusEditAfterCancelDelete';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import onyxSubscribe from '@libs/onyxSubscribe';
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
import * as ReportActionsUtils from '@libs/ReportActionsUtils';
Expand Down Expand Up @@ -105,8 +105,7 @@ function ReportActionItemMessageEdit(
const isCommentPendingSaved = useRef(false);

useEffect(() => {
const parser = new ExpensiMark();
const originalMessage = parser.htmlToMarkdown(action.message?.[0]?.html ?? '');
const originalMessage = parseHtmlToMarkdown(action.message?.[0]?.html ?? '');
if (ReportActionsUtils.isDeletedAction(action) || !!(action.message && draftMessage === originalMessage) || !!(prevDraftMessage === draftMessage || isCommentPendingSaved.current)) {
return;
}
Expand Down
3 changes: 2 additions & 1 deletion src/pages/tasks/NewTaskDescriptionPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import useThemeStyles from '@hooks/useThemeStyles';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {NewTaskNavigatorParamList} from '@libs/Navigation/types';
import {parseHtmlToMarkdown} from '@libs/OnyxAwareParser';
import updateMultilineInputRange from '@libs/updateMultilineInputRange';
import variables from '@styles/variables';
import * as TaskActions from '@userActions/Task';
Expand Down Expand Up @@ -78,7 +79,7 @@ function NewTaskDescriptionPage({task}: NewTaskDescriptionPageProps) {
<View style={styles.mb5}>
<InputWrapperWithRef
InputComponent={TextInput}
defaultValue={parser.htmlToMarkdown(parser.replace(task?.description ?? ''))}
defaultValue={parseHtmlToMarkdown(parser.replace(task?.description ?? ''))}
inputID={INPUT_IDS.TASK_DESCRIPTION}
label={translate('newTaskPage.descriptionOptional')}
accessibilityLabel={translate('newTaskPage.descriptionOptional')}
Expand Down