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:
parent
d713ea5b39
commit
1d8568ade8
51 changed files with 539 additions and 523 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -126,6 +126,7 @@ const ImageFile = ({
|
|||
tintDefaultSource={!file.localPath && !failed}
|
||||
onError={handleError}
|
||||
contentFit={contentFit}
|
||||
theme={theme}
|
||||
{...imageProps}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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()}
|
||||
/>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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[]) => {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 !== '')}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
Loading…
Reference in a new issue