From 6ed8a56be4057d71f6749d99bbbe94c27f52f2de Mon Sep 17 00:00:00 2001 From: Rahim Rahman Date: Wed, 8 Oct 2025 08:24:10 -0600 Subject: [PATCH] fix(MM-63466): hashtag displaying in smaller font size (#9193) --- app/components/markdown/markdown.test.tsx | 116 +++++++++++++++++++++- app/components/markdown/markdown.tsx | 38 ++++--- 2 files changed, 141 insertions(+), 13 deletions(-) diff --git a/app/components/markdown/markdown.test.tsx b/app/components/markdown/markdown.test.tsx index db78336fa..673c67882 100644 --- a/app/components/markdown/markdown.test.tsx +++ b/app/components/markdown/markdown.test.tsx @@ -5,15 +5,22 @@ import {screen} from '@testing-library/react-native'; import * as CommonMark from 'commonmark'; const {Node} = CommonMark; import React from 'react'; +import {StyleSheet} from 'react-native'; import {Preferences} from '@constants'; import {renderWithIntl} from '@test/intl-test-helper'; +import {getMarkdownTextStyles} from '@utils/markdown'; +import {typography} from '@utils/typography'; -import Markdown from './markdown'; +import Markdown, {testExports} from './markdown'; import * as Transforms from './transform'; const Parser = jest.requireActual('commonmark').Parser; +jest.mock('@screens/navigation', () => ({ + dismissAllModalsAndPopToRoot: jest.fn(), +})); + describe('Markdown', () => { const baseProps: React.ComponentProps = { baseTextStyle: {}, @@ -137,4 +144,111 @@ describe('Markdown', () => { expect(screen.getByText('An error occurred while rendering this text')).toBeVisible(); }); }); + + describe('renderHashtagWithStyles', () => { + const {renderHashtagWithStyles} = testExports; + + test('should render hashtag with correct text and link styling', () => { + const baseTextStyle = { + color: '#000000', + ...typography('Body', 200), + }; + const textStyles = getMarkdownTextStyles(baseProps.theme); + const context: string[] = []; + + renderWithIntl( + renderHashtagWithStyles(context, 'test', textStyles, baseTextStyle), + ); + + const hashtagElement = screen.getByText('#test'); + expect(hashtagElement).toBeVisible(); + + const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style); + expect(flattenedStyle).toMatchObject({ + fontSize: 16, + color: baseProps.theme.linkColor, + }); + }); + + test('should render hashtag in heading with correct heading font size', () => { + const baseTextStyle = { + color: '#000000', + ...typography('Body', 200), + }; + const textStyles = getMarkdownTextStyles(baseProps.theme); + const context: string[] = ['heading1']; + + renderWithIntl( + renderHashtagWithStyles(context, 'hashtag', textStyles, baseTextStyle), + ); + + const hashtagElement = screen.getByText('#hashtag'); + expect(hashtagElement).toBeVisible(); + + const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style); + expect(flattenedStyle).toMatchObject({ + fontSize: 28, + }); + }); + + test('should apply heading2 styles when in heading2 context', () => { + const baseTextStyle = { + color: '#000000', + ...typography('Body', 200), + }; + const textStyles = getMarkdownTextStyles(baseProps.theme); + const context: string[] = ['heading2']; + + renderWithIntl( + renderHashtagWithStyles(context, 'heading2tag', textStyles, baseTextStyle), + ); + + const hashtagElement = screen.getByText('#heading2tag'); + expect(hashtagElement).toBeVisible(); + + const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style); + expect(flattenedStyle).toMatchObject({ + fontSize: 25, + }); + }); + + test('should have clickable onPress handler', () => { + const baseTextStyle = { + color: '#000000', + ...typography('Body', 200), + }; + const textStyles = getMarkdownTextStyles(baseProps.theme); + const context: string[] = []; + + renderWithIntl( + renderHashtagWithStyles(context, 'clickable', textStyles, baseTextStyle), + ); + + const hashtagElement = screen.getByText('#clickable'); + expect(hashtagElement.props.onPress).toBeDefined(); + }); + + test('should include both base text style and link color', () => { + const baseTextStyle = { + color: '#000000', + ...typography('Body', 200), + fontFamily: 'OpenSans', + }; + const textStyles = getMarkdownTextStyles(baseProps.theme); + const context: string[] = []; + + renderWithIntl( + renderHashtagWithStyles(context, 'styled', textStyles, baseTextStyle), + ); + + const hashtagElement = screen.getByText('#styled'); + const flattenedStyle = StyleSheet.flatten(hashtagElement.props.style); + + expect(flattenedStyle).toMatchObject({ + fontSize: 16, + fontFamily: 'OpenSans', + color: baseProps.theme.linkColor, + }); + }); + }); }); diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index 614c157a6..757f37b33 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -124,6 +124,27 @@ const getExtraPropsForNode = (node: any) => { return extraProps; }; +const renderHashtagWithStyles = ( + context: string[], + hashtag: string, + textStyles: MarkdownTextStyles, + baseTextStyle: StyleProp, +) => { + const computedStyle = computeTextStyle(textStyles, baseTextStyle, context); + const linkStyle = [computedStyle, textStyles.link]; + const headingIndex = context.findIndex((c) => c.includes('heading')); + if (headingIndex > -1) { + linkStyle.push(textStyles[context[headingIndex]]); + } + + return ( + + ); +}; + const Markdown = ({ autolinkedUrlSchemes, baseTextStyle, blockStyles, channelId, channelMentions, disableAtChannelMentionHighlight, disableAtMentions, disableBlockQuote, disableChannelLink, @@ -277,18 +298,7 @@ const Markdown = ({ return renderText({context, literal: `#${hashtag}`}); } - const linkStyle = [textStyles.link]; - const headingIndex = context.findIndex((c) => c.includes('heading')); - if (headingIndex > -1) { - linkStyle.push(textStyles[context[headingIndex]]); - } - - return ( - - ); + return renderHashtagWithStyles(context, hashtag, textStyles, baseTextStyle); }; const renderHeading = ({children, level}: {children: ReactElement; level: string}) => { @@ -662,4 +672,8 @@ const Markdown = ({ return output; }; +export const testExports = { + renderHashtagWithStyles, +}; + export default Markdown;