diff --git a/app/components/emoji/emoji.tsx b/app/components/emoji/emoji.tsx
new file mode 100644
index 0000000000..8334aea4d1
--- /dev/null
+++ b/app/components/emoji/emoji.tsx
@@ -0,0 +1,152 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
+import withObservables from '@nozbe/with-observables';
+import React from 'react';
+import {
+ Platform,
+ StyleSheet,
+ Text,
+} from 'react-native';
+import FastImage from 'react-native-fast-image';
+import {of as of$} from 'rxjs';
+import {switchMap} from 'rxjs/operators';
+
+import {fetchCustomEmojiInBatch} from '@actions/remote/custom_emoji';
+import {useServerUrl} from '@context/server';
+import NetworkManager from '@managers/network_manager';
+import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
+import {observeConfigBooleanValue} from '@queries/servers/system';
+import {EmojiIndicesByAlias, Emojis} from '@utils/emoji';
+import {isUnicodeEmoji} from '@utils/emoji/helpers';
+
+import type {EmojiProps} from '@typings/components/emoji';
+import type {WithDatabaseArgs} from '@typings/database/database';
+
+const assetImages = new Map([['mattermost.png', require('@assets/images/emojis/mattermost.png')]]);
+
+const Emoji = (props: EmojiProps) => {
+ const {
+ customEmojis,
+ customEmojiStyle,
+ displayTextOnly,
+ emojiName,
+ literal = '',
+ testID,
+ textStyle,
+ } = props;
+ const serverUrl = useServerUrl();
+ let assetImage = '';
+ let unicode;
+ let imageUrl = '';
+ const name = emojiName.trim();
+ if (EmojiIndicesByAlias.has(name)) {
+ const emoji = Emojis[EmojiIndicesByAlias.get(name)!];
+ if (emoji.category === 'custom') {
+ assetImage = emoji.fileName;
+ } else {
+ unicode = emoji.image;
+ }
+ } else {
+ const custom = customEmojis.find((ce) => ce.name === name);
+ if (custom) {
+ try {
+ const client = NetworkManager.getClient(serverUrl);
+ imageUrl = client.getCustomEmojiImageUrl(custom.id);
+ } catch {
+ // do nothing
+ }
+ } else if (name && !isUnicodeEmoji(name)) {
+ fetchCustomEmojiInBatch(serverUrl, name);
+ }
+ }
+
+ let size = props.size;
+ let fontSize = size;
+ if (!size && textStyle) {
+ const flatten = StyleSheet.flatten(textStyle);
+ fontSize = flatten.fontSize;
+ size = fontSize;
+ }
+
+ if (displayTextOnly || (!imageUrl && !assetImage && !unicode)) {
+ return (
+
+ {literal}
+ );
+ }
+
+ const width = size;
+ const height = size;
+
+ if (unicode && !imageUrl) {
+ const codeArray = unicode.split('-');
+ const code = codeArray.reduce((acc: string, c: string) => {
+ return acc + String.fromCodePoint(parseInt(c, 16));
+ }, '');
+
+ return (
+
+ {code}
+
+ );
+ }
+
+ if (assetImage) {
+ const key = Platform.OS === 'android' ? (`${assetImage}-${height}-${width}`) : null;
+
+ const image = assetImages.get(assetImage);
+ if (!image) {
+ return null;
+ }
+ return (
+
+ );
+ }
+
+ if (!imageUrl) {
+ return null;
+ }
+
+ // Android can't change the size of an image after its first render, so
+ // force a new image to be rendered when the size changes
+ const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
+
+ return (
+
+ );
+};
+
+const withCustomEmojis = withObservables(['emojiName'], ({database, emojiName}: WithDatabaseArgs & {emojiName: string}) => {
+ const hasEmojiBuiltIn = EmojiIndicesByAlias.has(emojiName);
+
+ const displayTextOnly = hasEmojiBuiltIn ? of$(false) : observeConfigBooleanValue(database, 'EnableCustomEmoji').pipe(
+ switchMap((value) => of$(!value)),
+ );
+
+ return {
+ displayTextOnly,
+ customEmojis: hasEmojiBuiltIn ? of$([]) : queryCustomEmojisByName(database, [emojiName]).observe(),
+ };
+});
+
+export default withDatabase(withCustomEmojis(Emoji));
diff --git a/app/components/emoji/index.tsx b/app/components/emoji/index.tsx
index 090fe3ad30..918f4bd23a 100644
--- a/app/components/emoji/index.tsx
+++ b/app/components/emoji/index.tsx
@@ -1,165 +1,21 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import {withDatabase} from '@nozbe/watermelondb/DatabaseProvider';
-import withObservables from '@nozbe/with-observables';
-import React from 'react';
-import {
- Platform,
- StyleProp,
- StyleSheet,
- Text,
- TextStyle,
-} from 'react-native';
-import FastImage, {ImageStyle} from 'react-native-fast-image';
-import {of as of$} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import React, {useMemo} from 'react';
-import {fetchCustomEmojiInBatch} from '@actions/remote/custom_emoji';
-import {useServerUrl} from '@context/server';
-import NetworkManager from '@managers/network_manager';
-import {queryCustomEmojisByName} from '@queries/servers/custom_emoji';
-import {observeConfigBooleanValue} from '@queries/servers/system';
-import {EmojiIndicesByAlias, Emojis} from '@utils/emoji';
-import {isUnicodeEmoji} from '@utils/emoji/helpers';
+import {EmojiComponent, EmojiProps} from '@typings/components/emoji';
-import type {WithDatabaseArgs} from '@typings/database/database';
-import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
+let emojiComponent: EmojiComponent;
-const assetImages = new Map([['mattermost.png', require('@assets/images/emojis/mattermost.png')]]);
-
-type Props = {
- emojiName: string;
- displayTextOnly?: boolean;
- literal?: string;
- size?: number;
- textStyle?: StyleProp;
- customEmojiStyle?: StyleProp;
- customEmojis: CustomEmojiModel[];
- testID?: string;
-}
-
-const Emoji = (props: Props) => {
- const {
- customEmojis,
- customEmojiStyle,
- displayTextOnly,
- emojiName,
- literal = '',
- testID,
- textStyle,
- } = props;
- const serverUrl = useServerUrl();
- let assetImage = '';
- let unicode;
- let imageUrl = '';
- const name = emojiName.trim();
- if (EmojiIndicesByAlias.has(name)) {
- const emoji = Emojis[EmojiIndicesByAlias.get(name)!];
- if (emoji.category === 'custom') {
- assetImage = emoji.fileName;
- } else {
- unicode = emoji.image;
+const EmojiWrapper = (props: Omit) => {
+ const Emoji = useMemo(() => {
+ if (!emojiComponent) {
+ emojiComponent = require('./emoji').default;
}
- } else {
- const custom = customEmojis.find((ce) => ce.name === name);
- if (custom) {
- try {
- const client = NetworkManager.getClient(serverUrl);
- imageUrl = client.getCustomEmojiImageUrl(custom.id);
- } catch {
- // do nothing
- }
- } else if (name && !isUnicodeEmoji(name)) {
- fetchCustomEmojiInBatch(serverUrl, name);
- }
- }
+ return emojiComponent;
+ }, []);
- let size = props.size;
- let fontSize = size;
- if (!size && textStyle) {
- const flatten = StyleSheet.flatten(textStyle);
- fontSize = flatten.fontSize;
- size = fontSize;
- }
-
- if (displayTextOnly || (!imageUrl && !assetImage && !unicode)) {
- return (
-
- {literal}
- );
- }
-
- const width = size;
- const height = size;
-
- if (unicode && !imageUrl) {
- const codeArray = unicode.split('-');
- const code = codeArray.reduce((acc: string, c: string) => {
- return acc + String.fromCodePoint(parseInt(c, 16));
- }, '');
-
- return (
-
- {code}
-
- );
- }
-
- if (assetImage) {
- const key = Platform.OS === 'android' ? (`${assetImage}-${height}-${width}`) : null;
-
- const image = assetImages.get(assetImage);
- if (!image) {
- return null;
- }
- return (
-
- );
- }
-
- if (!imageUrl) {
- return null;
- }
-
- // Android can't change the size of an image after its first render, so
- // force a new image to be rendered when the size changes
- const key = Platform.OS === 'android' ? (`${imageUrl}-${height}-${width}`) : null;
-
- return (
-
- );
+ return ();
};
-const withCustomEmojis = withObservables(['emojiName'], ({database, emojiName}: WithDatabaseArgs & {emojiName: string}) => {
- const hasEmojiBuiltIn = EmojiIndicesByAlias.has(emojiName);
-
- const displayTextOnly = hasEmojiBuiltIn ? of$(false) : observeConfigBooleanValue(database, 'EnableCustomEmoji').pipe(
- switchMap((value) => of$(!value)),
- );
-
- return {
- displayTextOnly,
- customEmojis: hasEmojiBuiltIn ? of$([]) : queryCustomEmojisByName(database, [emojiName]).observe(),
- };
-});
-
-export default withDatabase(withCustomEmojis(Emoji));
+export default EmojiWrapper;
diff --git a/app/components/markdown/markdown.tsx b/app/components/markdown/markdown.tsx
index 4bdf7a6d5e..a7efb3a11b 100644
--- a/app/components/markdown/markdown.tsx
+++ b/app/components/markdown/markdown.tsx
@@ -9,13 +9,13 @@ import {Dimensions, GestureResponderEvent, Platform, StyleProp, Text, TextStyle,
import CompassIcon from '@components/compass_icon';
import Emoji from '@components/emoji';
import FormattedText from '@components/formatted_text';
-import Hashtag from '@components/markdown/hashtag';
import {computeTextStyle} from '@utils/markdown';
import {blendColors, changeOpacity, concatStyles, makeStyleSheetFromTheme} from '@utils/theme';
import {getScheme} from '@utils/url';
import AtMention from './at_mention';
import ChannelMention, {ChannelMentions} from './channel_mention';
+import Hashtag from './hashtag';
import MarkdownBlockQuote from './markdown_block_quote';
import MarkdownCodeBlock from './markdown_code_block';
import MarkdownImage from './markdown_image';
diff --git a/app/components/markdown/markdown_code_block/index.tsx b/app/components/markdown/markdown_code_block/index.tsx
index 8e0dad2ed2..20549d4270 100644
--- a/app/components/markdown/markdown_code_block/index.tsx
+++ b/app/components/markdown/markdown_code_block/index.tsx
@@ -3,14 +3,13 @@
import {useManagedConfig} from '@mattermost/react-native-emm';
import Clipboard from '@react-native-clipboard/clipboard';
-import React, {useCallback} from 'react';
+import React, {useCallback, useMemo} from 'react';
import {useIntl} from 'react-intl';
import {Keyboard, StyleSheet, Text, TextStyle, TouchableOpacity, View} from 'react-native';
import {useSafeAreaInsets} from 'react-native-safe-area-context';
import FormattedText from '@components/formatted_text';
import SlideUpPanelItem, {ITEM_HEIGHT} from '@components/slide_up_panel_item';
-import SyntaxHighlighter from '@components/syntax_highlight';
import {Screens} from '@constants';
import {useTheme} from '@context/theme';
import {bottomSheet, dismissBottomSheet, goToScreen} from '@screens/navigation';
@@ -19,6 +18,8 @@ import {getHighlightLanguageFromNameOrAlias, getHighlightLanguageName} from '@ut
import {preventDoubleTap} from '@utils/tap';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import type {SyntaxHiglightProps} from '@typings/components/syntax_highlight';
+
type MarkdownCodeBlockProps = {
language: string;
content: string;
@@ -27,6 +28,8 @@ type MarkdownCodeBlockProps = {
const MAX_LINES = 4;
+let syntaxHighlighter: (props: SyntaxHiglightProps) => JSX.Element;
+
const getStyleSheet = makeStyleSheetFromTheme((theme) => {
return {
bottomSheet: {
@@ -70,6 +73,13 @@ const MarkdownCodeBlock = ({language = '', content, textStyle}: MarkdownCodeBloc
const theme = useTheme();
const insets = useSafeAreaInsets();
const style = getStyleSheet(theme);
+ const SyntaxHighlighter = useMemo(() => {
+ if (!syntaxHighlighter) {
+ syntaxHighlighter = require('@components/syntax_highlight').default;
+ }
+
+ return syntaxHighlighter;
+ }, []);
const handlePress = useCallback(preventDoubleTap(() => {
const screen = Screens.CODE;
diff --git a/app/components/syntax_highlight/index.tsx b/app/components/syntax_highlight/index.tsx
index 963be47594..a83b2e3e01 100644
--- a/app/components/syntax_highlight/index.tsx
+++ b/app/components/syntax_highlight/index.tsx
@@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import React, {useCallback, useMemo} from 'react';
-import {StyleSheet, TextStyle, View} from 'react-native';
+import {StyleSheet, View} from 'react-native';
import SyntaxHighlighter from 'react-syntax-highlighter';
import {github, monokai, solarizedDark, solarizedLight} from 'react-syntax-highlighter/dist/cjs/styles/hljs';
@@ -10,12 +10,7 @@ import {useTheme} from '@context/theme';
import CodeHighlightRenderer from './renderer';
-type Props = {
- code: string;
- language: string;
- textStyle: TextStyle;
- selectable?: boolean;
-}
+import type {SyntaxHiglightProps} from '@typings/components/syntax_highlight';
const codeTheme: Record = {
github,
@@ -34,7 +29,7 @@ const styles = StyleSheet.create({
},
});
-const Highlighter = ({code, language, textStyle, selectable = false}: Props) => {
+const Highlighter = ({code, language, textStyle, selectable = false}: SyntaxHiglightProps) => {
const theme = useTheme();
const style = codeTheme[theme.codeTheme] || github;
const preTagStyle = useMemo(() => [
diff --git a/types/components/emoji.ts b/types/components/emoji.ts
new file mode 100644
index 0000000000..8daf4074f7
--- /dev/null
+++ b/types/components/emoji.ts
@@ -0,0 +1,19 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import type CustomEmojiModel from '@typings/database/models/servers/custom_emoji';
+import type {StyleProp, TextStyle} from 'react-native';
+import type {ImageStyle} from 'react-native-fast-image';
+
+export type EmojiProps = {
+ emojiName: string;
+ displayTextOnly?: boolean;
+ literal?: string;
+ size?: number;
+ textStyle?: StyleProp;
+ customEmojiStyle?: StyleProp;
+ customEmojis: CustomEmojiModel[];
+ testID?: string;
+}
+
+export type EmojiComponent = (props: Omit) => JSX.Element;
diff --git a/types/components/syntax_highlight.ts b/types/components/syntax_highlight.ts
new file mode 100644
index 0000000000..d8f959e5f8
--- /dev/null
+++ b/types/components/syntax_highlight.ts
@@ -0,0 +1,11 @@
+// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
+// See LICENSE.txt for license information.
+
+import {TextStyle} from 'react-native';
+
+export type SyntaxHiglightProps = {
+ code: string;
+ language: string;
+ textStyle: TextStyle;
+ selectable?: boolean;
+};