Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import {TNodeChildrenRenderer} from 'react-native-render-html';
import type {CustomRendererProps, TPhrasing, TText} from 'react-native-render-html';
import * as HTMLEngineUtils from '@components/HTMLEngineProvider/htmlEngineUtils';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';

function BlockQuoteRenderer({TDefaultRenderer, style, tnode, ...props}: CustomRendererProps<TText | TPhrasing>) {
const styles = useThemeStyles();
const isChildOfTaskTitle = HTMLEngineUtils.isChildOfTaskTitle(tnode);

return isChildOfTaskTitle ? (
<TNodeChildrenRenderer
tnode={tnode}
renderChild={(props2) => {
return (
<Text
style={[styles.webViewStyles.baseFontStyle]}
key={props2.key}
>
{props2.childElement}
</Text>
);
}}
/>
) : (
<TDefaultRenderer
// eslint-disable-next-line react/jsx-props-no-spreading
{...props}
tnode={tnode}
style={[style as StyleProp<TextStyle>]}
/>
);
}

BlockQuoteRenderer.displayName = 'BlockQuoteRenderer';

export default BlockQuoteRenderer;
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ import React, {memo} from 'react';
import {useOnyx} from 'react-native-onyx';
import type {OnyxEntry} from 'react-native-onyx';
import type {CustomRendererProps, TBlock} from 'react-native-render-html';
import AnchorForCommentsOnly from '@components/AnchorForCommentsOnly';
import {AttachmentContext} from '@components/AttachmentContext';
import {getButtonRole} from '@components/Button/utils';
import {isDeletedNode} from '@components/HTMLEngineProvider/htmlEngineUtils';
import {isChildOfTaskTitle, isDeletedNode} from '@components/HTMLEngineProvider/htmlEngineUtils';
import * as Expensicons from '@components/Icon/Expensicons';
import PressableWithoutFocus from '@components/Pressable/PressableWithoutFocus';
import {ShowContextMenuContext, showContextMenuForReport} from '@components/ShowContextMenuContext';
Expand Down Expand Up @@ -33,9 +34,25 @@ type ImageRendererProps = ImageRendererWithOnyxProps & CustomRendererProps<TBloc
function ImageRenderer({tnode}: ImageRendererProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const theme = useTheme();

const htmlAttribs = tnode.attributes;
const isDeleted = isDeletedNode(tnode);
const isTaskTitleChild = isChildOfTaskTitle(tnode);
const attrHref = htmlAttribs.src;

if (isTaskTitleChild) {
return (
<AnchorForCommentsOnly
href={attrHref}
target={htmlAttribs.target || '_blank'}
rel={htmlAttribs.rel || 'noopener noreferrer'}
style={[styles.link, styles.underlineLine, styles.textUnderlinePositionUnder, styles.textDecorationSkipInkNone, styles.taskTitleMenuItem]}
>
{htmlAttribs.alt}
</AnchorForCommentsOnly>
);
}

// There are two kinds of images that need to be displayed:
//
Expand Down Expand Up @@ -73,14 +90,12 @@ function ImageRenderer({tnode}: ImageRendererProps) {

const fileType = getFileType(attachmentSourceAttribute);
const fallbackIcon = fileType === CONST.ATTACHMENT_FILE_TYPE.FILE ? Expensicons.Document : Expensicons.GalleryNotFound;
const theme = useTheme();

let fileName = htmlAttribs[CONST.ATTACHMENT_ORIGINAL_FILENAME_ATTRIBUTE] || getFileName(`${isAttachmentOrReceipt ? attachmentSourceAttribute : htmlAttribs.src}`);
const fileInfo = splitExtensionFromFileName(fileName);
if (!fileInfo.fileExtension) {
fileName = `${fileInfo?.fileName || CONST.DEFAULT_IMAGE_FILE_NAME}.jpg`;
}

const thumbnailImageComponent = (
<ThumbnailImage
previewSourceURL={processedPreviewSource}
Expand Down
2 changes: 2 additions & 0 deletions src/components/HTMLEngineProvider/HTMLRenderers/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {CustomTagRendererRecord} from 'react-native-render-html';
import AnchorRenderer from './AnchorRenderer';
import BlockQuoteRenderer from './BlockQuotesRenderer';
import CodeRenderer from './CodeRenderer';
import DeletedActionRenderer from './DeletedActionRenderer';
import EditedRenderer from './EditedRenderer';
Expand Down Expand Up @@ -28,6 +29,7 @@ const HTMLEngineProviderComponentList: CustomTagRendererRecord = {
h1: HeadingRenderer,
strong: StrongRenderer,
em: EMRenderer,
blockquote: BlockQuoteRenderer,

// Custom tag renderers
edited: EditedRenderer,
Expand Down
10 changes: 9 additions & 1 deletion src/components/ReportActionItem/TaskPreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,14 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
return <RenderHTML html={`<deleted-action>${translate('parentReportAction.deletedTask')}</deleted-action>`} />;
}

const getTaskHTML = () => {
if (isTaskCompleted) {
return `<del><comment>${action?.childReportName ?? taskReport?.reportName ?? ''}</comment></del>`;
}

return `<comment>${action?.childReportName ?? taskReport?.reportName ?? ''}</comment>`;
};

return (
<View style={[styles.chatItemMessage, !hasAssignee && styles.mv1]}>
<PressableWithoutFeedback
Expand Down Expand Up @@ -127,7 +135,7 @@ function TaskPreview({taskReportID, action, contextMenuAnchor, chatReportID, che
</UserDetailsTooltip>
)}
<View style={[styles.alignSelfCenter, styles.flex1]}>
<RenderHTML html={`<comment>${taskReport?.reportName ?? action?.childReportName ?? ''}</comment>`} />
<RenderHTML html={getTaskHTML()} />
</View>
</View>
{shouldShowGreenDotIndicator && (
Expand Down
8 changes: 7 additions & 1 deletion src/components/Search/SearchAutocompleteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
parseForAutocomplete,
} from '@libs/SearchAutocompleteUtils';
import {buildSearchQueryJSON, buildUserReadableQueryString, sanitizeSearchValue} from '@libs/SearchQueryUtils';
import StringUtils from '@libs/StringUtils';
import Timing from '@userActions/Timing';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
Expand Down Expand Up @@ -478,7 +479,12 @@ function SearchAutocompleteList(
sections.push({title: translate('search.recentSearches'), data: recentSearchesData});
}

const styledRecentReports = recentReportsOptions.map((item) => ({...item, pressableStyle: styles.br2, wrapperStyle: [styles.pr3, styles.pl3]}));
const styledRecentReports = recentReportsOptions.map((item) => ({
...item,
text: StringUtils.lineBreaksToSpaces(item.text),
pressableStyle: styles.br2,
wrapperStyle: [styles.pr3, styles.pl3],
}));
sections.push({title: autocompleteQueryValue.trim() === '' ? translate('search.recentChats') : undefined, data: styledRecentReports});

if (autocompleteSuggestions.length > 0) {
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchRouter/SearchRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret}: SearchRouterProps,
{
data: [
{
text: `${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`,
text: StringUtils.lineBreaksToSpaces(`${translate('search.searchIn')} ${reportForContextualSearch.text ?? reportForContextualSearch.alternateText}`),
singleIcon: Expensicons.MagnifyingGlass,
searchQuery: reportQueryValue,
autocompleteID,
Expand Down
3 changes: 2 additions & 1 deletion src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4496,7 +4496,8 @@ function getReportNameInternal({
}

if (isTaskReport(report)) {
return Parser.htmlToText(report?.reportName ?? '');
return Parser.htmlToText(report?.reportName ?? '')
.trim();
}

if (isChatThread(report)) {
Expand Down
12 changes: 6 additions & 6 deletions src/libs/TaskUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,22 +52,22 @@ function getTaskReportActionMessage(action: OnyxEntry<ReportAction>): Pick<Messa
}
}

function getTaskTitleFromReport(taskReport: OnyxEntry<Report>, fallbackTitle = ''): string {
function getTaskTitleFromReport(taskReport: OnyxEntry<Report>, fallbackTitle = '', shouldReturnMarkdown = false): string {
// We need to check for reportID, not just reportName, because when a receiver opens the task for the first time,
// an optimistic report is created with the only property - reportName: 'Chat report',
// and it will be displayed as the task title without checking for reportID to be present.
const title = taskReport?.reportID && taskReport.reportName ? taskReport.reportName : fallbackTitle;
return Parser.htmlToText(title);
return shouldReturnMarkdown ? Parser.htmlToMarkdown(title) : Parser.htmlToText(title).trim();
}

function getTaskTitle(taskReportID: string | undefined, fallbackTitle = ''): string {
function getTaskTitle(taskReportID: string | undefined, fallbackTitle = '', shouldReturnMarkdown = false): string {
const taskReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${taskReportID}`];
return getTaskTitleFromReport(taskReport, fallbackTitle);
return getTaskTitleFromReport(taskReport, fallbackTitle, shouldReturnMarkdown);
}

function getTaskCreatedMessage(reportAction: OnyxEntry<ReportAction>) {
function getTaskCreatedMessage(reportAction: OnyxEntry<ReportAction>, shouldReturnMarkdown = false) {
const taskReportID = reportAction?.childReportID;
const taskTitle = getTaskTitle(taskReportID, reportAction?.childReportName);
const taskTitle = getTaskTitle(taskReportID, reportAction?.childReportName, shouldReturnMarkdown);
return taskTitle ? translateLocal('task.messages.created', {title: taskTitle}) : '';
}

Expand Down
2 changes: 1 addition & 1 deletion src/pages/home/report/ContextMenu/ContextMenuActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ const ContextMenuActions: ContextMenuAction[] = [
setClipboardMessage(displayMessage);
}
} else if (isCreatedTaskReportAction(reportAction)) {
const taskPreviewMessage = getTaskCreatedMessage(reportAction);
const taskPreviewMessage = getTaskCreatedMessage(reportAction, true);
Clipboard.setString(taskPreviewMessage);
} else if (isMemberChangeAction(reportAction)) {
const logMessage = getMemberChangeMessageFragment(reportAction, getReportName).html ?? '';
Expand Down
12 changes: 10 additions & 2 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,12 @@ const webViewStyles = (theme: ThemeColors) =>
textDecorationStyle: 'solid',
},

strong: {
// We set fontFamily and fontWeight directly in order to avoid overriding fontStyle.
fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontFamily,
fontWeight: FontUtils.fontFamily.platform.EXP_NEUE_BOLD.fontWeight,
},

a: link(theme),

ul: {
Expand Down Expand Up @@ -4278,18 +4284,20 @@ const styles = (theme: ThemeColors) =>
...writingDirection.ltr,
...headlineFont,
fontSize: variables.fontSizeXLarge,
lineHeight: variables.lineHeightSizeh2,
lineHeight: variables.lineHeighTaskTitle,
maxWidth: '100%',
...wordBreak.breakWord,
textUnderlineOffset: -1,
},

taskTitleMenuItemItalic: {
...writingDirection.ltr,
...headlineItalicFont,
fontSize: variables.fontSizeXLarge,
lineHeight: variables.lineHeightSizeh2,
lineHeight: variables.lineHeighTaskTitle,
maxWidth: '100%',
...wordBreak.breakWord,
textUnderlineOffset: -1,
},

taskDescriptionMenuItem: {
Expand Down
2 changes: 1 addition & 1 deletion src/styles/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -635,7 +635,7 @@ function getCodeFontSize(isInsideH1: boolean, isInsideTaskTitle?: boolean) {
return 15;
}
if (isInsideTaskTitle) {
return 19;
return 18;
}
return 13;
}
Expand Down
4 changes: 4 additions & 0 deletions src/styles/utils/textDecorationLine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ export default {
textDecorationLine: 'underline line-through',
textDecorationStyle: 'solid',
},
underlineLine: {
textDecorationLine: 'underline',
textDecorationStyle: 'solid',
},
} satisfies Record<string, TextStyle>;
1 change: 1 addition & 0 deletions src/styles/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export default {
lineHeightXXXLarge: getValueUsingPixelRatio(32, 37),
lineHeightSizeh1: getValueUsingPixelRatio(28, 32),
lineHeightSizeh2: getValueUsingPixelRatio(24, 28),
lineHeighTaskTitle: getValueUsingPixelRatio(26, 30),
lineHeightSignInHeroXSmall: getValueUsingPixelRatio(32, 37),
inputHeight: getValueUsingPixelRatio(52, 72),
inputHeightSmall: 28,
Expand Down