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 = {