Improve markdown code (#9164)

* Improve markdown code

* Add missing changes

* Simplify style composition

* Address feedback

* Fix color issue introduced by channel banner
This commit is contained in:
Daniel Espino García 2025-11-04 10:05:56 +01:00 committed by GitHub
parent d713ea5b39
commit 1d8568ade8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
51 changed files with 539 additions and 523 deletions

View file

@ -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(() => (
<ExpandedAnnouncementBanner
@ -165,7 +163,6 @@ const AnnouncementBanner = ({
{' '}
<RemoveMarkdown
value={bannerText}
textStyle={markdownTextStyles}
baseStyle={style.bannerText}
/>
</Text>

View file

@ -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 = ({
>
<Markdown
baseTextStyle={style.baseTextStyle}
blockStyles={getMarkdownBlockStyles(theme)}
disableGallery={true}
textStyles={getMarkdownTextStyles(theme)}
value={bannerText}
theme={theme}
location={Screens.BOTTOM_SHEET}

View file

@ -9,13 +9,11 @@ import Markdown from '@components/markdown';
import ShowMoreButton from '@components/post_list/post/body/message/show_more_button';
import {useTheme} from '@context/theme';
import {useShowMoreAnimatedStyle} from '@hooks/show_more';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import type DraftModel from '@typings/database/models/servers/draft';
import type ScheduledPostModel from '@typings/database/models/servers/scheduled_post';
import type {UserMentionKey} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type Props = {
@ -24,7 +22,6 @@ type Props = {
location: AvailableScreens;
}
const EMPTY_MENTION_KEYS: UserMentionKey[] = [];
const SHOW_MORE_HEIGHT = 54;
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -52,8 +49,6 @@ const DraftAndScheduledPostMessage: React.FC<Props> = ({
}) => {
const theme = useTheme();
const style = getStyleSheet(theme);
const blockStyles = getMarkdownBlockStyles(theme);
const textStyles = getMarkdownTextStyles(theme);
const [height, setHeight] = useState<number|undefined>();
const [open, setOpen] = useState(false);
const dimensions = useWindowDimensions();
@ -81,14 +76,11 @@ const DraftAndScheduledPostMessage: React.FC<Props> = ({
>
<Markdown
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={post.channelId}
layoutWidth={layoutWidth}
location={location}
postId={post.id}
textStyles={textStyles}
value={post.message}
mentionKeys={EMPTY_MENTION_KEYS}
theme={theme}
imagesMetadata={post.metadata?.images}
/>

View file

@ -126,6 +126,7 @@ const ImageFile = ({
tintDefaultSource={!file.localPath && !failed}
onError={handleError}
contentFit={contentFit}
theme={theme}
{...imageProps}
/>
);

View file

@ -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<string, string> = {};
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<string, string> = {};
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()}
/>
);

View file

@ -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<TextStyle>;
values?: Record<string, PrimitiveType>;
};
@ -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<TextStyle>, context: string[]) => {
return concatStyles(base, context.map((type) => (txtStyles as {[s: string]: TextStyle})[type]));
};
const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => {
return (
<AtMention
@ -89,8 +84,8 @@ const FormattedMarkdownText = ({baseTextStyle, channelId, defaultMessage, id, lo
mentionStyle={txtStyles.mention}
mentionName={mentionName}
location={location}
onPostPress={onPostPress}
textStyle={[computeTextStyle(baseTextStyle, context), styles.atMentionOpacity]}
textStyle={[computeTextStyle(txtStyles, baseTextStyle, context), styles.atMentionOpacity]}
theme={theme}
/>
);
};
@ -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 <Text style={computed}>{literal}</Text>;
};
@ -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 <MarkdownLink href={url}>{children}</MarkdownLink>;
return (
<MarkdownLink
href={url}
theme={theme}
>
{children}
</MarkdownLink>
);
};
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 <Text style={computed}>{literal}</Text>;
};

View file

@ -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<TextStyle>;
onPostPress?: (e: GestureResponderEvent) => void;
teammateNameDisplay: string;
textStyle?: StyleProp<TextStyle>;
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<ManagedConfig>();
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) {

View file

@ -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;

View file

@ -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<TextStyle>;
baseParagraphStyle?: StyleProp<TextStyle>;
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<MarkdownBlockStyles>(() => getMarkdownBlockStyles(theme), [theme]);
const textStyles = useMemo<MarkdownTextStyles>(() => getMarkdownTextStyles(theme), [theme]);
const managedConfig = useManagedConfig<ManagedConfig>();
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 (
<Text
testID='markdown_text'
selectable={selectable}
>
{literal}
</Text>
);
}
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<TextStyle>;
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 (
<Text
testID='markdown_text'
style={styles}
selectable={selectable}
>
{literal}
</Text>
);
}, [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}
</MarkdownBlockQuote>
);
};
}, [disableBlockQuote, blockStyles?.quoteBlockIcon]);
const renderBreak = () => {
return <Text testID='markdown_break'>{'\n'}</Text>;
};
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 (
<Text testID='markdown_checkbox'>
<CompassIcon
@ -229,9 +283,9 @@ const Markdown = ({
{' '}
</Text>
);
};
}, [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 (
<Text
@ -267,9 +322,9 @@ const Markdown = ({
{literal}
</Text>
);
};
}, [baseTextStyle, textStyles]);
const renderEditedIndicator = ({context}: {context: string[]}) => {
const renderEditedIndicator = useCallback(({context}: {context: string[]}) => {
return (
<EditedIndicator
baseTextStyle={baseTextStyle}
@ -280,9 +335,9 @@ const Markdown = ({
testID='edited_indicator'
/>
);
};
}, [baseTextStyle, theme]);
const renderEmoji = ({context, emojiName, literal}: MarkdownEmojiRenderer) => {
const renderEmoji = useCallback(({context, emojiName, literal}: MarkdownEmojiRenderer) => {
return (
<Emoji
emojiName={emojiName}
@ -291,17 +346,17 @@ const Markdown = ({
textStyle={computeTextStyle(textStyles, baseTextStyle, context)}
/>
);
};
}, [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 (
<Text
@ -329,9 +384,9 @@ const Markdown = ({
</Text>
</View>
);
};
}, [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 = ({
/>
</Text>
);
};
}, [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 = ({
<MarkdownLink
href={href}
onLinkLongPress={onLinkLongPress}
theme={theme}
>
{children}
</MarkdownLink>
);
};
}, [isUnsafeLinksPost, onLinkLongPress, renderText, theme]);
const renderList = ({children, start, tight, type}: any) => {
const renderList = useCallback(({children, start, tight, type}: any) => {
return (
<MarkdownList
ordered={type !== 'bullet'}
@ -428,9 +486,9 @@ const Markdown = ({
{children}
</MarkdownList>
);
};
}, []);
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}
</MarkdownListItem>
);
};
}, [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 = ({
</Text>
</View>
);
};
}, [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}
</MarkdownTable>
);
};
const renderTableCell = (args: MarkdownTableCellProps) => {
return <MarkdownTableCell {...args}/>;
};
const renderTableRow = (args: MarkdownTableRowProps) => {
return <MarkdownTableRow {...args}/>;
};
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 (
<Text
testID='markdown_text'
selectable={selectable}
>
{literal}
</Text>
);
}
// 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 (
<Text
testID='markdown_text'
style={styles}
selectable={selectable}
>
{literal}
</Text>
<MarkdownTableCell
{...args}
theme={theme}
/>
);
};
}, [theme]);
const renderThematicBreak = () => {
const renderTableRow = useCallback((args: MarkdownTableRowProps) => {
return (
<MarkdownTableRow
{...args}
theme={theme}
/>
);
}, [theme]);
const renderThematicBreak = useCallback(() => {
return (
<View
style={blockStyles?.horizontalRule}
testID='markdown_thematic_break'
/>
);
};
}, [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<Parser | null>(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 (
<FormattedText
id='markdown.parse_error'
defaultMessage='An error occurred while parsing this text'
style={style.errorMessage}
/>
);
}
return (
<FormattedText
id='markdown.parse_error'
defaultMessage='An error occurred while parsing this text'
style={style.errorMessage}
/>
);
}
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 (
<FormattedText
id='markdown.render_error'
defaultMessage='An error occurred while rendering this text'
style={style.errorMessage}
/>
);
}
return (
<FormattedText
id='markdown.render_error'
defaultMessage='An error occurred while rendering this text'
style={style.errorMessage}
/>
);
}
}, [highlightKeys, isEdited, mentionKeys, parser, renderer, searchPatterns, style.errorMessage, value]);
return output;
};

View file

@ -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<ManagedConfig>();
const theme = useTheme();
const style = getStyleSheet(theme);
const SyntaxHighlighter = useMemo(() => {
if (!syntaxHighlighter) {

View file

@ -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<ManagedConfig>();
const genericFileId = useRef(generateId('uid')).current;
// Pattern suggested in https://react.dev/reference/react/useRef#avoiding-recreating-the-ref-contents
const genericFileRef = useRef<string | null>(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}
/>
</Animated.View>
</TouchableWithoutFeedback>
@ -250,7 +270,8 @@ const MarkdownImage = ({
imageUri={fileInfo.uri}
onError={handleOnError}
contentFit='contain'
style={{width, height}}
style={progressiveImageStyle}
theme={theme}
/>
</TouchableWithFeedback>
);

View file

@ -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<ManagedConfig>();
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 (
<Text
@ -142,7 +143,7 @@ const MarkdownLink = ({children, experimentalNormalizeMarkdownLinks, href, siteU
onLongPress={handleLongPress}
testID='markdown_link'
>
{renderChildren}
{renderedChildren}
</Text>
);
};

View file

@ -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<ViewStyle> = [style.cell];

View file

@ -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}
/>
</Animated.View>
</TouchableWithoutFeedback>

View file

@ -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<ViewStyle> = [style.row];

View file

@ -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;

View file

@ -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}
/>

View file

@ -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[]) => {

View file

@ -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}

View file

@ -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}}
/>
</View>
);

View file

@ -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}
/>
</Animated.View>
</TouchableWithoutFeedback>

View file

@ -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<TextStyle>;
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
<Markdown
baseTextStyle={baseTextStyle}
channelId={channelId}
textStyles={textStyles}
blockStyles={blockStyles}
disableGallery={true}
imagesMetadata={metadata?.images}
location={location}
theme={theme}
value={(field.value || '')}
value={field.value || ''}
/>
</View>
</View>,

View file

@ -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}
/>
</Animated.View>
</TouchableWithoutFeedback>

View file

@ -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<TextStyle>;
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) {
<View style={style.container}>
<Markdown
baseTextStyle={baseTextStyle}
channelId={props.channelId}
textStyles={textStyles}
blockStyles={blockStyles}
channelId={channelId}
disableGallery={true}
imagesMetadata={metadata?.images}
location={props.location}
theme={props.theme}
location={location}
theme={theme}
value={value}
/>
</View>

View file

@ -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<TextStyle>;
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<number|undefined>();
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}

View file

@ -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) => {
<Markdown
channelId={channelId}
location={location}
isEdited={false}
isReplyPost={false}
disableHashtags={true}
disableAtMentions={true}
disableGallery={true}
autolinkedUrlSchemes={[]}
mentionKeys={[]}
theme={theme}
value={value}
baseTextStyle={style.title}
textStyles={{link: style.link}}
/>
);
}

View file

@ -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
<>
<AttachmentPreText
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={channelId}
location={location}
metadata={metadata}
textStyles={textStyles}
theme={theme}
value={attachment.pretext}
/>
@ -106,12 +101,10 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
{Boolean(attachment.text) &&
<AttachmentText
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={channelId}
location={location}
hasThumbnail={Boolean(attachment.thumb_url)}
metadata={metadata}
textStyles={textStyles}
value={attachment.text}
theme={theme}
/>
@ -119,12 +112,10 @@ export default function MessageAttachment({attachment, channelId, layoutWidth, l
{Boolean(attachment.fields?.length) &&
<AttachmentFields
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={channelId}
location={location}
fields={attachment.fields!}
metadata={metadata}
textStyles={textStyles}
theme={theme}
/>
}

View file

@ -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 = ({
<View style={styles.messageContainer}>
<Markdown
baseTextStyle={styles.messageText}
blockStyles={blockStyles}
channelId={embedData.channel_id}
location={location}
theme={theme}
textStyles={textStyles}
value={truncatedMessage}
mentionKeys={currentUser?.mentionKeys ?? EMPTY_MENTION_KEYS}
mentionKeys={mentionKeys}
/>
{isEdited ? (
<EditedIndicator

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useCallback, useState} from 'react';
import React, {useCallback, useMemo, useState} from 'react';
import {type LayoutChangeEvent, type StyleProp, View, type ViewStyle} from 'react-native';
import Files from '@components/files';
@ -105,7 +105,7 @@ const Body = ({
const isReplyPost = Boolean(post.rootId && (!isEphemeral || !hasBeenDeleted) && location !== THREAD);
const hasContent = Boolean((post.metadata?.embeds?.length || (appsEnabled && nBindings)) || nAttachments);
const replyBarStyle = useCallback((): StyleProp<ViewStyle>|undefined => {
const replyBarStyle = useMemo<StyleProp<ViewStyle>|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}
>
<View style={replyBarStyle()}/>
<View style={replyBarStyle}/>
{body}
{isFailed &&
<Failed

View file

@ -9,7 +9,6 @@ import Markdown from '@components/markdown';
import {isChannelMentions} from '@components/markdown/channel_mention/channel_mention';
import {SEARCH} from '@constants/screens';
import {useShowMoreAnimatedStyle} from '@hooks/show_more';
import {getMarkdownTextStyles, getMarkdownBlockStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -17,7 +16,7 @@ import ShowMoreButton from './show_more_button';
import type PostModel from '@typings/database/models/servers/post';
import type UserModel from '@typings/database/models/servers/user';
import type {HighlightWithoutNotificationKey, SearchPattern, UserMentionKey} from '@typings/global/markdown';
import type {HighlightWithoutNotificationKey, SearchPattern} from '@typings/global/markdown';
import type {AvailableScreens} from '@typings/screens/navigation';
type MessageProps = {
@ -36,7 +35,6 @@ type MessageProps = {
const SHOW_MORE_HEIGHT = 54;
const EMPTY_MENTION_KEYS: UserMentionKey[] = [];
const EMPTY_HIGHLIGHT_KEYS: HighlightWithoutNotificationKey[] = [];
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@ -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
>
<Markdown
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={post.channelId}
channelMentions={channelMentions}
imagesMetadata={post.metadata?.images}
@ -105,10 +110,9 @@ const Message = ({currentUser, isHighlightWithoutNotificationLicensed, highlight
layoutWidth={layoutWidth}
location={location}
postId={post.id}
textStyles={textStyles}
value={post.message}
mentionKeys={currentUser?.mentionKeys ?? EMPTY_MENTION_KEYS}
highlightKeys={isHighlightWithoutNotificationLicensed ? (currentUser?.highlightKeys ?? EMPTY_HIGHLIGHT_KEYS) : EMPTY_HIGHLIGHT_KEYS}
mentionKeys={mentionKeys}
highlightKeys={highlightKeys}
searchPatterns={searchPatterns}
theme={theme}
isUnsafeLinksPost={Boolean(post.props?.unsafe_links && post.props.unsafe_links !== '')}

View file

@ -9,7 +9,6 @@ import Markdown from '@components/markdown';
import {postTypeMessages} from '@components/post_list/combined_user_activity/messages';
import {Post} from '@constants';
import {useTheme} from '@context/theme';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {secureGetFromRecord, ensureString} from '@utils/types';
import {typography} from '@utils/typography';
@ -30,9 +29,6 @@ type RenderersProps = SystemMessageProps & {
styles: {
containerStyle: StyleProp<ViewStyle>;
messageStyle: StyleProp<TextStyle>;
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}
/>

View file

@ -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<ViewStyle>;
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(() => {

View file

@ -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<TextStyle>;
@ -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;

View file

@ -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>;
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 <Text style={baseStyle}>{literal}</Text>;
}, [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 <Text style={baseStyle}>{literal}</Text>;
}, [baseStyle]);
const renderAtMention = ({context, mentionName}: {context: string[]; mentionName: string}) => {
const renderAtMention = useCallback(({context, mentionName}: {context: string[]; mentionName: string}) => {
return (
<AtMention
textStyle={computeTextStyle(textStyle, baseStyle, context)}
mentionName={mentionName}
/>
);
};
}, [baseStyle, textStyle]);
const renderChannelLink = ({context, channelName}: MarkdownChannelMentionRenderer) => {
return (
<ChannelMention
linkStyle={textStyle.link}
textStyle={computeTextStyle(textStyle, [], context)}
channelName={channelName}
/>
);
};
const renderChannelLink = useCallback(({context, channelName}: MarkdownChannelMentionRenderer) => {
if (enableChannelLink) {
return (
<ChannelMention
linkStyle={textStyle.link}
textStyle={computeTextStyle(textStyle, baseStyle, context)}
channelName={channelName}
/>
);
}
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}
</Text>
);
}, [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<Parser | null>(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;

View file

@ -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}
/>
)}

View file

@ -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({
<View style={style.helpTextContainer} >
<Markdown
baseTextStyle={style.helpText}
textStyles={textStyles}
disableAtMentions={true}
location={location}
blockStyles={blockStyles}
value={disabledText}
theme={theme}
/>
@ -70,8 +65,6 @@ function Footer({
<View style={style.helpTextContainer} >
<Markdown
baseTextStyle={style.helpText}
textStyles={textStyles}
blockStyles={blockStyles}
disableAtMentions={true}
location={location}
value={helpText}
@ -83,8 +76,6 @@ function Footer({
<View style={style.errorTextContainer} >
<Markdown
baseTextStyle={style.errorText}
textStyles={textStyles}
blockStyles={blockStyles}
disableAtMentions={true}
location={location}
value={errorText}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {useEffect} from 'react';
import React, {useEffect, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Text, View} from 'react-native';
@ -9,7 +9,6 @@ import {fetchUsersByIds} from '@actions/remote/user';
import CompassIcon from '@components/compass_icon';
import Markdown from '@components/markdown';
import {useServerUrl} from '@context/server';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -88,8 +87,6 @@ const StatusUpdatePost = ({location, post, theme}: Props) => {
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 (
<View style={style.messageContainer}>
<Markdown
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={post.channelId}
postId={post.id}
textStyles={textStyles}
value={updatePosted}
mentionKeys={authorUsername ? [{key: authorUsername, caseSensitive: false}] : []}
mentionKeys={mentionKeys}
theme={theme}
location={location}
/>
<View style={style.updateContainer}>
<Markdown
baseTextStyle={style.message}
blockStyles={blockStyles}
channelId={post.channelId}
postId={post.id}
textStyles={textStyles}
value={post.message}
theme={theme}
location={location}
@ -144,7 +141,6 @@ const StatusUpdatePost = ({location, post, theme}: Props) => {
<Markdown
baseTextStyle={style.detailsText}
value={tasks}
textStyles={textStyles}
theme={theme}
location={location}
/>
@ -158,7 +154,6 @@ const StatusUpdatePost = ({location, post, theme}: Props) => {
participantIds={participantIds}
location={location}
baseTextStyle={style.detailsText}
textStyles={textStyles}
/>
</View>
</View>

View file

@ -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<TextStyle>;
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
<Markdown
baseTextStyle={baseTextStyle}
value={participants}
textStyles={textStyles}
theme={theme}
location={location}
/>

View file

@ -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)}
/>
</View>
</View>

View file

@ -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({
<View style={style.errorContainer} >
<Markdown
baseTextStyle={style.errorLabel}
textStyles={getMarkdownTextStyles(theme)}
blockStyles={getMarkdownBlockStyles(theme)}
location={Screens.APPS_FORM}
disableAtMentions={true}
value={error}

View file

@ -12,7 +12,6 @@ import {Screens, View as ViewConstants} from '@constants';
import {AppFieldTypes, SelectableAppFieldTypes} from '@constants/apps';
import {useTheme} from '@context/theme';
import {selectKeyboardType} from '@utils/integrations';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {makeStyleSheetFromTheme} from '@utils/theme';
const TEXT_DEFAULT_MAX_LENGTH = 150;
@ -81,7 +80,7 @@ function AppsFormField({
const handleChange = useCallback((newValue: string | boolean) => {
onChange(name, newValue);
}, [name]);
}, [name, onChange]);
const handleSelect = useCallback((newValue: SelectedDialogOption) => {
if (!newValue) {
@ -217,11 +216,8 @@ function AppsFormField({
>
<Markdown
value={field.description}
mentionKeys={[]}
disableAtMentions={true}
location={Screens.APPS_FORM}
blockStyles={getMarkdownBlockStyles(theme)}
textStyles={getMarkdownTextStyles(theme)}
baseTextStyle={style.markdownFieldText}
theme={theme}
/>

View file

@ -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) {
>
<RemoveMarkdown
value={bannerInfo.text}
textStyle={markdownTextStyle}
baseStyle={style.baseTextStyle}
/>
</Text>

View file

@ -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<ManagedConfig>();
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
<Markdown
channelId={channelId}
baseTextStyle={styles.header}
blockStyles={blockStyles}
disableBlockQuote={true}
disableCodeBlock={true}
disableGallery={true}
disableHeading={true}
disableTables={true}
location={Screens.CHANNEL_INFO}
textStyles={textStyles}
layoutHeight={48}
layoutWidth={100}
theme={theme}

View file

@ -17,7 +17,6 @@ import {useTheme} from '@context/theme';
import {useIsTablet} from '@hooks/device';
import {usePreventDoubleTap} from '@hooks/utils';
import {bottomSheetModalOptions, showModal, showModalOverCurrentContext} from '@screens/navigation';
import {getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
import {displayUsername} from '@utils/user';
@ -129,7 +128,6 @@ const Thread = ({author, channel, location, post, teammateNameDisplay, testID, t
const isTablet = useIsTablet();
const theme = useTheme();
const styles = getStyleSheet(theme);
const textStyles = getMarkdownTextStyles(theme);
const serverUrl = useServerUrl();
const [isChannelNamePressed, setIsChannelNamePressed] = useState<Boolean>(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
/>

View file

@ -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 (
<View style={style.introductionTextView}>
<Markdown
baseTextStyle={style.introductionText}
disableGallery={true}
textStyles={textStyles}
blockStyles={blockStyles}
value={value}
disableHashtags={true}
disableAtMentions={true}

View file

@ -11,7 +11,6 @@ import MessageNotViewable from '@components/illustrations/message_not_viewable';
import Markdown from '@components/markdown';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {getMarkdownBlockStyles, getMarkdownTextStyles} from '@utils/markdown';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
import {typography} from '@utils/typography';
@ -126,8 +125,6 @@ function PermalinkError({
disableChannelLink={true}
disableGallery={true}
disableHashtags={true}
textStyles={getMarkdownTextStyles(theme)}
blockStyles={getMarkdownBlockStyles(theme)}
location={Screens.PERMALINK}
/>
</View>

View file

@ -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 = ({
>
<Markdown
baseTextStyle={styles.baseText}
textStyles={textStyles}
blockStyles={blockStyles}
value={termsText}
disableHashtags={true}
disableAtMentions={true}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform, type TextStyle} from 'react-native';
import {Platform, StyleSheet, type TextStyle} from 'react-native';
import {Preferences} from '@constants';
@ -136,15 +136,15 @@ describe('Utility functions', () => {
});
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', () => {

View file

@ -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<TextStyle>, 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 {

View file

@ -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');

View file

@ -77,10 +77,6 @@ export function changeOpacity(oldColor: string, opacity: number): string {
return `rgba(${red},${green},${blue},${alpha * opacity})`;
}
export function concatStyles<T>(...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 = {