From 1d8568ade8c011cf22d92fce01e7c2d8ebbb727c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Espino=20Garc=C3=ADa?= Date: Tue, 4 Nov 2025 10:05:56 +0100 Subject: [PATCH] Improve markdown code (#9164) * Improve markdown code * Add missing changes * Simplify style composition * Address feedback * Fix color issue introduced by channel banner --- .../announcement_banner.tsx | 3 - .../expanded_announcement_banner.tsx | 3 - .../draft_and_scheduled_post_message.tsx | 8 - app/components/files/image_file.tsx | 1 + app/components/files/video_file.tsx | 100 ++--- .../formatted_markdown_text/index.tsx | 30 +- .../markdown/at_mention/at_mention.tsx | 11 +- .../channel_mention/channel_mention.tsx | 7 +- app/components/markdown/markdown.tsx | 384 ++++++++++-------- .../markdown/markdown_code_block/index.tsx | 5 +- .../markdown/markdown_image/index.tsx | 45 +- .../markdown/markdown_link/markdown_link.tsx | 57 +-- .../markdown/markdown_table_cell/index.tsx | 5 +- .../markdown/markdown_table_image/index.tsx | 30 +- .../markdown/markdown_table_row/index.tsx | 5 +- .../combined_user_activity.tsx | 5 +- .../combined_user_activity/last_users.tsx | 3 - .../post/body/add_members/add_members.tsx | 2 + .../content/embedded_bindings/embed_text.tsx | 5 - .../content/embedded_bindings/embed_title.tsx | 6 - .../content/image_preview/image_preview.tsx | 7 + .../message_attachments/attachment_fields.tsx | 9 +- .../attachment_image/index.tsx | 6 +- .../attachment_pretext.tsx | 27 +- .../message_attachments/attachment_text.tsx | 15 +- .../message_attachments/attachment_title.tsx | 10 +- .../message_attachment.tsx | 9 - .../permalink_preview/permalink_preview.tsx | 13 +- app/components/post_list/post/body/index.tsx | 8 +- .../post_list/post/body/message/message.tsx | 22 +- .../post/system_message/system_message.tsx | 11 +- app/components/progressive_image/index.tsx | 19 +- .../remove_markdown/at_mention/at_mention.tsx | 5 +- app/components/remove_markdown/index.tsx | 70 ++-- app/components/section_notice/index.tsx | 3 - app/components/settings/footer.tsx | 9 - .../components/status_update_post/index.tsx | 17 +- .../participants/participants.tsx | 5 +- .../screens/playbook_run/playbook_run.tsx | 3 - app/screens/apps_form/apps_form_component.tsx | 5 +- app/screens/apps_form/apps_form_field.tsx | 6 +- .../header/channel_banner/channel_banner.tsx | 17 - app/screens/channel_info/extra/extra.tsx | 5 - .../threads_list/thread/thread.tsx | 3 - .../dialog_introduction_text.tsx | 5 - app/screens/permalink/permalink_error.tsx | 3 - .../terms_of_service/terms_of_service.tsx | 9 +- app/utils/markdown/index.test.ts | 8 +- app/utils/markdown/index.ts | 4 +- app/utils/theme/index.test.ts | 10 - app/utils/theme/index.ts | 4 - 51 files changed, 539 insertions(+), 523 deletions(-) diff --git a/app/components/announcement_banner/announcement_banner.tsx b/app/components/announcement_banner/announcement_banner.tsx index 918d01fa3..892d4e633 100644 --- a/app/components/announcement_banner/announcement_banner.tsx +++ b/app/components/announcement_banner/announcement_banner.tsx @@ -18,7 +18,6 @@ import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {bottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; -import {getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -87,7 +86,6 @@ const AnnouncementBanner = ({ const theme = useTheme(); const [visible, setVisible] = useState(false); const style = getStyle(theme); - const markdownTextStyles = getMarkdownTextStyles(theme); const renderContent = useCallback(() => ( diff --git a/app/components/announcement_banner/expanded_announcement_banner.tsx b/app/components/announcement_banner/expanded_announcement_banner.tsx index 439e33bd1..6607b0478 100644 --- a/app/components/announcement_banner/expanded_announcement_banner.tsx +++ b/app/components/announcement_banner/expanded_announcement_banner.tsx @@ -16,7 +16,6 @@ import {useTheme} from '@context/theme'; import {useBottomSheetListsFix} from '@hooks/bottom_sheet_lists_fix'; import {useIsTablet} from '@hooks/device'; import {dismissBottomSheet} from '@screens/navigation'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown'; import {makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -97,9 +96,7 @@ const ExpandedAnnouncementBanner = ({ > { @@ -52,8 +49,6 @@ const DraftAndScheduledPostMessage: React.FC = ({ }) => { const theme = useTheme(); const style = getStyleSheet(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); const [height, setHeight] = useState(); const [open, setOpen] = useState(false); const dimensions = useWindowDimensions(); @@ -81,14 +76,11 @@ const DraftAndScheduledPostMessage: React.FC = ({ > diff --git a/app/components/files/image_file.tsx b/app/components/files/image_file.tsx index a1364bb03..87add0988 100644 --- a/app/components/files/image_file.tsx +++ b/app/components/files/image_file.tsx @@ -126,6 +126,7 @@ const ImageFile = ({ tintDefaultSource={!file.localPath && !failed} onError={handleError} contentFit={contentFit} + theme={theme} {...imageProps} /> ); diff --git a/app/components/files/video_file.tsx b/app/components/files/video_file.tsx index 2ebab49cf..ef0da7fd5 100644 --- a/app/components/files/video_file.tsx +++ b/app/components/files/video_file.tsx @@ -80,51 +80,6 @@ const VideoFile = ({ return undefined; }, [dimensions.height, dimensions.width, video.height, video.width, wrapperWidth, isSingleImage]); - const getThumbnail = async () => { - const data = {...file}; - try { - const exists = data.mini_preview ? await fileExists(data.mini_preview) : false; - if (!data.mini_preview || !exists) { - const videoUrl = buildFileUrl(serverUrl, data.id!); - if (videoUrl) { - const cred = await getServerCredentials(serverUrl); - const headers: Record = {}; - if (cred?.token) { - headers.Authorization = `Bearer ${cred.token}`; - } - const {uri, height, width} = await getThumbnailAsync(data.localPath || videoUrl, {time: 1000, headers}); - data.mini_preview = uri; - data.height = height; - data.width = width; - updateLocalFile(serverUrl, data); - if (mounted.current) { - setVideo(data); - setFailed(false); - } - } - } - } catch (error) { - data.mini_preview = buildFilePreviewUrl(serverUrl, data.id!); - if (mounted.current) { - setVideo(data); - } - } finally { - if (!data.width) { - data.height = wrapperWidth; - data.width = wrapperWidth; - } - const {width: tw, height: th} = calculateDimensions( - data.height, - data.width, - dimensions.width - 60, // size of the gallery header probably best to set that as a constant - dimensions.height, - ); - data.height = th; - data.width = tw; - updateFileForGallery?.(index, data); - } - }; - const handleError = useCallback(() => { setFailed(true); }, []); @@ -137,9 +92,59 @@ const VideoFile = ({ }, []); useEffect(() => { - if (inViewPort) { - getThumbnail(); + if (!inViewPort) { + return; } + + const getThumbnail = async () => { + const data = {...file}; + try { + const exists = data.mini_preview ? await fileExists(data.mini_preview) : false; + if (!data.mini_preview || !exists) { + const videoUrl = buildFileUrl(serverUrl, data.id!); + if (videoUrl) { + const cred = await getServerCredentials(serverUrl); + const headers: Record = {}; + if (cred?.token) { + headers.Authorization = `Bearer ${cred.token}`; + } + const {uri, height, width} = await getThumbnailAsync(data.localPath || videoUrl, {time: 1000, headers}); + data.mini_preview = uri; + data.height = height; + data.width = width; + updateLocalFile(serverUrl, data); + if (mounted.current) { + setVideo(data); + setFailed(false); + } + } + } + } catch (error) { + data.mini_preview = buildFilePreviewUrl(serverUrl, data.id!); + if (mounted.current) { + setVideo(data); + } + } finally { + if (!data.width) { + data.height = wrapperWidth; + data.width = wrapperWidth; + } + const {width: tw, height: th} = calculateDimensions( + data.height, + data.width, + dimensions.width - 60, // size of the gallery header probably best to set that as a constant + dimensions.height, + ); + data.height = th; + data.width = tw; + updateFileForGallery?.(index, data); + } + }; + + getThumbnail(); + + // Only get the thumbnail when the file changes or the file gets into the viewport + // eslint-disable-next-line react-hooks/exhaustive-deps }, [file, inViewPort]); const imageProps = () => { @@ -158,6 +163,7 @@ const VideoFile = ({ style={[isSingleImage ? null : style.imagePreview, imageDimensions]} onError={handleError} contentFit={contentFit} + theme={theme} {...imageProps()} /> ); diff --git a/app/components/formatted_markdown_text/index.tsx b/app/components/formatted_markdown_text/index.tsx index 94b6eb946..ae7275ada 100644 --- a/app/components/formatted_markdown_text/index.tsx +++ b/app/components/formatted_markdown_text/index.tsx @@ -5,14 +5,14 @@ import {Parser} from 'commonmark'; import Renderer from 'commonmark-react-renderer'; import React, {type ReactElement, useRef} from 'react'; import {useIntl} from 'react-intl'; -import {type GestureResponderEvent, type StyleProp, Text, type TextStyle, type ViewStyle} from 'react-native'; +import {type StyleProp, Text, type TextStyle, type ViewStyle} from 'react-native'; import AtMention from '@components/markdown/at_mention'; import MarkdownLink from '@components/markdown/markdown_link'; import {useTheme} from '@context/theme'; import {logWarning} from '@utils/log'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; -import {concatStyles, changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; +import {computeTextStyle, getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import type {AvailableScreens} from '@typings/screens/navigation'; import type {PrimitiveType} from 'intl-messageformat'; @@ -23,7 +23,6 @@ type Props = { defaultMessage: string; id: string; location: AvailableScreens; - onPostPress?: (e: GestureResponderEvent) => void; style?: StyleProp; values?: Record; }; @@ -48,7 +47,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, location, onPostPress, style, values}: Props) => { +const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, location, style, values}: Props) => { const intl = useIntl(); const theme = useTheme(); const styles = getStyleSheet(theme); @@ -78,10 +77,6 @@ const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, lo }); }; - const computeTextStyle = (base: StyleProp, context: string[]) => { - return concatStyles(base, context.map((type) => (txtStyles as {[s: string]: TextStyle})[type])); - }; - const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => { return ( ); }; @@ -100,7 +95,7 @@ const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, lo }; const renderCodeSpan = ({context, literal}: {context: string[]; literal: string}) => { - const computed = computeTextStyle([styles.message, txtStyles.code], context); + const computed = computeTextStyle(txtStyles, [styles.message, txtStyles.code], context); return {literal}; }; @@ -111,7 +106,14 @@ const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, lo const renderLink = ({children, href}: {children: ReactElement; href: string}) => { const url = href[0] === TARGET_BLANK_URL_PREFIX ? href.substring(1, href.length) : href; - return {children}; + return ( + + {children} + + ); }; const renderParagraph = ({children, first}: {children: ReactElement; first: boolean}) => { @@ -129,7 +131,7 @@ const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, lo }; const renderText = ({context, literal}: {context: string[]; literal: string}) => { - const computed = computeTextStyle(style || styles.message, context); + const computed = computeTextStyle(txtStyles, style || styles.message, context); return {literal}; }; diff --git a/app/components/markdown/at_mention/at_mention.tsx b/app/components/markdown/at_mention/at_mention.tsx index fec462ad7..5e57bde62 100644 --- a/app/components/markdown/at_mention/at_mention.tsx +++ b/app/components/markdown/at_mention/at_mention.tsx @@ -10,7 +10,6 @@ import {type GestureResponderEvent, type StyleProp, StyleSheet, Text, type TextS import {fetchUserOrGroupsByMentionsInBatch} from '@actions/remote/user'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; import GroupModel from '@database/models/server/group'; import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown'; import {bottomSheet, dismissBottomSheet, openUserProfileModal} from '@screens/navigation'; @@ -30,12 +29,12 @@ type AtMentionProps = { mentionKeys?: Array<{key: string }>; mentionName: string; mentionStyle: StyleProp; - onPostPress?: (e: GestureResponderEvent) => void; teammateNameDisplay: string; textStyle?: StyleProp; users: UserModelType[]; groups: GroupModel[]; groupMemberships: GroupMembershipModel[]; + theme: Theme; } const style = StyleSheet.create({ @@ -51,16 +50,15 @@ const AtMention = ({ mentionName, mentionKeys, mentionStyle, - onPostPress, teammateNameDisplay, textStyle, users, groups, groupMemberships, + theme, }: AtMentionProps) => { const intl = useIntl(); const managedConfig = useManagedConfig(); - const theme = useTheme(); const serverUrl = useServerUrl(); const user = useMemoMentionedUser(users, mentionName); @@ -88,6 +86,9 @@ const AtMention = ({ if (!user?.username && !group?.name) { fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName); } + + // Only fetch the user or group on mount + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const openUserProfile = () => { @@ -192,7 +193,7 @@ const AtMention = ({ if (canPress) { onLongPress = handleLongPress; - onPress = (isSearchResult ? onPostPress : openUserProfile); + onPress = (isSearchResult ? undefined : openUserProfile); } if (suffix) { diff --git a/app/components/markdown/channel_mention/channel_mention.tsx b/app/components/markdown/channel_mention/channel_mention.tsx index 18e59306b..27011dc37 100644 --- a/app/components/markdown/channel_mention/channel_mention.tsx +++ b/app/components/markdown/channel_mention/channel_mention.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {defineMessage, useIntl} from 'react-intl'; import {type StyleProp, Text, type TextStyle} from 'react-native'; @@ -87,7 +87,10 @@ const ChannelMention = ({ }: ChannelMentionProps) => { const intl = useIntl(); const serverUrl = useServerUrl(); - const channel = getChannelFromChannelName(channelName, channels, channelMentions, team.name); + const channel = useMemo( + () => getChannelFromChannelName(channelName, channels, channelMentions, team.name), + [channelMentions, channelName, channels, team.name], + ); const handlePress = usePreventDoubleTap(useCallback((async () => { let c = channel; diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx index 757f37b33..02160219e 100644 --- a/app/components/markdown/markdown.tsx +++ b/app/components/markdown/markdown.tsx @@ -5,18 +5,17 @@ import {useManagedConfig} from '@mattermost/react-native-emm'; import {Parser, Node} from 'commonmark'; import Renderer from 'commonmark-react-renderer'; -import React, {type ReactElement, useMemo, useRef} from 'react'; -import {Dimensions, type GestureResponderEvent, type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native'; +import React, {type ReactElement, useCallback, useMemo, useRef} from 'react'; +import {Dimensions, type StyleProp, StyleSheet, Text, type TextStyle, View, type ViewStyle} from 'react-native'; import CompassIcon from '@components/compass_icon'; import EditedIndicator from '@components/edited_indicator'; import Emoji from '@components/emoji'; import FormattedText from '@components/formatted_text'; import {logError} from '@utils/log'; -import {computeTextStyle} from '@utils/markdown'; -import {changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; +import {computeTextStyle, getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; -import {getScheme} from '@utils/url'; import AtMention from './at_mention'; import ChannelMention from './channel_mention'; @@ -43,10 +42,8 @@ import type { import type {AvailableScreens} from '@typings/screens/navigation'; type MarkdownProps = { - autolinkedUrlSchemes?: string[]; baseTextStyle: StyleProp; baseParagraphStyle?: StyleProp; - blockStyles?: MarkdownBlockStyles; channelId?: string; channelMentions?: ChannelMentions; disableAtChannelMentionHighlight?: boolean; @@ -72,10 +69,8 @@ type MarkdownProps = { maxNodes: number; mentionKeys?: UserMentionKey[]; minimumHashtagLength?: number; - onPostPress?: (event: GestureResponderEvent) => void; postId?: string; searchPatterns?: SearchPattern[]; - textStyles?: MarkdownTextStyles; theme: Theme; value?: string; onLinkLongPress?: (url?: string) => void; @@ -146,23 +141,82 @@ const renderHashtagWithStyles = ( }; const Markdown = ({ - autolinkedUrlSchemes, baseTextStyle, blockStyles, channelId, channelMentions, - disableAtChannelMentionHighlight, disableAtMentions, disableBlockQuote, disableChannelLink, - disableCodeBlock, disableGallery, disableHashtags, disableHeading, disableTables, - enableInlineLatex, enableLatex, maxNodes, - imagesMetadata, isEdited, isReplyPost, isSearchResult, layoutHeight, layoutWidth, - location, mentionKeys, highlightKeys, minimumHashtagLength = 3, onPostPress, postId, searchPatterns, - textStyles = {}, theme, value = '', baseParagraphStyle, onLinkLongPress, isUnsafeLinksPost, + baseTextStyle, + channelId, + channelMentions, + disableAtChannelMentionHighlight, + disableAtMentions, + disableBlockQuote, + disableChannelLink, + disableCodeBlock, + disableGallery, + disableHashtags, + disableHeading, + disableTables, + enableInlineLatex, + enableLatex, + maxNodes, + imagesMetadata, + isEdited, + isReplyPost, + isSearchResult, + layoutHeight, + layoutWidth, + location, + mentionKeys, + highlightKeys, + minimumHashtagLength = 3, + postId, + searchPatterns, + theme, + value = '', + baseParagraphStyle, + onLinkLongPress, + isUnsafeLinksPost, }: MarkdownProps) => { const style = getStyleSheet(theme); + const blockStyles = useMemo(() => getMarkdownBlockStyles(theme), [theme]); + const textStyles = useMemo(() => getMarkdownTextStyles(theme), [theme]); const managedConfig = useManagedConfig(); - const urlFilter = (url: string) => { - const scheme = getScheme(url); - return !scheme || autolinkedUrlSchemes?.indexOf(scheme) !== -1; - }; + const renderText = useCallback(({context, literal}: MarkdownBaseRenderer) => { + const selectable = (managedConfig.copyAndPasteProtection !== 'true') && context.includes('table_cell'); + if (context.indexOf('image') !== -1) { + // If this text is displayed, it will be styled by the image component + return ( + + {literal} + + ); + } - const renderAtMention = ({context, mentionName}: MarkdownAtMentionRenderer) => { + // Construct the text style based off of the parents of this node since RN's inheritance is limited + let styles: StyleProp; + if (disableHeading) { + styles = computeTextStyle(textStyles, baseTextStyle, context.filter((c) => !c.startsWith('heading'))); + } else { + styles = computeTextStyle(textStyles, baseTextStyle, context); + } + + if (context.includes('mention_highlight')) { + styles = [styles, {backgroundColor: theme.mentionHighlightBg}]; + } + + return ( + + {literal} + + ); + }, [baseTextStyle, disableHeading, managedConfig.copyAndPasteProtection, textStyles, theme.mentionHighlightBg]); + + const renderAtMention = useCallback(({context, mentionName}: MarkdownAtMentionRenderer) => { if (disableAtMentions) { return renderText({context, literal: `@${mentionName}`}); } @@ -178,13 +232,13 @@ const Markdown = ({ isSearchResult={isSearchResult} location={location} mentionName={mentionName} - onPostPress={onPostPress} mentionKeys={mentionKeys} + theme={theme} /> ); - }; + }, [baseTextStyle, channelId, disableAtChannelMentionHighlight, disableAtMentions, isSearchResult, location, mentionKeys, renderText, style.atMentionOpacity, textStyles, theme]); - const renderBlockQuote = ({children, ...otherProps}: any) => { + const renderBlockQuote = useCallback(({children, ...otherProps}: any) => { if (disableBlockQuote) { return null; } @@ -197,13 +251,13 @@ const Markdown = ({ {children} ); - }; + }, [disableBlockQuote, blockStyles?.quoteBlockIcon]); const renderBreak = () => { return {'\n'}; }; - const renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => { + const renderChannelLink = useCallback(({context, channelName}: MarkdownChannelMentionRenderer) => { if (disableChannelLink || isUnsafeLinksPost) { return renderText({context, literal: `~${channelName}`}); } @@ -216,9 +270,9 @@ const Markdown = ({ channelMentions={channelMentions} /> ); - }; + }, [baseTextStyle, channelMentions, disableChannelLink, isUnsafeLinksPost, renderText, textStyles]); - const renderCheckbox = ({isChecked}: {isChecked: boolean}) => { + const renderCheckbox = useCallback(({isChecked}: {isChecked: boolean}) => { return ( ); - }; + }, [theme.centerChannelColor]); - const renderCodeBlock = (props: any) => { + const renderCodeBlock = useCallback((props: any) => { if (disableCodeBlock) { return null; } @@ -253,11 +307,12 @@ const Markdown = ({ content={content} language={props.language} textStyle={textStyles.codeBlock} + theme={theme} /> ); - }; + }, [disableCodeBlock, enableLatex, isUnsafeLinksPost, textStyles.codeBlock, theme]); - const renderCodeSpan = ({context, literal}: MarkdownBaseRenderer) => { + const renderCodeSpan = useCallback(({context, literal}: MarkdownBaseRenderer) => { const {code} = textStyles; return ( ); - }; + }, [baseTextStyle, textStyles]); - const renderEditedIndicator = ({context}: {context: string[]}) => { + const renderEditedIndicator = useCallback(({context}: {context: string[]}) => { return ( ); - }; + }, [baseTextStyle, theme]); - const renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => { + const renderEmoji = useCallback(({context, emojiName, literal}: MarkdownEmojiRenderer) => { return ( ); - }; + }, [baseTextStyle, textStyles]); - const renderHashtag = ({context, hashtag}: {context: string[]; hashtag: string}) => { + const renderHashtag = useCallback(({context, hashtag}: {context: string[]; hashtag: string}) => { if (disableHashtags || isUnsafeLinksPost) { return renderText({context, literal: `#${hashtag}`}); } return renderHashtagWithStyles(context, hashtag, textStyles, baseTextStyle); - }; + }, [baseTextStyle, disableHashtags, isUnsafeLinksPost, renderText, textStyles]); - const renderHeading = ({children, level}: {children: ReactElement; level: string}) => { + const renderHeading = useCallback(({children, level}: {children: ReactElement; level: string}) => { if (disableHeading) { return ( ); - }; + }, [disableHeading, style.block, style.bold, textStyles]); - const renderHtml = (props: any) => { + const renderHtml = useCallback((props: any) => { let rendered = renderText(props); if (props.isBlock) { @@ -346,9 +401,9 @@ const Markdown = ({ } return rendered; - }; + }, [renderText, style.block]); - const renderImage = ({linkDestination, context, src, size}: MarkdownImageRenderer) => { + const renderImage = useCallback(({linkDestination, context, src, size}: MarkdownImageRenderer) => { if (!imagesMetadata || isUnsafeLinksPost) { return null; } @@ -366,6 +421,7 @@ const Markdown = ({ location={location} postId={postId!} source={src} + theme={theme} /> ); } @@ -383,11 +439,12 @@ const Markdown = ({ postId={postId!} source={src} sourceSize={size} + theme={theme} /> ); - }; + }, [baseTextStyle, disableGallery, imagesMetadata, isReplyPost, isUnsafeLinksPost, layoutHeight, layoutWidth, location, postId, textStyles, theme]); - const renderLatexInline = ({context, latexCode}: MarkdownLatexRenderer) => { + const renderLatexInline = useCallback(({context, latexCode}: MarkdownLatexRenderer) => { if (!enableInlineLatex || isUnsafeLinksPost) { return renderText({context, literal: `$${latexCode}$`}); } @@ -401,9 +458,9 @@ const Markdown = ({ /> ); - }; + }, [enableInlineLatex, isUnsafeLinksPost, renderText, theme]); - const renderLink = ({children, href}: {children: ReactElement; href: string}) => { + const renderLink = useCallback(({children, href}: {children: ReactElement; href: string}) => { if (isUnsafeLinksPost) { return renderText({context: [], literal: href}); } @@ -412,13 +469,14 @@ const Markdown = ({ {children} ); - }; + }, [isUnsafeLinksPost, onLinkLongPress, renderText, theme]); - const renderList = ({children, start, tight, type}: any) => { + const renderList = useCallback(({children, start, tight, type}: any) => { return ( ); - }; + }, []); - const renderListItem = ({children, context, ...otherProps}: any) => { + const renderListItem = useCallback(({children, context, ...otherProps}: any) => { const level = context.filter((type: string) => type === 'list').length; return ( @@ -442,9 +500,9 @@ const Markdown = ({ {children} ); - }; + }, [baseTextStyle]); - const renderParagraph = ({children, first}: {children: ReactElement[]; first: boolean}) => { + const renderParagraph = useCallback(({children, first}: {children: ReactElement[]; first: boolean}) => { if (!children || children.length === 0) { return null; } @@ -464,9 +522,9 @@ const Markdown = ({ ); - }; + }, [baseParagraphStyle, blockStyles?.adjacentParagraph, style.block]); - const renderTable = ({children, numColumns}: {children: ReactElement; numColumns: number}) => { + const renderTable = useCallback(({children, numColumns}: {children: ReactElement; numColumns: number}) => { if (disableTables) { return null; } @@ -478,63 +536,36 @@ const Markdown = ({ {children} ); - }; - - const renderTableCell = (args: MarkdownTableCellProps) => { - return ; - }; - - const renderTableRow = (args: MarkdownTableRowProps) => { - return ; - }; - - const renderText = ({context, literal}: MarkdownBaseRenderer) => { - const selectable = (managedConfig.copyAndPasteProtection !== 'true') && context.includes('table_cell'); - if (context.indexOf('image') !== -1) { - // If this text is displayed, it will be styled by the image component - return ( - - {literal} - - ); - } - - // Construct the text style based off of the parents of this node since RN's inheritance is limited - let styles; - if (disableHeading) { - styles = computeTextStyle(textStyles, baseTextStyle, context.filter((c) => !c.startsWith('heading'))); - } else { - styles = computeTextStyle(textStyles, baseTextStyle, context); - } - - if (context.includes('mention_highlight')) { - styles = concatStyles(styles, {backgroundColor: theme.mentionHighlightBg}); - } + }, [disableTables, theme]); + const renderTableCell = useCallback((args: MarkdownTableCellProps) => { return ( - - {literal} - + ); - }; + }, [theme]); - const renderThematicBreak = () => { + const renderTableRow = useCallback((args: MarkdownTableRowProps) => { + return ( + + ); + }, [theme]); + + const renderThematicBreak = useCallback(() => { return ( ); - }; + }, [blockStyles?.horizontalRule]); - const renderMaxNodesWarning = () => { + const renderMaxNodesWarning = useCallback(() => { const styles = [baseTextStyle, style.maxNodesWarning]; return ( @@ -545,7 +576,7 @@ const Markdown = ({ testID='max_nodes_warning' /> ); - }; + }, [baseTextStyle, style.maxNodesWarning]); const createRenderer = () => { const renderers: any = { @@ -600,74 +631,107 @@ const Markdown = ({ }); }; - const parser = useRef(new Parser({urlFilter, minimumHashtagLength})).current; - const renderer = useMemo(createRenderer, [theme, textStyles]); + // Pattern suggested in https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents + const parserRef = useRef(null); + if (parserRef.current === null) { + parserRef.current = new Parser({minimumHashtagLength}); + } + const parser = parserRef.current; + + const renderer = useMemo(createRenderer, [ + renderText, + renderCodeSpan, + renderLink, + renderImage, + renderAtMention, + renderChannelLink, + renderEmoji, + renderHashtag, + renderLatexInline, + renderParagraph, + renderHeading, + renderCodeBlock, + renderBlockQuote, + renderList, + renderListItem, + renderThematicBreak, + renderHtml, + renderTable, + renderTableRow, + renderTableCell, + renderCheckbox, + renderEditedIndicator, + renderMaxNodesWarning, + maxNodes, + ]); const errorLogged = useRef(false); - let ast; - try { - ast = parser.parse(value.toString()); + const output = useMemo(() => { + let ast; + try { + ast = parser.parse(value.toString()); - ast = combineTextNodes(ast); - ast = addListItemIndices(ast); - ast = pullOutImages(ast); - ast = parseTaskLists(ast); - if (mentionKeys) { - ast = highlightMentions(ast, mentionKeys); - } - if (highlightKeys) { - ast = highlightWithoutNotification(ast, highlightKeys); - } - if (searchPatterns) { - ast = highlightSearchPatterns(ast, searchPatterns); - } - - if (isEdited) { - const editIndicatorNode = new Node('edited_indicator'); - if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { - ast.lastChild.appendChild(editIndicatorNode); - } else { - const node = new Node('paragraph'); - node.appendChild(editIndicatorNode); - - ast.appendChild(node); + ast = combineTextNodes(ast); + ast = addListItemIndices(ast); + ast = pullOutImages(ast); + ast = parseTaskLists(ast); + if (mentionKeys) { + ast = highlightMentions(ast, mentionKeys); + } + if (highlightKeys) { + ast = highlightWithoutNotification(ast, highlightKeys); + } + if (searchPatterns) { + ast = highlightSearchPatterns(ast, searchPatterns); } - } - } catch (e) { - if (!errorLogged.current) { - logError('An error occurred while parsing Markdown', e); - errorLogged.current = true; + if (isEdited) { + const editIndicatorNode = new Node('edited_indicator'); + if (ast.lastChild && ['heading', 'paragraph'].includes(ast.lastChild.type)) { + ast.lastChild.appendChild(editIndicatorNode); + } else { + const node = new Node('paragraph'); + node.appendChild(editIndicatorNode); + + ast.appendChild(node); + } + } + } catch (e) { + if (!errorLogged.current) { + logError('An error occurred while parsing Markdown', e); + + errorLogged.current = true; + } + + return ( + + ); } - return ( - - ); - } + try { + const generatedOutput = renderer.render(ast); + return generatedOutput; + } catch (e) { + if (!errorLogged.current) { + logError('An error occurred while rendering Markdown', e); - let output; - try { - output = renderer.render(ast); - } catch (e) { - if (!errorLogged.current) { - logError('An error occurred while rendering Markdown', e); + errorLogged.current = true; + } - errorLogged.current = true; + return ( + + ); } - - return ( - - ); - } + }, [highlightKeys, isEdited, mentionKeys, parser, renderer, searchPatterns, style.errorMessage, value]); return output; }; diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx index fbc749f78..cc63b99c9 100644 --- a/app/components/markdown/markdown_code_block/index.tsx +++ b/app/components/markdown/markdown_code_block/index.tsx @@ -10,7 +10,6 @@ import {Keyboard, StyleSheet, Text, type TextStyle, TouchableOpacity, View} from import FormattedText from '@components/formatted_text'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {Screens} from '@constants'; -import {useTheme} from '@context/theme'; import {usePreventDoubleTap} from '@hooks/utils'; import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation'; import {bottomSheetSnapPoint} from '@utils/helpers'; @@ -23,6 +22,7 @@ type MarkdownCodeBlockProps = { language: string; content: string; textStyle: TextStyle; + theme: Theme; }; const MAX_LINES = 4; @@ -66,10 +66,9 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBlockProps) => { +const MarkdownCodeBlock = ({language = '', content, textStyle, theme}: MarkdownCodeBlockProps) => { const intl = useIntl(); const managedConfig = useManagedConfig(); - const theme = useTheme(); const style = getStyleSheet(theme); const SyntaxHighlighter = useMemo(() => { if (!syntaxHighlighter) { diff --git a/app/components/markdown/markdown_image/index.tsx b/app/components/markdown/markdown_image/index.tsx index d34f24267..5e4116661 100644 --- a/app/components/markdown/markdown_image/index.tsx +++ b/app/components/markdown/markdown_image/index.tsx @@ -17,7 +17,6 @@ import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import TouchableWithFeedback from '@components/touchable_with_feedback'; import {GalleryInit} from '@context/gallery'; import {useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {useGalleryItem} from '@hooks/gallery'; import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; @@ -46,6 +45,7 @@ type MarkdownImageProps = { postId: string; source: string; sourceSize?: {width?: number; height?: number}; + theme: Theme; } const ANDROID_MAX_HEIGHT = 4096; @@ -71,23 +71,40 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ })); const MarkdownImage = ({ - disabled, errorTextStyle, imagesMetadata, isReplyPost = false, - layoutWidth, layoutHeight, linkDestination, location, postId, source, sourceSize, + disabled, + errorTextStyle, + imagesMetadata, + isReplyPost = false, + layoutWidth, + layoutHeight, + linkDestination, + location, + postId, + source, + sourceSize, + theme, }: MarkdownImageProps) => { const intl = useIntl(); const isTablet = useIsTablet(); - const theme = useTheme(); const style = getStyleSheet(theme); const managedConfig = useManagedConfig(); - const genericFileId = useRef(generateId('uid')).current; + + // Pattern suggested in https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents + const genericFileRef = useRef(null); + if (genericFileRef.current === null) { + genericFileRef.current = generateId('uid'); + } + const genericFileId = genericFileRef.current; + const metadata = secureGetFromRecord(imagesMetadata, removeImageProxyForKey(source)) || Object.values(imagesMetadata || {})[0]; - const [failed, setFailed] = useState(isGifTooLarge(metadata)); - const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight); + const [failed, setFailed] = useState(() => isGifTooLarge(metadata)); const serverUrl = useServerUrl(); const galleryIdentifier = `${postId}-${genericFileId}-${location}`; - const uri = source.startsWith('/') ? serverUrl + source : source; const fileInfo = useMemo(() => { + const uri = source.startsWith('/') ? serverUrl + source : source; + const originalSize = getMarkdownImageSize(isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight); + const decodedLink = safeDecodeURIComponent(uri); let filename = parseUrl(decodedLink.substr(decodedLink.lastIndexOf('/'))).pathname.replace('/', ''); let extension = metadata?.format || filename.split('.').pop(); @@ -108,7 +125,7 @@ const MarkdownImage = ({ width: originalSize.width, height: originalSize.height, } as FileInfo; - }, [originalSize, metadata]); + }, [source, serverUrl, isReplyPost, isTablet, sourceSize, metadata, layoutWidth, layoutHeight, genericFileId, postId]); const handlePreviewImage = useCallback(() => { const item: GalleryItemType = { @@ -117,7 +134,7 @@ const MarkdownImage = ({ type: 'image', }; openGalleryAtIndex(galleryIdentifier, 0, [item]); - }, [fileInfo]); + }, [fileInfo, galleryIdentifier]); const {ref, onGestureEvent, styles} = useGalleryItem( galleryIdentifier, @@ -127,6 +144,8 @@ const MarkdownImage = ({ const {height, width} = calculateDimensions(fileInfo.height, fileInfo.width, layoutWidth || getViewPortWidth(isReplyPost, isTablet)); + const progressiveImageStyle = useMemo(() => ({width, height}), [width, height]); + const handleLinkPress = useCallback(() => { if (linkDestination) { const url = normalizeProtocol(linkDestination); @@ -230,7 +249,8 @@ const MarkdownImage = ({ imageUri={fileInfo.uri} onError={handleOnError} contentFit='contain' - style={{width, height}} + style={progressiveImageStyle} + theme={theme} /> @@ -250,7 +270,8 @@ const MarkdownImage = ({ imageUri={fileInfo.uri} onError={handleOnError} contentFit='contain' - style={{width, height}} + style={progressiveImageStyle} + theme={theme} /> ); diff --git a/app/components/markdown/markdown_link/markdown_link.tsx b/app/components/markdown/markdown_link/markdown_link.tsx index bad509358..69fba5b30 100644 --- a/app/components/markdown/markdown_link/markdown_link.tsx +++ b/app/components/markdown/markdown_link/markdown_link.tsx @@ -3,14 +3,13 @@ import {useManagedConfig} from '@mattermost/react-native-emm'; import Clipboard from '@react-native-clipboard/clipboard'; -import React, {Children, type ReactElement, useCallback} from 'react'; +import React, {Children, type ReactElement, useCallback, useMemo} from 'react'; import {useIntl, defineMessages} from 'react-intl'; import {StyleSheet, Text, View} from 'react-native'; import urlParse from 'url-parse'; import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item'; import {useServerUrl} from '@context/server'; -import {useTheme} from '@context/theme'; import {usePreventDoubleTap} from '@hooks/utils'; import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint, isEmail} from '@utils/helpers'; @@ -22,6 +21,7 @@ type MarkdownLinkProps = { href: string; siteURL: string; onLinkLongPress?: (url?: string) => void; + theme: Theme; } const messages = defineMessages({ @@ -54,38 +54,15 @@ const parseLinkLiteral = (literal: string) => { return parsed.href; }; -const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL, onLinkLongPress}: MarkdownLinkProps) => { +const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteURL, onLinkLongPress, theme}: MarkdownLinkProps) => { const intl = useIntl(); const managedConfig = useManagedConfig(); const serverUrl = useServerUrl(); - const theme = useTheme(); const handlePress = usePreventDoubleTap(useCallback(() => { openLink(href, serverUrl, siteURL, intl); }, [href, intl, serverUrl, siteURL])); - const parseChildren = useCallback(() => { - return Children.map(children, (child: ReactElement) => { - if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) { - return child; - } - - const {props, ...otherChildProps} = child; - - const {literal, ...otherProps} = props; - - const nextProps = { - literal: parseLinkLiteral(literal), - ...otherProps, - }; - - return { - props: nextProps, - ...otherChildProps, - }; - }); - }, [children]); - const handleLongPress = useCallback(() => { if (managedConfig?.copyAndPasteProtection !== 'true') { if (onLinkLongPress) { @@ -134,7 +111,31 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU } }, [managedConfig?.copyAndPasteProtection, onLinkLongPress, intl, theme, href]); - const renderChildren = experimentalNormalizeMarkdownLinks ? parseChildren() : children; + const renderedChildren = useMemo(() => { + if (!experimentalNormalizeMarkdownLinks) { + return children; + } + + return Children.map(children, (child: ReactElement) => { + if (!child.props.literal || typeof child.props.literal !== 'string' || (child.props.context && child.props.context.length && !child.props.context.includes('link'))) { + return child; + } + + const {props, ...otherChildProps} = child; + + const {literal, ...otherProps} = props; + + const nextProps = { + literal: parseLinkLiteral(literal), + ...otherProps, + }; + + return { + props: nextProps, + ...otherChildProps, + }; + }); + }, [children, experimentalNormalizeMarkdownLinks]); return ( - {renderChildren} + {renderedChildren} ); }; diff --git a/app/components/markdown/markdown_table_cell/index.tsx b/app/components/markdown/markdown_table_cell/index.tsx index 3556424ad..0968cc4b7 100644 --- a/app/components/markdown/markdown_table_cell/index.tsx +++ b/app/components/markdown/markdown_table_cell/index.tsx @@ -4,13 +4,13 @@ import React, {type ReactNode} from 'react'; import {type StyleProp, View, type ViewStyle} from 'react-native'; -import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export type MarkdownTableCellProps = { align: 'left' | 'center' | 'right'; children: ReactNode; isLastCell: boolean; + theme: Theme; }; export const CELL_MIN_WIDTH = 96; @@ -41,8 +41,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -const MarkdownTableCell = ({isLastCell, align, children}: MarkdownTableCellProps) => { - const theme = useTheme(); +const MarkdownTableCell = ({isLastCell, align, children, theme}: MarkdownTableCellProps) => { const style = getStyleSheet(theme); const cellStyle: StyleProp = [style.cell]; diff --git a/app/components/markdown/markdown_table_image/index.tsx b/app/components/markdown/markdown_table_image/index.tsx index d054cd9ff..2eb5ab935 100644 --- a/app/components/markdown/markdown_table_image/index.tsx +++ b/app/components/markdown/markdown_table_image/index.tsx @@ -10,6 +10,7 @@ import CompassIcon from '@components/compass_icon'; import ProgressiveImage from '@components/progressive_image'; import {useServerUrl} from '@context/server'; import {useGalleryItem} from '@hooks/gallery'; +import {lookupMimeType} from '@utils/file'; import {fileToGalleryItem, openGalleryAtIndex} from '@utils/gallery'; import {generateId} from '@utils/general'; import {calculateDimensions, isGifTooLarge} from '@utils/images'; @@ -26,6 +27,7 @@ type MarkdownTableImageProps = { postId: string; serverURL?: string; source: string; + theme: Theme; } const style = StyleSheet.create({ @@ -35,7 +37,15 @@ const style = StyleSheet.create({ }, }); -const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, source}: MarkdownTableImageProps) => { +const MarkTableImage = ({ + disabled, + imagesMetadata, + location, + postId, + serverURL, + source, + theme, +}: MarkdownTableImageProps) => { const sourceKey = removeImageProxyForKey(source); const metadata = secureGetFromRecord(imagesMetadata, sourceKey); const fileId = useRef(generateId('uid')).current; @@ -43,7 +53,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, const currentServerUrl = useServerUrl(); const galleryIdentifier = `${postId}-${fileId}-${location}`; - const getImageSource = () => { + const getImageSource = useCallback(() => { let uri = source; let server = serverURL; @@ -56,15 +66,15 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, } return uri; - }; + }, [source, serverURL, currentServerUrl]); - const getFileInfo = () => { + const getFileInfo = useCallback((): FileInfo => { const height = metadata?.height || 0; const width = metadata?.width || 0; const uri = getImageSource(); const decodedLink = safeDecodeURIComponent(uri); let filename = parseUrl(decodedLink.substring(decodedLink.lastIndexOf('/'))).pathname.replace('/', ''); - let extension = filename.split('.').pop(); + let extension = filename.split('.').pop() || ''; if (extension === filename) { const ext = filename.indexOf('.') === -1 ? '.png' : filename.substring(filename.lastIndexOf('.')); @@ -81,11 +91,14 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, uri, width, height, + mime_type: lookupMimeType(filename), + size: 0, + user_id: '', }; - }; + }, [fileId, getImageSource, metadata?.height, metadata?.width, postId]); const handlePreviewImage = useCallback(() => { - const file = getFileInfo() as FileInfo; + const file = getFileInfo(); if (!file?.uri) { return; } @@ -94,7 +107,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, type: 'image', }; openGalleryAtIndex(galleryIdentifier, 0, [item]); - }, [metadata, source, serverURL, currentServerUrl, postId]); + }, [getFileInfo, galleryIdentifier]); const {ref, onGestureEvent, styles} = useGalleryItem( galleryIdentifier, @@ -129,6 +142,7 @@ const MarkTableImage = ({disabled, imagesMetadata, location, postId, serverURL, onError={onLoadFailed} contentFit='contain' style={{width, height}} + theme={theme} /> diff --git a/app/components/markdown/markdown_table_row/index.tsx b/app/components/markdown/markdown_table_row/index.tsx index c34808536..3d228df5d 100644 --- a/app/components/markdown/markdown_table_row/index.tsx +++ b/app/components/markdown/markdown_table_row/index.tsx @@ -4,13 +4,13 @@ import React, {type ReactElement, type ReactNode} from 'react'; import {type StyleProp, View, type ViewStyle} from 'react-native'; -import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; export type MarkdownTableRowProps = { isFirstRow: boolean; isLastRow: boolean; children: ReactNode; + theme: Theme; } const getStyleSheet = makeStyleSheetFromTheme((theme) => { @@ -29,8 +29,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }; }); -const MarkdownTableRow = ({isFirstRow, isLastRow, children}: MarkdownTableRowProps) => { - const theme = useTheme(); +const MarkdownTableRow = ({isFirstRow, isLastRow, children, theme}: MarkdownTableRowProps) => { const style = getStyleSheet(theme); const rowStyle: StyleProp = [style.row]; diff --git a/app/components/post_list/combined_user_activity/combined_user_activity.tsx b/app/components/post_list/combined_user_activity/combined_user_activity.tsx index 1e42dd962..b3ba6cfdf 100644 --- a/app/components/post_list/combined_user_activity/combined_user_activity.tsx +++ b/app/components/post_list/combined_user_activity/combined_user_activity.tsx @@ -14,7 +14,6 @@ import {useServerUrl} from '@context/server'; import {useIsTablet} from '@hooks/device'; import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation'; import {emptyFunction} from '@utils/general'; -import {getMarkdownTextStyles} from '@utils/markdown'; import {isUserActivityProp} from '@utils/post_list'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {secureGetFromRecord} from '@utils/types'; @@ -69,7 +68,6 @@ const CombinedUserActivity = ({ const intl = useIntl(); const isTablet = useIsTablet(); const serverUrl = useServerUrl(); - const textStyles = getMarkdownTextStyles(theme); const styles = getStyleSheet(theme); const content = []; const removedUserIds: string[] = []; @@ -172,7 +170,6 @@ const CombinedUserActivity = ({ key={postType + actorId} baseTextStyle={styles.baseText} location={location} - textStyles={textStyles} value={formattedMessage} theme={theme} /> @@ -193,7 +190,7 @@ const CombinedUserActivity = ({ if (allUsernames.length) { fetchMissingProfilesByUsernames(serverUrl, allUsernames); } - }, [userActivity?.allUserIds, userActivity?.allUsernames]); + }, [serverUrl, userActivity]); if (!post) { return null; diff --git a/app/components/post_list/combined_user_activity/last_users.tsx b/app/components/post_list/combined_user_activity/last_users.tsx index af7530d3f..bf2a05a0b 100644 --- a/app/components/post_list/combined_user_activity/last_users.tsx +++ b/app/components/post_list/combined_user_activity/last_users.tsx @@ -8,7 +8,6 @@ import {Text} from 'react-native'; import FormattedMarkdownText from '@components/formatted_markdown_text'; import FormattedText from '@components/formatted_text'; import Markdown from '@components/markdown'; -import {getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {secureGetFromRecord} from '@utils/types'; @@ -44,7 +43,6 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las const [expanded, setExpanded] = useState(false); const intl = useIntl(); const style = getStyleSheet(theme); - const textStyles = getMarkdownTextStyles(theme); const onPress = () => { setExpanded(true); @@ -67,7 +65,6 @@ const LastUsers = ({actor, channelId, location, postType, theme, usernames}: Las baseTextStyle={style.baseText} channelId={channelId} location={location} - textStyles={textStyles} value={formattedMessage} theme={theme} /> diff --git a/app/components/post_list/post/body/add_members/add_members.tsx b/app/components/post_list/post/body/add_members/add_members.tsx index daa208a81..4342caeaf 100644 --- a/app/components/post_list/post/body/add_members/add_members.tsx +++ b/app/components/post_list/post/body/add_members/add_members.tsx @@ -142,6 +142,7 @@ const AddMembers = ({channelType, currentUser, location, post, theme}: AddMember location={location} mentionName={names[0]} mentionStyle={textStyles.mention} + theme={theme} /> ); } else if (names.length > 1) { @@ -170,6 +171,7 @@ const AddMembers = ({channelType, currentUser, location, post, theme}: AddMember location={location} mentionStyle={textStyles.mention} mentionName={username} + theme={theme} /> ); }).reduce((acc: ReactNode[], el: ReactNode, idx: number, arr: ReactNode[]) => { diff --git a/app/components/post_list/post/body/content/embedded_bindings/embed_text.tsx b/app/components/post_list/post/body/content/embedded_bindings/embed_text.tsx index 78101aa9a..0d01c780a 100644 --- a/app/components/post_list/post/body/content/embedded_bindings/embed_text.tsx +++ b/app/components/post_list/post/body/content/embedded_bindings/embed_text.tsx @@ -8,7 +8,6 @@ import Animated from 'react-native-reanimated'; import Markdown from '@components/markdown'; import ShowMoreButton from '@components/post_list/post/body/message/show_more_button'; import {useShowMoreAnimatedStyle} from '@hooks/show_more'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {makeStyleSheetFromTheme} from '@utils/theme'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -36,8 +35,6 @@ const EmbedText = ({channelId, location, theme, value}: Props) => { const dimensions = useWindowDimensions(); const maxHeight = Math.round((dimensions.height * 0.4) + SHOW_MORE_HEIGHT); const animatedStyle = useShowMoreAnimatedStyle(height, maxHeight, open); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); const style = getStyles(theme); const onLayout = useCallback((event: LayoutChangeEvent) => setHeight(event.nativeEvent.layout.height), []); @@ -57,8 +54,6 @@ const EmbedText = ({channelId, location, theme, value}: Props) => { baseTextStyle={style.message} channelId={channelId} location={location} - textStyles={textStyles} - blockStyles={blockStyles} disableGallery={true} theme={theme} value={value} diff --git a/app/components/post_list/post/body/content/embedded_bindings/embed_title.tsx b/app/components/post_list/post/body/content/embedded_bindings/embed_title.tsx index fed8976a8..50822901f 100644 --- a/app/components/post_list/post/body/content/embedded_bindings/embed_title.tsx +++ b/app/components/post_list/post/body/content/embedded_bindings/embed_title.tsx @@ -30,9 +30,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { fontSize: 14, lineHeight: 20, }, - link: { - color: theme.linkColor, - }, }; }); @@ -48,12 +45,9 @@ const EmbedTitle = ({channelId, location, theme, value}: Props) => { disableChannelLink={true} disableGallery={true} location={location} - autolinkedUrlSchemes={[]} - mentionKeys={[]} theme={theme} value={value} baseTextStyle={style.title} - textStyles={{link: style.link}} /> ); diff --git a/app/components/post_list/post/body/content/image_preview/image_preview.tsx b/app/components/post_list/post/body/content/image_preview/image_preview.tsx index cf123261e..14bc011fc 100644 --- a/app/components/post_list/post/body/content/image_preview/image_preview.tsx +++ b/app/components/post_list/post/body/content/image_preview/image_preview.tsx @@ -88,6 +88,9 @@ const ImagePreview = ({expandedLink, isReplyPost, layoutWidth, link, location, m if (!isImageLink(link) && expandedLink === undefined) { getRedirectLocation(serverUrl, link); } + + // Only check if we need to get the redirect location when the link changes + // eslint-disable-next-line react-hooks/exhaustive-deps }, [link]); useDidUpdate(() => { @@ -102,6 +105,9 @@ const ImagePreview = ({expandedLink, isReplyPost, layoutWidth, link, location, m if (expandedLink && expandedLink !== imageUrl) { setImageUrl(expandedLink); } + + // Only re-set the image url when the expanded link changes + // eslint-disable-next-line react-hooks/exhaustive-deps }, [expandedLink]); if (error || !isValidUrl(expandedLink || link) || isGifTooLarge(imageProps)) { @@ -128,6 +134,7 @@ const ImagePreview = ({expandedLink, isReplyPost, layoutWidth, link, location, m onError={onError} contentFit='contain' style={[style.image, {width: dimensions.width, height: dimensions.height}]} + theme={theme} /> diff --git a/app/components/post_list/post/body/content/message_attachments/attachment_fields.tsx b/app/components/post_list/post/body/content/message_attachments/attachment_fields.tsx index 38802b0d8..9e166dd34 100644 --- a/app/components/post_list/post/body/content/message_attachments/attachment_fields.tsx +++ b/app/components/post_list/post/body/content/message_attachments/attachment_fields.tsx @@ -7,17 +7,14 @@ import {type StyleProp, Text, type TextStyle, View} from 'react-native'; import Markdown from '@components/markdown'; import {makeStyleSheetFromTheme} from '@utils/theme'; -import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown'; import type {AvailableScreens} from '@typings/screens/navigation'; type Props = { baseTextStyle: StyleProp; - blockStyles?: MarkdownBlockStyles; channelId: string; fields: MessageAttachmentField[]; location: AvailableScreens; metadata?: PostMetadata | null; - textStyles?: MarkdownTextStyles; theme: Theme; } @@ -47,7 +44,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { }; }); -const AttachmentFields = ({baseTextStyle, blockStyles, channelId, fields, location, metadata, textStyles, theme}: Props) => { +const AttachmentFields = ({baseTextStyle, channelId, fields, location, metadata, theme}: Props) => { const style = getStyleSheet(theme); const fieldTables = []; @@ -96,13 +93,11 @@ const AttachmentFields = ({baseTextStyle, blockStyles, channelId, fields, locati , diff --git a/app/components/post_list/post/body/content/message_attachments/attachment_image/index.tsx b/app/components/post_list/post/body/content/message_attachments/attachment_image/index.tsx index a00eeb36b..2664fc177 100644 --- a/app/components/post_list/post/body/content/message_attachments/attachment_image/index.tsx +++ b/app/components/post_list/post/body/content/message_attachments/attachment_image/index.tsx @@ -1,7 +1,7 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import React, {useCallback, useRef, useState} from 'react'; +import React, {useCallback, useMemo, useRef, useState} from 'react'; import {TouchableWithoutFeedback, View} from 'react-native'; import Animated from 'react-native-reanimated'; @@ -59,6 +59,7 @@ const AttachmentImage = ({imageUrl, imageMetadata, layoutWidth, location, postId const isTablet = useIsTablet(); const {height, width} = calculateDimensions(imageMetadata.height, imageMetadata.width, layoutWidth || getViewPortWidth(false, isTablet, true)); const style = getStyleSheet(theme); + const progressiveImageStyle = useMemo(() => ({width, height}), [width, height]); const onError = useCallback(() => { setError(true); @@ -109,7 +110,8 @@ const AttachmentImage = ({imageUrl, imageMetadata, layoutWidth, location, postId imageUri={imageUrl} onError={onError} contentFit='contain' - style={{height, width}} + style={progressiveImageStyle} + theme={theme} /> diff --git a/app/components/post_list/post/body/content/message_attachments/attachment_pretext.tsx b/app/components/post_list/post/body/content/message_attachments/attachment_pretext.tsx index af7749861..8f8bae9c8 100644 --- a/app/components/post_list/post/body/content/message_attachments/attachment_pretext.tsx +++ b/app/components/post_list/post/body/content/message_attachments/attachment_pretext.tsx @@ -6,16 +6,13 @@ import {type StyleProp, StyleSheet, type TextStyle, View} from 'react-native'; import Markdown from '@components/markdown'; -import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown'; import type {AvailableScreens} from '@typings/screens/navigation'; type Props = { baseTextStyle: StyleProp; - blockStyles?: MarkdownBlockStyles; channelId: string; location: AvailableScreens; metadata?: PostMetadata | null; - textStyles?: MarkdownTextStyles; theme: Theme; value?: string; } @@ -26,14 +23,14 @@ const style = StyleSheet.create({ }, }); -export default function AttachmentPreText(props: Props) { - const { - baseTextStyle, - blockStyles, - metadata, - value, - textStyles, - } = props; +export default function AttachmentPreText({ + baseTextStyle, + channelId, + location, + theme, + metadata, + value, +}: Props) { if (!value) { return null; @@ -43,13 +40,11 @@ export default function AttachmentPreText(props: Props) { diff --git a/app/components/post_list/post/body/content/message_attachments/attachment_text.tsx b/app/components/post_list/post/body/content/message_attachments/attachment_text.tsx index b415b9f7e..0bc69c7f9 100644 --- a/app/components/post_list/post/body/content/message_attachments/attachment_text.tsx +++ b/app/components/post_list/post/body/content/message_attachments/attachment_text.tsx @@ -9,17 +9,14 @@ import Markdown from '@components/markdown'; import ShowMoreButton from '@components/post_list/post/body/message/show_more_button'; import {useShowMoreAnimatedStyle} from '@hooks/show_more'; -import type {MarkdownBlockStyles, MarkdownTextStyles} from '@typings/global/markdown'; import type {AvailableScreens} from '@typings/screens/navigation'; type Props = { baseTextStyle: StyleProp; - blockStyles?: MarkdownBlockStyles; channelId: string; hasThumbnail?: boolean; location: AvailableScreens; metadata?: PostMetadata | null; - textStyles?: MarkdownTextStyles; theme: Theme; value?: string; } @@ -31,7 +28,15 @@ const style = StyleSheet.create({ }, }); -const AttachmentText = ({baseTextStyle, blockStyles, channelId, hasThumbnail, location, metadata, textStyles, theme, value}: Props) => { +const AttachmentText = ({ + baseTextStyle, + channelId, + hasThumbnail, + location, + metadata, + theme, + value, +}: Props) => { const [open, setOpen] = useState(false); const [height, setHeight] = useState(); const dimensions = useWindowDimensions(); @@ -55,8 +60,6 @@ const AttachmentText = ({baseTextStyle, blockStyles, channelId, hasThumbnail, lo channelId={channelId} location={location} baseTextStyle={baseTextStyle} - textStyles={textStyles} - blockStyles={blockStyles} disableGallery={true} imagesMetadata={metadata?.images} value={value} diff --git a/app/components/post_list/post/body/content/message_attachments/attachment_title.tsx b/app/components/post_list/post/body/content/message_attachments/attachment_title.tsx index d332b711c..76e96948c 100644 --- a/app/components/post_list/post/body/content/message_attachments/attachment_title.tsx +++ b/app/components/post_list/post/body/content/message_attachments/attachment_title.tsx @@ -7,6 +7,7 @@ import {Text, View} from 'react-native'; import Markdown from '@components/markdown'; import {useExternalLinkHandler} from '@hooks/use_external_link_handler'; import {makeStyleSheetFromTheme} from '@utils/theme'; +import {typography} from '@utils/typography'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -28,9 +29,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { link: {color: theme.linkColor}, title: { color: theme.centerChannelColor, - fontSize: 14, - fontFamily: 'OpenSans-SemiBold', - lineHeight: 20, + ...typography('Heading', 100, 'SemiBold'), marginBottom: 5, }, }; @@ -55,17 +54,12 @@ const AttachmentTitle = ({channelId, link, location, theme, value}: Props) => { ); } diff --git a/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx b/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx index 7942e9fc8..cab2e7aef 100644 --- a/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx +++ b/app/components/post_list/post/body/content/message_attachments/message_attachment.tsx @@ -4,7 +4,6 @@ import React from 'react'; import {View} from 'react-native'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {getStatusColors} from '@utils/message_attachment'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {secureGetFromRecord} from '@utils/types'; @@ -58,8 +57,6 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => { export default function MessageAttachment({attachment, channelId, layoutWidth, location, metadata, postId, theme}: Props) { const style = getStyleSheet(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); const STATUS_COLORS = getStatusColors(theme); let borderStyle; if (attachment.color) { @@ -74,11 +71,9 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l <> @@ -106,12 +101,10 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l {Boolean(attachment.text) && @@ -119,12 +112,10 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l {Boolean(attachment.fields?.length) && } diff --git a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx index 586dc7c28..4f82205e9 100644 --- a/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx +++ b/app/components/post_list/post/body/content/permalink_preview/permalink_preview.tsx @@ -18,7 +18,6 @@ import {useTheme} from '@context/theme'; import {useUserLocale} from '@context/user_locale'; import {useIsTablet, useWindowDimensions} from '@hooks/device'; import {usePreventDoubleTap} from '@hooks/utils'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {displayUsername, getUserTimezone} from '@utils/user'; @@ -29,12 +28,10 @@ import PermalinkFiles from './permalink_files'; import type PostModel from '@typings/database/models/servers/post'; import type UserModel from '@typings/database/models/servers/user'; -import type {UserMentionKey} from '@typings/global/markdown'; import type {AvailableScreens} from '@typings/screens/navigation'; const MAX_PERMALINK_PREVIEW_CHARACTERS = 150; const EDITED_INDICATOR_CONTEXT = ['paragraph']; -const EMPTY_MENTION_KEYS: UserMentionKey[] = []; const MIN_PERMALINK_WIDTH = 340; const TABLET_PADDING_OFFSET = 40; @@ -154,8 +151,6 @@ const PermalinkPreview = ({ }, [dimensions.width, dimensions.height, isTablet]); const maxPermalinkHeight = Math.round(dimensions.height * 0.5); - const textStyles = getMarkdownTextStyles(theme); - const blockStyles = getMarkdownBlockStyles(theme); const userId = embedData?.post?.user_id; @@ -221,6 +216,10 @@ const PermalinkPreview = ({ setShowGradient(height >= maxPermalinkHeight); }, [maxPermalinkHeight]); + // We need to memoize this value because it is actually a getter that returns a new list + // on every render. We need to trust that changes in the currentUser will trigger the recalculation. + const mentionKeys = useMemo(() => currentUser?.mentionKeys ?? undefined, [currentUser]); + if (!post) { return null; } @@ -264,13 +263,11 @@ const PermalinkPreview = ({ {isEdited ? ( |undefined => { + const replyBarStyle = useMemo|undefined>(() => { if (!isReplyPost || (isCRTEnabled && location === Screens.PERMALINK)) { return undefined; } @@ -125,7 +125,7 @@ const Body = ({ } return barStyle; - }, []); + }, [highlightReplyBar, isCRTEnabled, isFirstReply, isLastReply, isReplyPost, location, style]); const onLayout = useCallback((e: LayoutChangeEvent) => { if (location === Screens.SAVED_MESSAGES) { @@ -233,7 +233,7 @@ const Body = ({ style={style.messageContainerWithReplyBar} onLayout={onLayout} > - + {body} {isFailed && { @@ -65,8 +63,16 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight const maxHeight = Math.round((dimensions.height * 0.5) + SHOW_MORE_HEIGHT); const animatedStyle = useShowMoreAnimatedStyle(height, maxHeight, open); const style = getStyleSheet(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); + + // We need to memoize these two values because they are actually getters that return a new list + // on every render. We need to trust that changes in the currentUser will trigger the recalculation. + const mentionKeys = useMemo(() => currentUser?.mentionKeys ?? undefined, [currentUser]); + const highlightKeys = useMemo(() => { + if (isHighlightWithoutNotificationLicensed) { + return currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS; + } + return EMPTY_HIGHLIGHT_KEYS; + }, [currentUser, isHighlightWithoutNotificationLicensed]); const onLayout = useCallback((event: LayoutChangeEvent) => { const h = event.nativeEvent.layout.height; @@ -95,7 +101,6 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight > ; messageStyle: StyleProp; - textStyles: { - [key: string]: TextStyle; - }; }; theme: Theme; } @@ -68,7 +64,7 @@ const renderUsername = (value = '') => { }; const renderMessage = ({location, post, styles, intl, localeHolder, theme, values, skipMarkdown = false}: RenderMessageProps) => { - const {containerStyle, messageStyle, textStyles} = styles; + const {containerStyle, messageStyle} = styles; if (skipMarkdown) { return ( @@ -85,7 +81,6 @@ const renderMessage = ({location, post, styles, intl, localeHolder, theme, value channelId={post.channelId} disableGallery={true} location={location} - textStyles={textStyles} value={intl.formatMessage(localeHolder, values)} theme={theme} /> @@ -299,8 +294,7 @@ export const SystemMessage = ({post, location, author, hideGuestTags}: SystemMes const intl = useIntl(); const theme = useTheme(); const style = getStyleSheet(theme); - const textStyles = getMarkdownTextStyles(theme); - const styles = {messageStyle: style.systemMessage, textStyles, containerStyle: style.container}; + const styles = {messageStyle: style.systemMessage, containerStyle: style.container}; if (post.type === Post.POST_TYPES.GUEST_JOIN_CHANNEL) { return renderGuestJoinChannelMessage({post, author, location, styles, intl, theme}, hideGuestTags); @@ -317,7 +311,6 @@ export const SystemMessage = ({post, location, author, hideGuestTags}: SystemMes channelId={post.channelId} location={location} disableGallery={true} - textStyles={styles.textStyles} value={post.message} theme={theme} /> diff --git a/app/components/progressive_image/index.tsx b/app/components/progressive_image/index.tsx index 77afbc9a5..fb8d174a2 100644 --- a/app/components/progressive_image/index.tsx +++ b/app/components/progressive_image/index.tsx @@ -6,7 +6,6 @@ import React, {type ReactNode, useEffect, useState} from 'react'; import {type StyleProp, StyleSheet, View, type ViewStyle} from 'react-native'; import Animated from 'react-native-reanimated'; -import {useTheme} from '@context/theme'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; const AnimatedImage = Animated.createAnimatedComponent(Image); @@ -21,6 +20,7 @@ type Props = ProgressiveImageProps & { contentFit?: ImageContentFit; style?: StyleProp; tintDefaultSource?: boolean; + theme: Theme; }; const getStyleSheet = makeStyleSheetFromTheme((theme) => { @@ -38,11 +38,22 @@ const getStyleSheet = makeStyleSheetFromTheme((theme) => { }); const ProgressiveImage = ({ - children, defaultSource, forwardRef, id, imageStyle, imageUri, inViewPort, isBackgroundImage, - onError, contentFit = 'contain', style = {}, thumbnailUri, tintDefaultSource, + children, + defaultSource, + forwardRef, + id, + imageStyle, + imageUri, + inViewPort, + isBackgroundImage, + onError, + contentFit = 'contain', + style = {}, + thumbnailUri, + tintDefaultSource, + theme, }: Props) => { const [showHighResImage, setShowHighResImage] = useState(false); - const theme = useTheme(); const styles = getStyleSheet(theme); useEffect(() => { diff --git a/app/components/remove_markdown/at_mention/at_mention.tsx b/app/components/remove_markdown/at_mention/at_mention.tsx index c648a50d5..a859d0592 100644 --- a/app/components/remove_markdown/at_mention/at_mention.tsx +++ b/app/components/remove_markdown/at_mention/at_mention.tsx @@ -10,11 +10,9 @@ import GroupModel from '@database/models/server/group'; import {useMemoMentionedGroup, useMemoMentionedUser} from '@hooks/markdown'; import {displayUsername} from '@utils/user'; -import type {Database} from '@nozbe/watermelondb'; import type UserModelType from '@typings/database/models/servers/user'; type AtMentionProps = { - database: Database; mentionName: string; teammateNameDisplay: string; textStyle?: StyleProp; @@ -40,6 +38,9 @@ const AtMention = ({ if (!user?.username && !group?.name) { fetchUserOrGroupsByMentionsInBatch(serverUrl, mentionName); } + + // Only fetch the user or group on mount + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); let mention; diff --git a/app/components/remove_markdown/index.tsx b/app/components/remove_markdown/index.tsx index d01e9a368..30c51697b 100644 --- a/app/components/remove_markdown/index.tsx +++ b/app/components/remove_markdown/index.tsx @@ -7,13 +7,14 @@ import React, {type ReactElement, useCallback, useMemo, useRef} from 'react'; import {type StyleProp, Text, type TextStyle} from 'react-native'; import Emoji from '@components/emoji'; -import {computeTextStyle} from '@utils/markdown'; +import {useTheme} from '@context/theme'; +import {computeTextStyle, getMarkdownTextStyles} from '@utils/markdown'; import ChannelMention from '../markdown/channel_mention'; import AtMention from './at_mention'; -import type {MarkdownBaseRenderer, MarkdownChannelMentionRenderer, MarkdownEmojiRenderer, MarkdownTextStyles} from '@typings/global/markdown'; +import type {MarkdownBaseRenderer, MarkdownChannelMentionRenderer, MarkdownEmojiRenderer} from '@typings/global/markdown'; type Props = { enableEmoji?: boolean; @@ -22,11 +23,17 @@ type Props = { enableSoftBreak?: boolean; enableChannelLink?: boolean; baseStyle?: StyleProp; - textStyle?: MarkdownTextStyles; value: string; }; -const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enableSoftBreak, enableCodeSpan, baseStyle, textStyle = {}, value}: Props) => { +const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enableSoftBreak, enableCodeSpan, baseStyle, value}: Props) => { + const theme = useTheme(); + const textStyle = getMarkdownTextStyles(theme); + + const renderText = useCallback(({literal}: {literal: string}) => { + return {literal}; + }, [baseStyle]); + const renderEmoji = useCallback(({emojiName, literal}: MarkdownEmojiRenderer) => { if (!enableEmoji) { return renderText({literal}); @@ -40,34 +47,34 @@ const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enable textStyle={baseStyle} /> ); - }, [baseStyle, enableEmoji]); + }, [baseStyle, enableEmoji, renderText]); const renderBreak = useCallback(() => { return '\n'; }, []); - const renderText = useCallback(({literal}: {literal: string}) => { - return {literal}; - }, [baseStyle]); - - const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => { + const renderAtMention = useCallback(({context, mentionName}: {context: string[]; mentionName: string}) => { return ( ); - }; + }, [baseStyle, textStyle]); - const renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => { - return ( - - ); - }; + const renderChannelLink = useCallback(({context, channelName}: MarkdownChannelMentionRenderer) => { + if (enableChannelLink) { + return ( + + ); + } + + return renderText({literal: `~${channelName}`}); + }, [baseStyle, enableChannelLink, renderText, textStyle]); const renderCodeSpan = useCallback(({context, literal}: MarkdownBaseRenderer) => { if (!enableCodeSpan) { @@ -83,7 +90,7 @@ const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enable {literal} ); - }, [baseStyle, textStyle, enableCodeSpan]); + }, [enableCodeSpan, textStyle, baseStyle, renderText]); const renderNull = () => { return null; @@ -101,7 +108,7 @@ const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enable link: Renderer.forwardChildren, image: renderNull, atMention: renderAtMention, - channelLink: enableChannelLink ? renderChannelLink : Renderer.forwardChildren, + channelLink: renderChannelLink, emoji: renderEmoji, hashtag: Renderer.forwardChildren, latexinline: Renderer.forwardChildren, @@ -131,8 +138,23 @@ const RemoveMarkdown = ({enableEmoji, enableChannelLink, enableHardBreak, enable }); }; - const parser = useRef(new Parser()).current; - const renderer = useMemo(createRenderer, [renderText, renderEmoji]); + // Pattern suggested in https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents + const parserRef = useRef(null); + if (parserRef.current === null) { + parserRef.current = new Parser(); + } + const parser = parserRef.current; + + const renderer = useMemo(createRenderer, [ + renderText, + renderCodeSpan, + renderAtMention, + renderChannelLink, + renderEmoji, + enableHardBreak, + renderBreak, + enableSoftBreak, + ]); const ast = parser.parse(value); return renderer.render(ast) as ReactElement; diff --git a/app/components/section_notice/index.tsx b/app/components/section_notice/index.tsx index fc7f92a55..6391c3e83 100644 --- a/app/components/section_notice/index.tsx +++ b/app/components/section_notice/index.tsx @@ -6,7 +6,6 @@ import {Pressable, Text, View} from 'react-native'; import Tag from '@components/tag'; import {useTheme} from '@context/theme'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -206,8 +205,6 @@ const SectionNotice = ({ theme={theme} location={location} baseTextStyle={styles.baseText} - textStyles={getMarkdownTextStyles(theme)} - blockStyles={getMarkdownBlockStyles(theme)} value={text} /> )} diff --git a/app/components/settings/footer.tsx b/app/components/settings/footer.tsx index 8eda86f39..793ca9aba 100644 --- a/app/components/settings/footer.tsx +++ b/app/components/settings/footer.tsx @@ -6,7 +6,6 @@ import {View} from 'react-native'; import Markdown from '@components/markdown'; import {useTheme} from '@context/theme'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import type {AvailableScreens} from '@typings/screens/navigation'; @@ -48,8 +47,6 @@ function Footer({ }: Props) { const theme = useTheme(); const style = getStyleSheet(theme); - const textStyles = getMarkdownTextStyles(theme); - const blockStyles = getMarkdownBlockStyles(theme); return ( <> @@ -57,10 +54,8 @@ function Footer({ @@ -70,8 +65,6 @@ function Footer({ { const {authorUsername, numTasks, numTasksChecked, participantIds, playbookRunId, runName} = post.props as StatusUpdatePostProps; const serverUrl = useServerUrl(); const style = getStyleSheet(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); const intl = useIntl(); useEffect(() => { @@ -108,26 +105,26 @@ const StatusUpdatePost = ({location, post, theme}: Props) => { defaultMessage: '**{numTasksChecked, number}** of **{numTasks, number}** {numTasks, plural, =1 {task} other {tasks}} checked', }, {numTasksChecked, numTasks}); + const mentionKeys = useMemo(() => { + return authorUsername ? [{key: authorUsername, caseSensitive: false}] : []; + }, [authorUsername]); + return ( { @@ -158,7 +154,6 @@ const StatusUpdatePost = ({location, post, theme}: Props) => { participantIds={participantIds} location={location} baseTextStyle={style.detailsText} - textStyles={textStyles} /> diff --git a/app/products/playbooks/components/status_update_post/participants/participants.tsx b/app/products/playbooks/components/status_update_post/participants/participants.tsx index ab58e196f..0f904389c 100644 --- a/app/products/playbooks/components/status_update_post/participants/participants.tsx +++ b/app/products/playbooks/components/status_update_post/participants/participants.tsx @@ -19,7 +19,6 @@ import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import type {UserModel} from '@database/models/server'; -import type {MarkdownTextStyles} from '@typings/global/markdown'; import type {AvailableScreens} from '@typings/screens/navigation'; type Props = { @@ -27,7 +26,6 @@ type Props = { users: UserModel[]; location: AvailableScreens; baseTextStyle: StyleProp; - textStyles: MarkdownTextStyles; } const USER_ROW_HEIGHT = 40; @@ -50,7 +48,7 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({ }, })); -const Participants = ({baseTextStyle, participantIds, users, location, textStyles}: Props) => { +const Participants = ({baseTextStyle, participantIds, users, location}: Props) => { const intl = useIntl(); const theme = useTheme(); const isTablet = useIsTablet(); @@ -117,7 +115,6 @@ const Participants = ({baseTextStyle, participantIds, users, location, textStyle diff --git a/app/products/playbooks/screens/playbook_run/playbook_run.tsx b/app/products/playbooks/screens/playbook_run/playbook_run.tsx index c2a4624d1..ba4ea50c6 100644 --- a/app/products/playbooks/screens/playbook_run/playbook_run.tsx +++ b/app/products/playbooks/screens/playbook_run/playbook_run.tsx @@ -17,7 +17,6 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import {finishRun, setOwner} from '@playbooks/actions/remote/runs'; import {getRunScheduledTimestamp, isRunFinished} from '@playbooks/utils/run'; import {openUserProfileModal, popTopScreen} from '@screens/navigation'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {showPlaybookErrorSnackbar} from '@utils/snack_bar'; import {makeStyleSheetFromTheme, changeOpacity} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -295,8 +294,6 @@ export default function PlaybookRun({ theme={theme} location={componentId} baseTextStyle={styles.infoText} - blockStyles={getMarkdownBlockStyles(theme)} - textStyles={getMarkdownTextStyles(theme)} /> diff --git a/app/screens/apps_form/apps_form_component.tsx b/app/screens/apps_form/apps_form_component.tsx index 7e69775df..fe9b3dd34 100644 --- a/app/screens/apps_form/apps_form_component.tsx +++ b/app/screens/apps_form/apps_form_component.tsx @@ -19,7 +19,6 @@ import useNavButtonPressed from '@hooks/navigation_button_pressed'; import SecurityManager from '@managers/security_manager'; import {filterEmptyOptions} from '@utils/apps'; import {checkDialogElementForError, checkIfErrorsMatchElements} from '@utils/integrations'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {secureGetFromRecord} from '@utils/types'; @@ -158,7 +157,7 @@ function AppsFormComponent({ base.showAsAction = 'always'; base.color = theme.sidebarHeaderTextColor; return base; - }, [theme.sidebarHeaderTextColor, Boolean(submitButtons), submitting, intl]); + }, [submitButtons, intl, submitting, theme.sidebarHeaderTextColor]); useEffect(() => { setButtons(componentId, { @@ -396,8 +395,6 @@ function AppsFormComponent({ { onChange(name, newValue); - }, [name]); + }, [name, onChange]); const handleSelect = useCallback((newValue: SelectedDialogOption) => { if (!newValue) { @@ -217,11 +216,8 @@ function AppsFormField({ > diff --git a/app/screens/channel/header/channel_banner/channel_banner.tsx b/app/screens/channel/header/channel_banner/channel_banner.tsx index 4e9825c5d..7bf392892 100644 --- a/app/screens/channel/header/channel_banner/channel_banner.tsx +++ b/app/screens/channel/header/channel_banner/channel_banner.tsx @@ -12,7 +12,6 @@ import {useDefaultHeaderHeight} from '@hooks/header'; import {bottomSheet} from '@screens/navigation'; import {getContrastingSimpleColor} from '@utils/general'; import {bottomSheetSnapPoint} from '@utils/helpers'; -import {getMarkdownTextStyles} from '@utils/markdown'; import {typography} from '@utils/typography'; const BUTTON_HEIGHT = 48; // From /app/utils/buttonStyles.ts, lg button @@ -74,21 +73,6 @@ export function ChannelBanner({bannerInfo, isTopItem}: Props) { zIndex: 1, }), [bannerInfo?.background_color, defaultHeight, style.container]); - const markdownTextStyle = useMemo(() => { - // We do a shallow copy to avoid mutating the original object. - const textStyle = {...getMarkdownTextStyles(theme)}; - - // channel banner colors are theme independent. - // If we let the link color being set by the theme, it will be unreadable in some cases. - // So we set the link color to the banner text color explicitly. This, with the controlled - // background color, ensures the banner text is always readable. - textStyle.link = { - ...textStyle.link, - color: bannerTextColor, - }; - return textStyle; - }, [bannerTextColor, theme]); - const handlePress = useCallback(() => { // set snap point based on text length, with a defined // minimum and maximum height for the text container @@ -136,7 +120,6 @@ export function ChannelBanner({bannerInfo, isTopItem}: Props) { > diff --git a/app/screens/channel_info/extra/extra.tsx b/app/screens/channel_info/extra/extra.tsx index cbbe1f051..b54edb8a0 100644 --- a/app/screens/channel_info/extra/extra.tsx +++ b/app/screens/channel_info/extra/extra.tsx @@ -21,7 +21,6 @@ import {ANDROID_33, OS_VERSION} from '@constants/versions'; import {useTheme} from '@context/theme'; import {bottomSheet, dismissBottomSheet} from '@screens/navigation'; import {bottomSheetSnapPoint, isEmail} from '@utils/helpers'; -import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown'; import {showSnackBar} from '@utils/snack_bar'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -111,8 +110,6 @@ const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomS const managedConfig = useManagedConfig(); const styles = getStyleSheet(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); const created = useMemo(() => ({ user: createdBy, date: ( @@ -242,14 +239,12 @@ const Extra = ({channelId, createdAt, createdBy, customStatus, header, isCustomS (false); @@ -223,7 +221,6 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t enableChannelLink={true} enableHardBreak={true} enableSoftBreak={true} - textStyle={textStyles} baseStyle={styles.message} value={post.message.substring(0, 100)} // This substring helps to avoid ANR's /> diff --git a/app/screens/interactive_dialog/dialog_introduction_text.tsx b/app/screens/interactive_dialog/dialog_introduction_text.tsx index 0b061d8c5..a910e9895 100644 --- a/app/screens/interactive_dialog/dialog_introduction_text.tsx +++ b/app/screens/interactive_dialog/dialog_introduction_text.tsx @@ -7,7 +7,6 @@ import {View} from 'react-native'; import Markdown from '@components/markdown'; import {Screens} from '@constants'; import {useTheme} from '@context/theme'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown'; import {makeStyleSheetFromTheme} from '@utils/theme'; const getStyleFromTheme = makeStyleSheetFromTheme((theme) => { @@ -28,16 +27,12 @@ type Props = { function DialogIntroductionText({value}: Props) { const theme = useTheme(); const style = getStyleFromTheme(theme); - const blockStyles = getMarkdownBlockStyles(theme); - const textStyles = getMarkdownTextStyles(theme); return ( diff --git a/app/screens/terms_of_service/terms_of_service.tsx b/app/screens/terms_of_service/terms_of_service.tsx index 2331fe2a3..d6726067e 100644 --- a/app/screens/terms_of_service/terms_of_service.tsx +++ b/app/screens/terms_of_service/terms_of_service.tsx @@ -23,7 +23,6 @@ import useAndroidHardwareBackHandler from '@hooks/android_back_handler'; import SecurityManager from '@managers/security_manager'; import {dismissOverlay} from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; -import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown'; import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; @@ -193,6 +192,9 @@ const TermsOfService = ({ useEffect(() => { getTerms(); + + // Only get the terms on mount + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); useEffect(() => { @@ -209,9 +211,6 @@ const TermsOfService = ({ useAndroidHardwareBackHandler(componentId, onPressClose); - const blockStyles = useMemo(() => getMarkdownBlockStyles(theme), [theme]); - const textStyles = useMemo(() => getMarkdownTextStyles(theme), [theme]); - let content; if (loading) { content = ( @@ -253,8 +252,6 @@ const TermsOfService = ({ > { }); it('should apply a single context style', () => { - expect(computeTextStyle(textStyles, baseStyle, ['bold'])).toEqual([baseStyle, textStyles.bold]); + expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold']))).toEqual({...baseStyle, ...textStyles.bold}); }); it('should apply multiple context styles', () => { - expect(computeTextStyle(textStyles, baseStyle, ['bold', 'italic'])).toEqual([baseStyle, textStyles.bold, textStyles.italic]); + expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold', 'italic']))).toEqual({...baseStyle, ...textStyles.bold, ...textStyles.italic}); }); it('should ignore undefined styles', () => { - expect(computeTextStyle(textStyles, baseStyle, ['bold', 'unknown'])).toEqual([baseStyle, textStyles.bold]); + expect(StyleSheet.flatten(computeTextStyle(textStyles, baseStyle, ['bold', 'unknown']))).toEqual({...baseStyle, ...textStyles.bold}); }); it('should handle multiple undefined styles', () => { diff --git a/app/utils/markdown/index.ts b/app/utils/markdown/index.ts index 4a4a55039..81f760a49 100644 --- a/app/utils/markdown/index.ts +++ b/app/utils/markdown/index.ts @@ -6,7 +6,7 @@ import parseUrl from 'url-parse'; import {getViewPortWidth} from '@utils/images'; import {logError} from '@utils/log'; -import {changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme'; +import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme'; import {typography} from '@utils/typography'; import {safeDecodeURIComponent} from '@utils/url'; @@ -276,7 +276,7 @@ export const getMarkdownImageSize = ( export const computeTextStyle = (textStyles: MarkdownTextStyles, baseStyle: StyleProp, context: string[]) => { const contextStyles: TextStyle[] = context.map((type) => textStyles[type]).filter((f) => f !== undefined); - return contextStyles.length ? concatStyles(baseStyle, contextStyles) : baseStyle; + return contextStyles.length ? [baseStyle, contextStyles] : baseStyle; }; export function parseSearchTerms(searchTerm: string): string[] | undefined { diff --git a/app/utils/theme/index.test.ts b/app/utils/theme/index.test.ts index d7115bb01..9a3ee0cef 100644 --- a/app/utils/theme/index.test.ts +++ b/app/utils/theme/index.test.ts @@ -10,7 +10,6 @@ import NavigationStore from '@store/navigation_store'; import { blendColors, changeOpacity, - concatStyles, getComponents, getKeyboardAppearanceFromTheme, hexToHue, @@ -116,15 +115,6 @@ describe('changeOpacity', () => { }); }); -describe('concatStyles', () => { - it('should concatenate styles', () => { - const style1 = {backgroundColor: 'red'} as any; - const style2 = {color: 'blue'} as any; - const result = concatStyles(style1, style2); - expect(result).toEqual([style1, style2]); - }); -}); - describe('setNavigatorStyles', () => { it('should set the navigator styles', () => { const statusSpy = jest.spyOn(StatusBar, 'setBarStyle'); diff --git a/app/utils/theme/index.ts b/app/utils/theme/index.ts index e10f30703..f1a44e28b 100644 --- a/app/utils/theme/index.ts +++ b/app/utils/theme/index.ts @@ -77,10 +77,6 @@ export function changeOpacity(oldColor: string, opacity: number): string { return `rgba(${red},${green},${blue},${alpha * opacity})`; } -export function concatStyles(...styles: T[]) { - return ([] as T[]).concat(...styles); -} - export function setNavigatorStyles(componentId: string, theme: Theme, additionalOptions: Options = {}, statusBarColor?: string) { const isDark = tinyColor(statusBarColor || theme.sidebarBg).isDark(); const options: Options = {