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

Feature: Render markdown in thread header #24574

Closed
Closed
Show file tree
Hide file tree
Changes from 75 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
32a46ee
feature: render markdown in thread header
tienifr Aug 10, 2023
9b3c217
add comment
tienifr Aug 15, 2023
d6fafdb
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Aug 15, 2023
0c9829c
render code fence as inline code
tienifr Aug 15, 2023
1cb14d0
render markdown on native
tienifr Aug 18, 2023
ad73440
refactor html message
tienifr Aug 18, 2023
eea1007
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Aug 18, 2023
8111823
run prettier
tienifr Aug 18, 2023
cc8071e
rename shouldRenderHTML
tienifr Aug 18, 2023
6a61a36
update block element list
tienifr Aug 18, 2023
3bfe861
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Aug 22, 2023
23ac696
custom renderer to pass numberOfLines
tienifr Aug 22, 2023
f8fea9b
fix lint
tienifr Aug 22, 2023
e069de0
fix lint
tienifr Aug 22, 2023
804672f
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Aug 22, 2023
4b2b7ce
remove flex 1
tienifr Aug 22, 2023
4596197
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Aug 23, 2023
5b2951e
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Aug 23, 2023
555d4be
only show first line in thread lhn
tienifr Aug 23, 2023
6102ac9
fix lint
tienifr Aug 23, 2023
d8459fa
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Aug 23, 2023
8fb1b8a
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Sep 8, 2023
171edbd
revert removing flex: 1
tienifr Sep 8, 2023
96da94e
Merge branch 'main'
tienifr Sep 18, 2023
0744817
fix: ellipsis header
tienifr Sep 18, 2023
6d57338
Revert "fix: ellipsis header"
tienifr Sep 18, 2023
6aca837
fix ellipsis header
tienifr Sep 18, 2023
5f1b01c
Merge branch 'main'
tienifr Sep 26, 2023
727436a
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Sep 26, 2023
55b01cf
render html in LHN and report details page
tienifr Sep 27, 2023
96da21f
display one line text
tienifr Sep 27, 2023
99eff2e
adjust height of render html container
tienifr Sep 27, 2023
488e9d9
adjust height on native only
tienifr Sep 27, 2023
72209df
truncate text on web
tienifr Sep 27, 2023
e27a398
do not render html in details page
tienifr Sep 27, 2023
90c9ead
fix lint
tienifr Sep 27, 2023
51235e9
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Sep 27, 2023
759ec0e
remove redundant imports
tienifr Sep 27, 2023
62afa9d
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Oct 4, 2023
99fe8dd
resolve conflicts
tienifr Oct 4, 2023
cdfdd58
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Oct 10, 2023
0632f7d
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 11, 2023
057a78d
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 12, 2023
b630ff0
render first line of text in LHN
tienifr Oct 12, 2023
a9cc3d7
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 13, 2023
65164ff
fix text overflown on android
tienifr Oct 13, 2023
3543ea3
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 13, 2023
8f95128
fix text overflown on native
tienifr Oct 13, 2023
e162f59
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Oct 24, 2023
c9b5c9e
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Oct 30, 2023
3543cf6
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 30, 2023
753b75c
fix lint
tienifr Oct 30, 2023
8b12df6
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Oct 31, 2023
ce9953b
unescape html entities
tienifr Oct 31, 2023
3384654
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Dec 5, 2023
ddde5d6
reapply changes
tienifr Dec 5, 2023
ee78205
rename param
tienifr Dec 6, 2023
f98485f
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Jan 22, 2024
91903c1
unescape html chars in thread title
tienifr Jan 22, 2024
b9bbccf
fix: code blocks flicker
tienifr Jan 22, 2024
0ef5a6d
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Feb 23, 2024
7767205
unescape html entity in LHN
tienifr Feb 23, 2024
e868c58
show ellipsis for truncated text
tienifr Feb 23, 2024
fbc84da
fix: clipped blockquote
tienifr Feb 23, 2024
14b82ac
fix: clipped code snippet
tienifr Feb 23, 2024
8857f04
fix: some markdowns are clipped
tienifr Feb 23, 2024
292f113
fix lint
tienifr Feb 23, 2024
f3acd60
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Feb 24, 2024
8eb159e
fix: header text on native is not bold
tienifr Feb 24, 2024
c712a20
fix lint
tienifr Feb 24, 2024
b9aff52
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Feb 28, 2024
c8edb0d
remove redundant change
tienifr Feb 28, 2024
7068ddb
remove oudated comment
tienifr Feb 28, 2024
4979581
remove redundant changes
tienifr Feb 28, 2024
a70bd42
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Mar 7, 2024
b09f817
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Mar 11, 2024
7b3d922
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Mar 12, 2024
d309f69
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Mar 13, 2024
4780811
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr Mar 21, 2024
4f969fc
fix: blockquote and inline code truncation
tienifr Mar 21, 2024
137e138
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Mar 25, 2024
7b01ff9
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr Apr 1, 2024
db51209
Merge branch 'main' into feature/render-markdown-in-thread-header
tienifr May 8, 2024
808fd69
fix: trimmed spaces in thread markdown
tienifr May 9, 2024
f8d8472
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr May 15, 2024
f34b7db
Merge branch 'main' of https://github.com/tienifr/App into feature/re…
tienifr May 24, 2024
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
8 changes: 6 additions & 2 deletions src/components/DisplayNames/DisplayNamesWithTooltip.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import React, {Fragment, useCallback, useRef} from 'react';
import React, {Fragment, useCallback, useMemo, useRef} from 'react';
// eslint-disable-next-line no-restricted-imports
import type {Text as RNText} from 'react-native';
import {View} from 'react-native';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import Tooltip from '@components/Tooltip';
import useThemeStyles from '@hooks/useThemeStyles';
import * as ReportUtils from '@libs/ReportUtils';
import StringUtils from '@libs/StringUtils';
import DisplayNamesTooltipItem from './DisplayNamesTooltipItem';
import type DisplayNamesProps from './types';

Expand Down Expand Up @@ -47,6 +49,8 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit
return textNodeRight > containerRight ? -(tooltipX - newToolX) : 0;
}, []);

const title = useMemo(() => (StringUtils.containsHtml(fullTitle) ? <RenderHTML html={fullTitle} /> : ReportUtils.formatReportLastMessageText(fullTitle)), [fullTitle]);

return (
// Tokenization of string only support prop numberOfLines on Web
<Text
Expand All @@ -56,7 +60,7 @@ function DisplayNamesWithToolTip({shouldUseFullTitle, fullTitle, displayNamesWit
testID={DisplayNamesWithToolTip.displayName}
>
{shouldUseFullTitle
? ReportUtils.formatReportLastMessageText(fullTitle)
? title
: displayNamesWithTooltips?.map(({displayName, accountID, avatar, login}, index) => (
// eslint-disable-next-line react/no-array-index-key
<Fragment key={index}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import React from 'react';
import type {StyleProp, TextStyle} from 'react-native';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import useThemeStyles from '@hooks/useThemeStyles';
import StringUtils from '@libs/StringUtils';

type DisplayNamesWithoutTooltipProps = {
/** The full title of the DisplayNames component (not split up) */
Expand All @@ -19,12 +21,14 @@ type DisplayNamesWithoutTooltipProps = {

function DisplayNamesWithoutTooltip({textStyles = [], numberOfLines = 1, fullTitle = '', renderAdditionalText}: DisplayNamesWithoutTooltipProps) {
const styles = useThemeStyles();
const title = StringUtils.containsHtml(fullTitle) ? <RenderHTML html={fullTitle} /> : fullTitle;

return (
<Text
style={[textStyles, numberOfLines === 1 ? styles.pre : styles.preWrap]}
numberOfLines={numberOfLines}
>
{fullTitle}
{title}
{renderAdditionalText?.()}
</Text>
);
Expand Down
15 changes: 15 additions & 0 deletions src/components/DisplayNames/index.native.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@
import React from 'react';
import {View} from 'react-native';
import RenderHTML from '@components/RenderHTML';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import StringUtils from '@libs/StringUtils';
import type DisplayNamesProps from './types';

// As we don't have to show tooltips of the Native platform so we simply render the full display names list.
function DisplayNames({accessibilityLabel, fullTitle, textStyles = [], numberOfLines = 1, renderAdditionalText}: DisplayNamesProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();

const containsHtml = StringUtils.containsHtml(fullTitle);
if (containsHtml) {
return (
<View style={styles.renderHTMLThreadTitle}>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I cannot nest with the below Text because that would add a huge gap between the header title and subtitle on native devices.

<RenderHTML html={fullTitle} />
</View>
);
}

return (
<Text
accessibilityLabel={accessibilityLabel}
Expand Down
12 changes: 10 additions & 2 deletions src/components/HTMLEngineProvider/BaseHTMLEngineProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
mixedUAStyles: {whiteSpace: 'pre'},
contentModel: HTMLContentModel.textual,
}),
'thread-title': HTMLElementModel.fromCustomModel({
tagName: 'thread-title',
contentModel: HTMLContentModel.textual,
mixedUAStyles: {...styles.headerText, whiteSpace: 'pre'},
reactNativeProps: {
text: {numberOfLines: 1},
},
}),
'mention-user': HTMLElementModel.fromCustomModel({tagName: 'mention-user', contentModel: HTMLContentModel.textual}),
'mention-here': HTMLElementModel.fromCustomModel({tagName: 'mention-here', contentModel: HTMLContentModel.textual}),
'next-step': HTMLElementModel.fromCustomModel({
Expand All @@ -71,13 +79,13 @@ function BaseHTMLEngineProvider({textSelectable = false, children, enableExperim
contentModel: HTMLContentModel.block,
}),
}),
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16],
[styles.colorMuted, styles.formError, styles.mb0, styles.textLabelSupporting, styles.lh16, styles.headerText],
);
/* eslint-enable @typescript-eslint/naming-convention */

// We need to memoize this prop to make it referentially stable.
const defaultTextProps: TextProps = useMemo(() => ({selectable: textSelectable, allowFontScaling: false, textBreakStrategy: 'simple'}), [textSelectable]);
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText]};
const defaultViewProps = {style: [styles.alignItemsStart, styles.userSelectText, styles.mw100]};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

To make the header text truncate.

return (
<TRenderEngineProvider
customHTMLElementModels={customHTMLElementModels}
Expand Down
54 changes: 48 additions & 6 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import lodashEscape from 'lodash/escape';
import lodashFindLastIndex from 'lodash/findLastIndex';
import lodashIntersection from 'lodash/intersection';
import lodashIsEqual from 'lodash/isEqual';
import lodashUnescape from 'lodash/unescape';
import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import type {ValueOf} from 'type-fest';
Expand Down Expand Up @@ -68,6 +69,7 @@ import * as PolicyUtils from './PolicyUtils';
import type {LastVisibleMessage} from './ReportActionsUtils';
import * as ReportActionsUtils from './ReportActionsUtils';
import shouldAllowRawHTMLMessages from './shouldAllowRawHTMLMessages';
import StringUtils from './StringUtils';
import * as TransactionUtils from './TransactionUtils';
import * as Url from './Url';
import * as UserUtils from './UserUtils';
Expand Down Expand Up @@ -2546,10 +2548,54 @@ function getAdminRoomInvitedParticipants(parentReportAction: ReportAction | Reco
return roomName ? `${verb} ${users} ${preposition} ${roomName}` : `${verb} ${users}`;
}

/**
* Get the formatted title in HTML for a thread based on parent message.
* Only the first line of the message should display.
*/
function getThreadReportNameHtml(reportActionMessageHtml: string): string {
const blockTags = ['br', 'h1', 'pre', 'div', 'blockquote', 'p', 'li', 'div'];
const blockTagRegExp = `(?:<\\/?(?:${blockTags.join('|')})(?:[^>]*)>|\\r\\n|\\n|\\r)`;
const threadHeaderHtmlRegExp = new RegExp(`^(?:<([^>]+)>)?((?:(?!${blockTagRegExp}).)*)(${blockTagRegExp}.*)`, 'gmi');
return reportActionMessageHtml.replace(threadHeaderHtmlRegExp, (match, g1: string, g2: string) => {
if (!g1 || g1 === 'h1') {
return g2;
}
if (g1 === 'pre') {
return `<code>${g2}</code>`;
}
const parser = new ExpensiMark();
if (parser.containsNonPairTag(g2)) {
return `<${g1}>${g2}`;
}
return `<${g1}>${g2}</${g1}>`;
});
}

/**
* Get the title for a thread based on parent message.
* If render in html, only the first line of the message should display.
*/
function getThreadReportName(parentReportAction: OnyxEntry<ReportAction> | EmptyObject = {}, shouldRenderAsHTML = false, shouldRenderFirstLineOnly = true): string {
if (ReportActionsUtils.isApprovedOrSubmittedReportAction(parentReportAction)) {
return ReportActionsUtils.getReportActionMessageText(parentReportAction).replace(/(\r\n|\n|\r)/gm, ' ');
}
if (!shouldRenderAsHTML && !shouldRenderFirstLineOnly) {
return (parentReportAction?.message?.[0]?.text ?? '').replace(/(\r\n|\n|\r)/gm, ' ');
}

const threadReportNameHtml = getThreadReportNameHtml(parentReportAction?.message?.[0]?.html ?? '');

if (!shouldRenderAsHTML && shouldRenderFirstLineOnly) {
return lodashUnescape(Str.stripHTML(threadReportNameHtml));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unescape html characters to avoid the cases of < turned into &lt as we've found earlier.

}

return StringUtils.containsHtml(threadReportNameHtml) ? `<thread-title>${threadReportNameHtml}</thread-title>` : lodashUnescape(threadReportNameHtml);
}

/**
* Get the title for a report.
*/
function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = null): string {
function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = null, shouldRenderAsHTML = false, shouldRenderFirstLineOnly = false): string {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

shouldRenderFirstLineOnly is used for LHN where we don't want HTML and only the first line of text.

let formattedName: string | undefined;
const parentReportAction = ReportActionsUtils.getParentReportAction(report);
if (isChatThread(report)) {
Expand All @@ -2562,11 +2608,7 @@ function getReportName(report: OnyxEntry<Report>, policy: OnyxEntry<Policy> = nu
}

const isAttachment = ReportActionsUtils.isReportActionAttachment(!isEmptyObject(parentReportAction) ? parentReportAction : null);
const parentReportActionMessage = (
ReportActionsUtils.isApprovedOrSubmittedReportAction(parentReportAction)
? ReportActionsUtils.getReportActionMessageText(parentReportAction)
: parentReportAction?.message?.[0]?.text ?? ''
).replace(/(\r\n|\n|\r)/gm, ' ');
const parentReportActionMessage = getThreadReportName(parentReportAction, shouldRenderAsHTML, shouldRenderFirstLineOnly);
if (isAttachment && parentReportActionMessage) {
return `[${Localize.translateLocal('common.attachment')}]`;
}
Expand Down
2 changes: 1 addition & 1 deletion src/libs/SidebarUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ function getOptionData({
result.phoneNumber = personalDetail?.phoneNumber;
}

const reportName = ReportUtils.getReportName(report, policy);
const reportName = ReportUtils.getReportName(report, policy, false, true);

result.text = reportName;
result.subtitle = subtitle;
Expand Down
11 changes: 10 additions & 1 deletion src/libs/StringUtils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import _ from 'lodash';
import CONST from '@src/CONST';

/**
* Check if the text contains HTML
* @param text
* @return whether the text contains HTML
*/
function containsHtml(text: string): boolean {
return /<\/?[a-z][\s\S]*>/i.test(text);
}

/**
* Removes diacritical marks and non-alphabetic and non-latin characters from a string.
* @param str - The input string to be sanitized.
Expand Down Expand Up @@ -72,4 +81,4 @@ function normalizeCRLF(value?: string): string | undefined {
return value?.replace(/\r\n/g, '\n');
}

export default {sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF};
export default {containsHtml, sanitizeString, isEmptyString, removeInvisibleCharacters, normalizeCRLF};
2 changes: 1 addition & 1 deletion src/pages/home/HeaderView.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ function HeaderView(props) {
const isTaskReport = ReportUtils.isTaskReport(props.report);
const reportHeaderData = !isTaskReport && !isChatThread && props.report.parentReportID ? props.parentReport : props.report;
// Use sorted display names for the title for group chats on native small screen widths
const title = ReportUtils.isGroupChat(props.report) ? getGroupChatName(props.report) : ReportUtils.getReportName(reportHeaderData);
const title = ReportUtils.isGroupChat(props.report) ? getGroupChatName(props.report) : ReportUtils.getReportName(reportHeaderData, undefined, true);
const subtitle = ReportUtils.getChatRoomSubtitle(reportHeaderData);
const parentNavigationSubtitleData = ReportUtils.getParentNavigationSubtitle(reportHeaderData);
const isConcierge = ReportUtils.hasSingleParticipant(props.report) && _.contains(participants, CONST.ACCOUNT_ID.CONCIERGE);
Expand Down
5 changes: 5 additions & 0 deletions src/styles/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1883,6 +1883,11 @@ const styles = (theme: ThemeColors) =>
...wordBreak.breakWord,
},

renderHTMLThreadTitle: {
display: 'flex',
flexDirection: 'row',
},

renderHTMLTitle: {
color: theme.text,
fontSize: variables.fontSizeNormal,
Expand Down
Loading