Chore: Migrate Markdown to Typescript (#3558)

* Chore: Migrate Markdown to TS

* Chore: Migrate Markdown to TS

* minor tweak

* added preview where markdown was preview and fixed params within markdown

* removed ts-ignore

* fix lint

* removed numbersofline={0} and default value to numberOfLines=1

* change how to import markdown preview and remove numberOfLines

* using useTheme inside markdownPreview and remove theme from components

* minor tweak on interfaces

* isNewMarkdown return as boolean

* minor tweaks

* minor tweaks

* removed unused component

* fixed markdown stories

* updated snapshot because removed numberOfLines={0} from message/content

* create IEmoji.ts in definitions and refactor all places where getCustomEmoji was called

* onLinkPress typed

* todo: refactor navtoroominfo

* formatText.test.ts

* markdown stories to typescript too

* minor tweak

* IMessage definition

* refactor: update new types and interfaces for use ISubscription

* refactor: update threadItem for use new MarkdownPreview

* refactor: rollback wrong file commited

* formatHyperlink

* fix lint

* updated item story shot

* refactor and refactor some types

* Remove non-null assertion

* Minor change on useRealName

* tweak

Co-authored-by: AlexAlexandre <alexalexandrejr@gmail.com>
Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Reinaldo Neto 2022-02-17 12:27:01 -03:00 committed by GitHub
parent 9ec598407d
commit 5ac4700d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
53 changed files with 353 additions and 396 deletions

View File

@ -44,7 +44,7 @@ const Avatar = React.memo(
if (emoji) { if (emoji) {
image = ( image = (
<Emoji <Emoji
theme={theme} theme={theme!}
baseUrl={server} baseUrl={server}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
isMessageContainsOnlyEmoji isMessageContainsOnlyEmoji

View File

@ -1,3 +1,5 @@
import { TGetCustomEmoji } from '../../definitions/IEmoji';
export interface IAvatar { export interface IAvatar {
server?: string; server?: string;
style?: any; style?: any;
@ -14,7 +16,7 @@ export interface IAvatar {
}; };
theme?: string; theme?: string;
onPress?: () => void; onPress?: () => void;
getCustomEmoji?: () => any; getCustomEmoji?: TGetCustomEmoji;
avatarETag?: string; avatarETag?: string;
isStatic?: boolean | string; isStatic?: boolean | string;
rid?: string; rid?: string;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
import { ICustomEmoji } from './interfaces'; import { ICustomEmoji } from '../../definitions/IEmoji';
const CustomEmoji = React.memo( const CustomEmoji = React.memo(
({ baseUrl, emoji, style }: ICustomEmoji) => ( ({ baseUrl, emoji, style }: ICustomEmoji) => (

View File

@ -5,7 +5,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import styles from './styles'; import styles from './styles';
import CustomEmoji from './CustomEmoji'; import CustomEmoji from './CustomEmoji';
import scrollPersistTaps from '../../utils/scrollPersistTaps'; import scrollPersistTaps from '../../utils/scrollPersistTaps';
import { IEmoji, IEmojiCategory } from './interfaces'; import { IEmoji, IEmojiCategory } from '../../definitions/IEmoji';
const EMOJI_SIZE = 50; const EMOJI_SIZE = 50;

View File

@ -17,7 +17,7 @@ import shortnameToUnicode from '../../utils/shortnameToUnicode';
import log from '../../utils/log'; import log from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { IEmoji } from './interfaces'; import { IEmoji } from '../../definitions/IEmoji';
const scrollProps = { const scrollProps = {
keyboardShouldPersistTaps: 'always', keyboardShouldPersistTaps: 'always',

View File

@ -10,8 +10,8 @@ import database from '../../lib/database';
import { Button } from '../ActionSheet'; import { Button } from '../ActionSheet';
import { useDimensions } from '../../dimensions'; import { useDimensions } from '../../dimensions';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { IEmoji } from '../../definitions/IEmoji';
import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji'; import { TFrequentlyUsedEmojiModel } from '../../definitions/IFrequentlyUsedEmoji';
import { IEmoji } from '../EmojiPicker/interfaces';
interface IHeader { interface IHeader {
handleReaction: Function; handleReaction: Function;

View File

@ -6,7 +6,7 @@ import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import styles from '../styles'; import styles from '../styles';
import MessageboxContext from '../Context'; import MessageboxContext from '../Context';
import CustomEmoji from '../../EmojiPicker/CustomEmoji'; import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import { IEmoji } from '../../EmojiPicker/interfaces'; import { IEmoji } from '../../../definitions/IEmoji';
interface IMessageBoxMentionEmoji { interface IMessageBoxMentionEmoji {
item: IEmoji; item: IEmoji;

View File

@ -8,7 +8,7 @@ import FixedMentionItem from './FixedMentionItem';
import MentionEmoji from './MentionEmoji'; import MentionEmoji from './MentionEmoji';
import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants'; import { MENTIONS_TRACKING_TYPE_EMOJIS, MENTIONS_TRACKING_TYPE_COMMANDS, MENTIONS_TRACKING_TYPE_CANNED } from '../constants';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { IEmoji } from '../../EmojiPicker/interfaces'; import { IEmoji } from '../../../definitions/IEmoji';
interface IMessageBoxMentionItem { interface IMessageBoxMentionItem {
item: { item: {

View File

@ -3,10 +3,11 @@ import { StyleSheet, Text, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { IMessage } from '../../definitions/IMessage';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -42,11 +43,7 @@ const styles = StyleSheet.create({
interface IMessageBoxReplyPreview { interface IMessageBoxReplyPreview {
replying: boolean; replying: boolean;
message: { message: IMessage;
ts: Date;
msg: string;
u: any;
};
Message_TimeFormat: string; Message_TimeFormat: string;
close(): void; close(): void;
baseUrl: string; baseUrl: string;
@ -57,17 +54,7 @@ interface IMessageBoxReplyPreview {
} }
const ReplyPreview = React.memo( const ReplyPreview = React.memo(
({ ({ message, Message_TimeFormat, replying, close, theme, useRealName }: IMessageBoxReplyPreview) => {
message,
Message_TimeFormat,
baseUrl,
username,
replying,
getCustomEmoji,
close,
theme,
useRealName
}: IMessageBoxReplyPreview) => {
if (!replying) { if (!replying) {
return null; return null;
} }
@ -82,16 +69,7 @@ const ReplyPreview = React.memo(
</Text> </Text>
<Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View> </View>
{/* @ts-ignore*/} <MarkdownPreview msg={message.msg} />
<Markdown
msg={message.msg}
baseUrl={baseUrl}
username={username}
getCustomEmoji={getCustomEmoji}
numberOfLines={1}
preview
theme={theme}
/>
</View> </View>
<CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} /> <CustomIcon name='close' color={themes[theme].auxiliaryText} size={20} style={styles.close} onPress={close} />
</View> </View>

View File

@ -47,6 +47,7 @@ import Navigation from '../../lib/Navigation';
import { withActionSheet } from '../ActionSheet'; import { withActionSheet } from '../ActionSheet';
import { sanitizeLikeString } from '../../lib/database/utils'; import { sanitizeLikeString } from '../../lib/database/utils';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { IMessage } from '../../definitions/IMessage';
import { forceJpgExtension } from './forceJpgExtension'; import { forceJpgExtension } from './forceJpgExtension';
if (isAndroid) { if (isAndroid) {
@ -74,12 +75,7 @@ const videoPickerConfig = {
interface IMessageBoxProps { interface IMessageBoxProps {
rid: string; rid: string;
baseUrl: string; baseUrl: string;
message: { message: IMessage;
u: {
username: string;
};
id: any;
};
replying: boolean; replying: boolean;
editing: boolean; editing: boolean;
threadsEnabled: boolean; threadsEnabled: boolean;
@ -1072,7 +1068,6 @@ class MessageBox extends Component<IMessageBoxProps, IMessageBoxState> {
const replyPreview = !recording ? ( const replyPreview = !recording ? (
<ReplyPreview <ReplyPreview
// @ts-ignore
message={message} message={message}
close={replyCancel} close={replyCancel}
username={user.username} username={user.username}

View File

@ -9,6 +9,7 @@ import { CustomIcon } from '../lib/Icons';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { TGetCustomEmoji } from '../definitions/IEmoji';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -66,7 +67,7 @@ interface IItem {
}; };
user?: { username: any }; user?: { username: any };
baseUrl?: string; baseUrl?: string;
getCustomEmoji?: Function; getCustomEmoji?: TGetCustomEmoji;
theme?: string; theme?: string;
} }

View File

@ -4,7 +4,7 @@ import { StyleSheet, Text, TouchableOpacity, View } from 'react-native';
import I18n from '../../i18n'; import I18n from '../../i18n';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import RoomTypeIcon from '../RoomTypeIcon'; import RoomTypeIcon from '../RoomTypeIcon';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
@ -101,16 +101,7 @@ const SubTitle = React.memo(({ usersTyping, subtitle, renderFunc, theme, scale }
// subtitle // subtitle
if (subtitle) { if (subtitle) {
return ( return <MarkdownPreview msg={subtitle} style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]} />;
// @ts-ignore
<Markdown
preview
msg={subtitle}
style={[styles.subtitle, { fontSize, color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/>
);
} }
return null; return null;
@ -126,10 +117,7 @@ const HeaderTitle = React.memo(({ title, tmid, prid, scale, theme, testID }: TRo
); );
} }
return ( return <MarkdownPreview msg={title} style={[styles.title, titleStyle]} testID={testID} />;
// @ts-ignore
<Markdown preview msg={title} style={[styles.title, titleStyle]} numberOfLines={1} theme={theme} testID={testID} />
);
}); });
const Header = React.memo( const Header = React.memo(

View File

@ -3,7 +3,7 @@ import React, { useContext } from 'react';
import { StyleSheet, Text } from 'react-native'; import { StyleSheet, Text } from 'react-native';
import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKitModal } from '@rocket.chat/ui-kit'; import { BLOCK_CONTEXT, UiKitParserMessage, UiKitParserModal, uiKitMessage, uiKitModal } from '@rocket.chat/ui-kit';
import Markdown from '../markdown'; import Markdown, { MarkdownPreview } from '../markdown';
import Button from '../Button'; import Button from '../Button';
import TextInput from '../TextInput'; import TextInput from '../TextInput';
import { textParser, useBlockContext } from './utils'; import { textParser, useBlockContext } from './utils';
@ -49,10 +49,10 @@ class MessageParser extends UiKitParserMessage {
} }
const isContext = context === BLOCK_CONTEXT.CONTEXT; const isContext = context === BLOCK_CONTEXT.CONTEXT;
return ( if (isContext) {
// @ts-ignore return <MarkdownPreview msg={text} style={[isContext && { color: themes[theme].auxiliaryText }]} numberOfLines={0} />;
<Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} preview={isContext} /> }
); return <Markdown msg={text} theme={theme} style={[isContext && { color: themes[theme].auxiliaryText }]} />;
} }
button(element: any, context: any) { button(element: any, context: any) {

View File

@ -8,10 +8,10 @@ import { events, logEvent } from '../../utils/log';
interface IAtMention { interface IAtMention {
mention: string; mention: string;
username: string; username?: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
style?: any; style?: any;
useRealName: boolean; useRealName?: boolean;
mentions: any; mentions: any;
} }
@ -51,7 +51,9 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
t: 'd', t: 'd',
rid: user && user._id rid: user && user._id
}; };
if (navToRoomInfo) {
navToRoomInfo(navParam); navToRoomInfo(navParam);
}
}; };
if (user) { if (user) {

View File

@ -12,8 +12,8 @@ interface IEmoji {
getCustomEmoji?: Function; getCustomEmoji?: Function;
baseUrl: string; baseUrl: string;
customEmojis?: any; customEmojis?: any;
style: object; style?: object;
theme?: string; theme: string;
onEmojiSelected?: Function; onEmojiSelected?: Function;
tabEmojiStyle?: object; tabEmojiStyle?: object;
} }
@ -32,7 +32,7 @@ const Emoji = React.memo(
); );
} }
return ( return (
<Text style={[{ color: themes[theme!].bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}> <Text style={[{ color: themes[theme].bodyText }, isMessageContainsOnlyEmoji ? styles.textBig : styles.text, style]}>
{emojiUnicode} {emojiUnicode}
</Text> </Text>
); );

View File

@ -1,19 +1,16 @@
import React from 'react'; import React from 'react';
import { Text, TextStyle } from 'react-native'; import { Text, TextStyle, StyleProp } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme'; import { useTheme } from '../../theme';
import { IUserChannel } from './interfaces';
import styles from './styles'; import styles from './styles';
interface IHashtag { interface IHashtag {
hashtag: string; hashtag: string;
navToRoomInfo: Function; navToRoomInfo?: Function;
style?: TextStyle[]; style?: StyleProp<TextStyle>[];
channels: { channels?: IUserChannel[];
[index: number]: string | number;
name: string;
_id: number;
}[];
} }
const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => { const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => {
@ -21,11 +18,13 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IH
const handlePress = () => { const handlePress = () => {
const index = channels?.findIndex(channel => channel.name === hashtag); const index = channels?.findIndex(channel => channel.name === hashtag);
if (index && navToRoomInfo) {
const navParam = { const navParam = {
t: 'c', t: 'c',
rid: channels[index]._id rid: channels?.[index]._id
}; };
navToRoomInfo(navParam); navToRoomInfo(navParam);
}
}; };
if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) { if (channels && channels.length && channels.findIndex(channel => channel.name === hashtag) !== -1) {

View File

@ -7,12 +7,13 @@ import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TOnLinkPress } from './interfaces';
interface ILink { interface ILink {
children: JSX.Element; children: JSX.Element;
link: string; link: string;
theme: string; theme: string;
onLinkPress: Function; onLinkPress?: TOnLinkPress;
} }
const Link = React.memo(({ children, link, theme, onLinkPress }: ILink) => { const Link = React.memo(({ children, link, theme, onLinkPress }: ILink) => {

View File

@ -5,7 +5,7 @@ interface IList {
ordered: boolean; ordered: boolean;
start: number; start: number;
tight: boolean; tight: boolean;
numberOfLines: number; numberOfLines?: number;
} }
const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => { const List = React.memo(({ children, ordered, tight, start = 1, numberOfLines = 0 }: IList) => {

View File

@ -0,0 +1,44 @@
import React from 'react';
import { StyleProp, Text, TextStyle } from 'react-native';
import removeMarkdown from 'remove-markdown';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import { themes } from '../../constants/colors';
import { formatText } from './formatText';
import { useTheme } from '../../theme';
import styles from './styles';
import { formatHyperlink } from './formatHyperlink';
interface IMarkdownPreview {
msg?: string;
numberOfLines?: number;
testID?: string;
style?: StyleProp<TextStyle>[];
}
const MarkdownPreview = ({ msg, numberOfLines = 1, testID, style = [] }: IMarkdownPreview): React.ReactElement | null => {
if (!msg) {
return null;
}
const { theme } = useTheme();
let m = formatText(msg);
m = formatHyperlink(m);
m = shortnameToUnicode(m);
// Removes sequential empty spaces
m = m.replace(/\s+/g, ' ');
m = removeMarkdown(m);
m = m.replace(/\n+/g, ' ');
return (
<Text
accessibilityLabel={m}
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
numberOfLines={numberOfLines}
testID={testID}>
{m}
</Text>
);
};
export default MarkdownPreview;

View File

@ -0,0 +1,26 @@
import { formatHyperlink } from './formatHyperlink';
describe('FormatText', () => {
test('empty to be empty', () => {
expect(formatHyperlink('')).toBe('');
});
test('A123 to be A123', () => {
expect(formatHyperlink('A123')).toBe('A123');
});
test('Format <http://link|Text> to be <http://link|Text>', () => {
expect(formatHyperlink('<http://link|Text>')).toBe('<http://link|Text>');
});
test('Format "[ ](https://open.rocket.chat/) Test" to be Test', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) Test')).toBe('Test');
});
test('Format "[Open](https://open.rocket.chat/) Test" to be Test', () => {
expect(formatHyperlink('[Open](https://open.rocket.chat/) Test')).toBe('[Open](https://open.rocket.chat/) Test');
});
test('render test (arabic)', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) اختبا')).toBe('اختبا');
});
test('render test (russian)', () => {
expect(formatHyperlink('[ ](https://open.rocket.chat/) тест123')).toBe(ест123');
});
});

View File

@ -0,0 +1,3 @@
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
export const formatHyperlink = (text: string): string => text.replace(/^\[([\s]*)\]\(([^)]*)\)\s/, '').trim();

View File

@ -0,0 +1,20 @@
import { formatText } from './formatText';
describe('FormatText', () => {
test('empty to be empty', () => {
expect(formatText('')).toBe('');
});
test('A123 to be A123', () => {
expect(formatText('A123')).toBe('A123');
});
test('Format <http://link|Text> to be [Text](http://link)', () => {
expect(formatText('<http://link|Text>')).toBe('[Text](http://link)');
});
test('render test (arabic)', () => {
expect(formatText('اختبا <http://link|ر123>')).toBe('اختبا [ر123](http://link)');
});
test('render test (russian)', () => {
expect(formatText('<http://link|тест123>')).toBe('[тест123](http://link)');
});
});

View File

@ -0,0 +1,6 @@
// Support <http://link|Text>
export const formatText = (text: string): string =>
text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${title}](${url})`
);

View File

@ -1,12 +1,9 @@
import React, { PureComponent } from 'react'; import React, { PureComponent } from 'react';
import { Image, Text } from 'react-native'; import { Image, StyleProp, Text, TextStyle } from 'react-native';
import { Node, Parser } from 'commonmark'; import { Node, Parser } from 'commonmark';
import Renderer from 'commonmark-react-renderer'; import Renderer from 'commonmark-react-renderer';
import removeMarkdown from 'remove-markdown';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { UserMention } from '../message/interfaces';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MarkdownLink from './Link'; import MarkdownLink from './Link';
@ -23,43 +20,39 @@ import mergeTextNodes from './mergeTextNodes';
import styles from './styles'; import styles from './styles';
import { isValidURL } from '../../utils/url'; import { isValidURL } from '../../utils/url';
import NewMarkdown from './new'; import NewMarkdown from './new';
import { formatText } from './formatText';
import { IUserMention, IUserChannel, TOnLinkPress } from './interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import { formatHyperlink } from './formatHyperlink';
export { default as MarkdownPreview } from './Preview';
interface IMarkdownProps { interface IMarkdownProps {
msg?: string; msg?: string;
md: MarkdownAST;
mentions: UserMention[];
getCustomEmoji: Function;
baseUrl: string;
username: string;
tmid: string;
isEdited: boolean;
numberOfLines: number;
customEmojis: boolean;
useRealName: boolean;
channels: {
name: string;
_id: number;
}[];
enableMessageParser: boolean;
navToRoomInfo: Function;
preview: boolean;
theme: string; theme: string;
testID: string; md?: MarkdownAST;
style: any; mentions?: IUserMention[];
onLinkPress: Function; getCustomEmoji?: TGetCustomEmoji;
baseUrl?: string;
username?: string;
tmid?: string;
isEdited?: boolean;
numberOfLines?: number;
customEmojis?: boolean;
useRealName?: boolean;
channels?: IUserChannel[];
enableMessageParser?: boolean;
// TODO: Refactor when migrate Room
navToRoomInfo?: Function;
testID?: string;
style?: StyleProp<TextStyle>[];
onLinkPress?: TOnLinkPress;
} }
type TLiteral = { type TLiteral = {
literal: string; literal: string;
}; };
// Support <http://link|Text>
const formatText = (text: string) =>
text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${title}](${url})`
);
const emojiRanges = [ const emojiRanges = [
'\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421 '\u00a9|\u00ae|[\u2000-\u3300]|\ud83c[\ud000-\udfff]|\ud83d[\ud000-\udfff]|\ud83e[\ud000-\udfff]', // unicode emoji from https://www.regextester.com/106421
':.{1,40}:', // custom emoji ':.{1,40}:', // custom emoji
@ -148,7 +141,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
get isNewMarkdown(): boolean { get isNewMarkdown(): boolean {
const { md, enableMessageParser } = this.props; const { md, enableMessageParser } = this.props;
return enableMessageParser && !!md; return !!enableMessageParser && !!md;
} }
editedMessage = (ast: any) => { editedMessage = (ast: any) => {
@ -244,7 +237,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderAtMention = ({ mentionName }: { mentionName: string }) => { renderAtMention = ({ mentionName }: { mentionName: string }) => {
const { username, mentions, navToRoomInfo, useRealName, style } = this.props; const { username = '', mentions, navToRoomInfo, useRealName, style } = this.props;
return ( return (
<MarkdownAtMention <MarkdownAtMention
mentions={mentions} mentions={mentions}
@ -258,7 +251,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderEmoji = ({ literal }: TLiteral) => { renderEmoji = ({ literal }: TLiteral) => {
const { getCustomEmoji, baseUrl, customEmojis, style, theme } = this.props; const { getCustomEmoji, baseUrl = '', customEmojis, style, theme } = this.props;
return ( return (
<MarkdownEmoji <MarkdownEmoji
literal={literal} literal={literal}
@ -343,18 +336,13 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
const { const {
msg, msg,
md, md,
numberOfLines,
preview = false,
theme,
style = [],
testID,
mentions, mentions,
channels, channels,
navToRoomInfo, navToRoomInfo,
useRealName, useRealName,
username, username = '',
getCustomEmoji, getCustomEmoji,
baseUrl, baseUrl = '',
onLinkPress onLinkPress
} = this.props; } = this.props;
@ -362,7 +350,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
return null; return null;
} }
if (this.isNewMarkdown && !preview) { if (this.isNewMarkdown) {
return ( return (
<NewMarkdown <NewMarkdown
username={username} username={username}
@ -379,28 +367,7 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
} }
let m = formatText(msg); let m = formatText(msg);
m = formatHyperlink(m);
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'
// Return: 'Test'
m = m.replace(/^\[([\s]*)\]\(([^)]*)\)\s/, '').trim();
if (preview) {
m = shortnameToUnicode(m);
// Removes sequential empty spaces
m = m.replace(/\s+/g, ' ');
m = removeMarkdown(m);
m = m.replace(/\n+/g, ' ');
return (
<Text
accessibilityLabel={m}
style={[styles.text, { color: themes[theme].bodyText }, ...style]}
numberOfLines={numberOfLines}
testID={testID}>
{m}
</Text>
);
}
let ast = parser.parse(m); let ast = parser.parse(m);
ast = mergeTextNodes(ast); ast = mergeTextNodes(ast);
this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3; this.isMessageContainsOnlyEmoji = isOnlyEmoji(m) && emojiCount(m) <= 3;

View File

@ -0,0 +1,12 @@
export interface IUserMention {
_id: string;
username: string;
name?: string;
}
export interface IUserChannel {
name: string;
_id: string;
}
export type TOnLinkPress = (link: string) => void;

View File

@ -1,17 +1,14 @@
import React from 'react'; import React from 'react';
import { UserMention } from '../../message/interfaces'; import { IUserMention, IUserChannel } from '../interfaces';
interface IMarkdownContext { interface IMarkdownContext {
mentions: UserMention[]; mentions?: IUserMention[];
channels: { channels?: IUserChannel[];
name: string; useRealName?: boolean;
_id: number; username?: string;
}[]; baseUrl?: string;
useRealName: boolean; navToRoomInfo?: Function;
username: string;
baseUrl: string;
navToRoomInfo: Function;
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: Function;
} }

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import isEmpty from 'lodash/isEmpty';
import Quote from './Quote'; import Quote from './Quote';
import Paragraph from './Paragraph'; import Paragraph from './Paragraph';
@ -8,21 +9,18 @@ import Code from './Code';
import BigEmoji from './BigEmoji'; import BigEmoji from './BigEmoji';
import OrderedList from './OrderedList'; import OrderedList from './OrderedList';
import UnorderedList from './UnorderedList'; import UnorderedList from './UnorderedList';
import { UserMention } from '../../message/interfaces'; import { IUserMention, IUserChannel, TOnLinkPress } from '../interfaces';
import TaskList from './TaskList'; import TaskList from './TaskList';
import MarkdownContext from './MarkdownContext'; import MarkdownContext from './MarkdownContext';
interface IBodyProps { interface IBodyProps {
tokens: MarkdownAST; tokens?: MarkdownAST;
mentions: UserMention[]; mentions?: IUserMention[];
channels: { channels?: IUserChannel[];
name: string;
_id: number;
}[];
getCustomEmoji?: Function; getCustomEmoji?: Function;
onLinkPress?: Function; onLinkPress?: TOnLinkPress;
navToRoomInfo: Function; navToRoomInfo?: Function;
useRealName: boolean; useRealName?: boolean;
username: string; username: string;
baseUrl: string; baseUrl: string;
} }
@ -37,7 +35,12 @@ const Body = ({
getCustomEmoji, getCustomEmoji,
baseUrl, baseUrl,
onLinkPress onLinkPress
}: IBodyProps): JSX.Element => ( }: IBodyProps): React.ReactElement | null => {
if (isEmpty(tokens)) {
return null;
}
return (
<MarkdownContext.Provider <MarkdownContext.Provider
value={{ value={{
mentions, mentions,
@ -49,7 +52,7 @@ const Body = ({
baseUrl, baseUrl,
onLinkPress onLinkPress
}}> }}>
{tokens.map(block => { {tokens?.map(block => {
switch (block.type) { switch (block.type) {
case 'BIG_EMOJI': case 'BIG_EMOJI':
return <BigEmoji value={block.value} />; return <BigEmoji value={block.value} />;
@ -73,5 +76,6 @@ const Body = ({
})} })}
</MarkdownContext.Provider> </MarkdownContext.Provider>
); );
};
export default Body; export default Body;

View File

@ -15,6 +15,7 @@ import { isAndroid, isIOS } from '../../utils/deviceInfo';
import MessageContext from './Context'; import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
import { withDimensions } from '../../dimensions'; import { withDimensions } from '../../dimensions';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IButton { interface IButton {
loading: boolean; loading: boolean;
@ -29,7 +30,7 @@ interface IMessageAudioProps {
description: string; description: string;
}; };
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
scale?: number; scale?: number;
} }
@ -280,7 +281,6 @@ class MessageAudio extends React.Component<IMessageAudioProps, IMessageAudioStat
/> />
<Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text> <Text style={[styles.duration, { color: themes[theme].auxiliaryText }]}>{this.duration}</Text>
</View> </View>
{/* @ts-ignore*/}
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} /> <Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
</> </>
); );

View File

@ -4,7 +4,7 @@ import { dequal } from 'dequal';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown from '../markdown'; import Markdown, { MarkdownPreview } from '../markdown';
import User from './User'; import User from './User';
import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils'; import { SYSTEM_MESSAGE_TYPES_WITH_AUTHOR_NAME, getInfoMessage } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -49,10 +49,11 @@ const Content = React.memo(
{I18n.t('Encrypted_message')} {I18n.t('Encrypted_message')}
</Text> </Text>
); );
} else if (isPreview) {
content = <MarkdownPreview msg={props.msg} />;
} else { } else {
const { baseUrl, user, onLinkPress } = useContext(MessageContext); const { baseUrl, user, onLinkPress } = useContext(MessageContext);
content = ( content = (
// @ts-ignore
<Markdown <Markdown
msg={props.msg} msg={props.msg}
md={props.md} md={props.md}
@ -61,8 +62,6 @@ const Content = React.memo(
enableMessageParser={user.enableMessageParserEarlyAdoption} enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username} username={user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={isPreview ? 1 : 0}
preview={isPreview}
channels={props.channels} channels={props.channels}
mentions={props.mentions} mentions={props.mentions}
navToRoomInfo={props.navToRoomInfo} navToRoomInfo={props.navToRoomInfo}

View File

@ -11,6 +11,7 @@ import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
type TMessageButton = { type TMessageButton = {
children: JSX.Element; children: JSX.Element;
@ -28,7 +29,7 @@ interface IMessageImage {
imageUrl?: string; imageUrl?: string;
showAttachment: Function; showAttachment: Function;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
@ -66,7 +67,6 @@ const ImageContainer = React.memo(
<Button theme={theme} onPress={onPress}> <Button theme={theme} onPress={onPress}>
<View> <View>
<MessageImage img={img} theme={theme} /> <MessageImage img={img} theme={theme} />
{/* @ts-ignore */}
<Markdown <Markdown
msg={file.description} msg={file.description}
baseUrl={baseUrl} baseUrl={baseUrl}

View File

@ -9,6 +9,7 @@ import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import MessageContext from './Context'; import MessageContext from './Context';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IMessageAddReaction { interface IMessageAddReaction {
theme: string; theme: string;
@ -19,13 +20,13 @@ interface IMessageReaction {
usernames: []; usernames: [];
emoji: object; emoji: object;
}; };
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
interface IMessageReactions { interface IMessageReactions {
reactions?: object[]; reactions?: object[];
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }

View File

@ -5,7 +5,7 @@ import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import I18n from '../../i18n'; import I18n from '../../i18n';
import Markdown from '../markdown'; import { MarkdownPreview } from '../markdown';
import { IMessageRepliedThread } from './interfaces'; import { IMessageRepliedThread } from './interfaces';
const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => { const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncrypted, theme }: IMessageRepliedThread) => {
@ -32,14 +32,7 @@ const RepliedThread = memo(({ tmid, tmsg, isHeader, fetchThreadName, id, isEncry
return ( return (
<View style={styles.repliedThread} testID={`message-thread-replied-on-${msg}`}> <View style={styles.repliedThread} testID={`message-thread-replied-on-${msg}`}>
<CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} /> <CustomIcon name='threads' size={20} style={styles.repliedThreadIcon} color={themes[theme].tintColor} />
{/* @ts-ignore*/} <MarkdownPreview msg={msg} style={[styles.repliedThreadName, { color: themes[theme].tintColor }]} />
<Markdown
msg={msg}
theme={theme}
style={[styles.repliedThreadName, { color: themes[theme].tintColor }]}
preview
numberOfLines={1}
/>
<View style={styles.repliedThreadDisclosure}> <View style={styles.repliedThreadDisclosure}>
<CustomIcon name='chevron-right' color={themes[theme].auxiliaryText} size={20} /> <CustomIcon name='chevron-right' color={themes[theme].auxiliaryText} size={20} />
</View> </View>

View File

@ -14,6 +14,7 @@ import MessageContext from './Context';
import { fileDownloadAndPreview } from '../../utils/fileDownload'; import { fileDownloadAndPreview } from '../../utils/fileDownload';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -99,14 +100,14 @@ interface IMessageTitle {
interface IMessageDescription { interface IMessageDescription {
attachment: IAttachment; attachment: IAttachment;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
interface IMessageFields { interface IMessageFields {
attachment: IAttachment; attachment: IAttachment;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
interface IMessageReply { interface IMessageReply {
@ -114,7 +115,7 @@ interface IMessageReply {
timeFormat: string; timeFormat: string;
index: number; index: number;
theme: string; theme: string;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => { const Title = React.memo(({ attachment, timeFormat, theme }: IMessageTitle) => {
@ -137,10 +138,7 @@ const Description = React.memo(
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext); const { baseUrl, user } = useContext(MessageContext);
return ( return <Markdown msg={text} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />;
// @ts-ignore
<Markdown msg={text} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} theme={theme} />
);
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {
if (prevProps.attachment.text !== nextProps.attachment.text) { if (prevProps.attachment.text !== nextProps.attachment.text) {
@ -180,7 +178,6 @@ const Fields = React.memo(
{attachment.fields.map(field => ( {attachment.fields.map(field => (
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}> <View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
<Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text> <Text style={[styles.fieldTitle, { color: themes[theme].bodyText }]}>{field.title}</Text>
{/* @ts-ignore*/}
<Markdown <Markdown
msg={field?.value || ''} msg={field?.value || ''}
baseUrl={baseUrl} baseUrl={baseUrl}
@ -266,9 +263,8 @@ const Reply = React.memo(
) : null} ) : null}
</View> </View>
</Touchable> </Touchable>
{/* @ts-ignore*/}
<Markdown <Markdown
msg={attachment.description!} msg={attachment.description}
baseUrl={baseUrl} baseUrl={baseUrl}
username={user.username} username={user.username}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}

View File

@ -15,6 +15,7 @@ import { LISTENER } from '../Toast';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { IAttachment } from '../../definitions/IAttachment'; import { IAttachment } from '../../definitions/IAttachment';
import RCActivityIndicator from '../ActivityIndicator'; import RCActivityIndicator from '../ActivityIndicator';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = (type: any) => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -33,7 +34,7 @@ const styles = StyleSheet.create({
interface IMessageVideo { interface IMessageVideo {
file: IAttachment; file: IAttachment;
showAttachment: Function; showAttachment: Function;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
@ -82,7 +83,6 @@ const Video = React.memo(
<CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} /> <CustomIcon name='play-filled' size={54} color={themes[theme].buttonText} />
)} )}
</Touchable> </Touchable>
{/* @ts-ignore*/}
<Markdown <Markdown
msg={file.description} msg={file.description}
baseUrl={baseUrl} baseUrl={baseUrl}

View File

@ -9,6 +9,7 @@ import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
interface IMessageContainerProps { interface IMessageContainerProps {
item: any; item: any;
@ -42,7 +43,7 @@ interface IMessageContainerProps {
status?: number; status?: number;
isIgnored?: boolean; isIgnored?: boolean;
highlighted?: boolean; highlighted?: boolean;
getCustomEmoji(name: string): void; getCustomEmoji: TGetCustomEmoji;
onLongPress?: Function; onLongPress?: Function;
onReactionPress?: Function; onReactionPress?: Function;
onEncryptedPress?: Function; onEncryptedPress?: Function;
@ -67,7 +68,7 @@ interface IMessageContainerProps {
class MessageContainer extends React.Component<IMessageContainerProps> { class MessageContainer extends React.Component<IMessageContainerProps> {
static defaultProps = { static defaultProps = {
getCustomEmoji: () => {}, getCustomEmoji: () => null,
onLongPress: () => {}, onLongPress: () => {},
onReactionPress: () => {}, onReactionPress: () => {},
onEncryptedPress: () => {}, onEncryptedPress: () => {},
@ -303,7 +304,7 @@ class MessageContainer extends React.Component<IMessageContainerProps> {
} }
}; };
onLinkPress = (link: any) => { onLinkPress = (link: string): void => {
const { item, theme, jumpToMessage } = this.props; const { item, theme, jumpToMessage } = this.props;
const isMessageLink = item?.attachments?.findIndex((att: any) => att?.message_link === link) !== -1; const isMessageLink = item?.attachments?.findIndex((att: any) => att?.message_link === link) !== -1;
if (isMessageLink) { if (isMessageLink) {

View File

@ -1,12 +1,15 @@
import { MarkdownAST } from '@rocket.chat/message-parser'; import { MarkdownAST } from '@rocket.chat/message-parser';
import { IUserChannel, IUserMention } from '../markdown/interfaces';
import { TGetCustomEmoji } from '../../definitions/IEmoji';
export type TMessageType = 'discussion-created' | 'jitsi_call_started'; export type TMessageType = 'discussion-created' | 'jitsi_call_started';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments: any; attachments: any;
timeFormat: string; timeFormat: string;
showAttachment: Function; showAttachment: Function;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
@ -28,7 +31,7 @@ export interface IMessageAvatar {
}; };
small?: boolean; small?: boolean;
navToRoomInfo: Function; navToRoomInfo: Function;
getCustomEmoji(): void; getCustomEmoji: TGetCustomEmoji;
theme: string; theme: string;
} }
@ -59,8 +62,6 @@ export interface IUser {
name: string; name: string;
} }
export type UserMention = Pick<IUser, 'id' | 'username' | 'name'>;
export interface IMessageContent { export interface IMessageContent {
_id: string; _id: string;
isTemp: boolean; isTemp: boolean;
@ -72,12 +73,9 @@ export interface IMessageContent {
theme: string; theme: string;
isEdited: boolean; isEdited: boolean;
isEncrypted: boolean; isEncrypted: boolean;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
channels: { channels: IUserChannel[];
name: string; mentions: IUserMention[];
_id: number;
}[];
mentions: UserMention[];
navToRoomInfo: Function; navToRoomInfo: Function;
useRealName: boolean; useRealName: boolean;
isIgnored: boolean; isIgnored: boolean;
@ -96,7 +94,7 @@ export interface IMessageEmoji {
baseUrl: string; baseUrl: string;
standardEmojiStyle: object; standardEmojiStyle: object;
customEmojiStyle: object; customEmojiStyle: object;
getCustomEmoji: Function; getCustomEmoji: TGetCustomEmoji;
} }
export interface IMessageThread { export interface IMessageThread {

View File

@ -76,7 +76,7 @@ type TInfoMessage = {
msg: string; msg: string;
author: { username: string }; author: { username: string };
}; };
export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage) => { export const getInfoMessage = ({ type, role, msg, author }: TInfoMessage): string => {
const { username } = author; const { username } = author;
if (type === 'rm') { if (type === 'rm') {
return I18n.t('Message_removed'); return I18n.t('Message_removed');

View File

@ -6,7 +6,7 @@ export interface IEmoji {
} }
export interface ICustomEmoji { export interface ICustomEmoji {
baseUrl: string; baseUrl?: string;
emoji: IEmoji; emoji: IEmoji;
style: any; style: any;
} }
@ -20,3 +20,5 @@ export interface IEmojiCategory {
style: any; style: any;
tabLabel: string; tabLabel: string;
} }
export type TGetCustomEmoji = (name: string) => IEmoji | null;

View File

@ -63,6 +63,7 @@ export interface IMessage {
_id: string; _id: string;
rid: string; rid: string;
msg?: string; msg?: string;
id?: string;
t?: MessageType; t?: MessageType;
ts: string | Date; ts: string | Date;
u: IUserMessage; u: IUserMessage;

View File

@ -3,7 +3,7 @@ import { dequal } from 'dequal';
import I18n from '../../i18n'; import I18n from '../../i18n';
import styles from './styles'; import styles from './styles';
import Markdown from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants'; import { E2E_MESSAGE_TYPE, E2E_STATUS } from '../../lib/encryption/constants';
@ -65,8 +65,7 @@ const arePropsEqual = (oldProps: any, newProps: any) => dequal(oldProps, newProp
const LastMessage = React.memo( const LastMessage = React.memo(
({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessage) => ( ({ lastMessage, type, showLastMessage, username, alert, useRealName, theme }: ILastMessage) => (
// @ts-ignore <MarkdownPreview
<Markdown
msg={formatMsg({ msg={formatMsg({
lastMessage, lastMessage,
type, type,
@ -75,11 +74,7 @@ const LastMessage = React.memo(
useRealName useRealName
})} })}
style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]} style={[styles.markdownText, { color: alert ? themes[theme].bodyText : themes[theme].auxiliaryText }]}
customEmojis={false}
useRealName={useRealName}
numberOfLines={2} numberOfLines={2}
preview
theme={theme}
testID='room-item-last-message' testID='room-item-last-message'
/> />
), ),

View File

@ -7,7 +7,7 @@ import { useTheme } from '../../theme';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Markdown from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { formatDateThreads, makeThreadName } from '../../utils/room'; import { formatDateThreads, makeThreadName } from '../../utils/room';
import DiscussionDetails from './DiscussionDetails'; import DiscussionDetails from './DiscussionDetails';
import { TThreadModel } from '../../definitions/IThread'; import { TThreadModel } from '../../definitions/IThread';
@ -49,14 +49,13 @@ const styles = StyleSheet.create({
interface IItem { interface IItem {
item: TThreadModel; item: TThreadModel;
baseUrl: string;
onPress: { onPress: {
(...args: any[]): void; (...args: any[]): void;
stop(): void; stop(): void;
}; };
} }
const Item = ({ item, baseUrl, onPress }: IItem): JSX.Element => { const Item = ({ item, onPress }: IItem): JSX.Element => {
const { theme } = useTheme(); const { theme } = useTheme();
const username = item?.u?.username; const username = item?.u?.username;
let messageTime = ''; let messageTime = '';
@ -82,18 +81,7 @@ const Item = ({ item, baseUrl, onPress }: IItem): JSX.Element => {
{messageTime ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{messageTime}</Text> : null} {messageTime ? <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{messageTime}</Text> : null}
</View> </View>
<View style={styles.messageContainer}> <View style={styles.messageContainer}>
{username ? ( {username ? <MarkdownPreview msg={makeThreadName(item)} numberOfLines={2} style={[styles.markdown]} /> : null}
/* @ts-ignore */
<Markdown
msg={makeThreadName(item)}
baseUrl={baseUrl}
username={username}
theme={theme}
numberOfLines={2}
style={[styles.markdown]}
preview
/>
) : null}
</View> </View>
{messageDate ? <DiscussionDetails item={item} date={messageDate} /> : null} {messageDate ? <DiscussionDetails item={item} date={messageDate} /> : null}
</View> </View>

View File

@ -46,16 +46,11 @@ class E2EHowItWorksView extends React.Component<IE2EHowItWorksViewProps, any> {
const infoStyle = [styles.info, { color: themes[theme].bodyText }]; const infoStyle = [styles.info, { color: themes[theme].bodyText }];
// TODO: Refactor when migrate Markdown
return ( return (
<SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-how-it-works-view'> <SafeAreaView style={[styles.container, { backgroundColor: themes[theme].backgroundColor }]} testID='e2e-how-it-works-view'>
{/* @ts-ignore */}
<Markdown msg={I18n.t('E2E_How_It_Works_info1')} style={infoStyle} theme={theme} /> <Markdown msg={I18n.t('E2E_How_It_Works_info1')} style={infoStyle} theme={theme} />
{/* @ts-ignore */}
<Markdown msg={I18n.t('E2E_How_It_Works_info2')} style={infoStyle} theme={theme} /> <Markdown msg={I18n.t('E2E_How_It_Works_info2')} style={infoStyle} theme={theme} />
{/* @ts-ignore */}
<Markdown msg={I18n.t('E2E_How_It_Works_info3')} style={infoStyle} theme={theme} /> <Markdown msg={I18n.t('E2E_How_It_Works_info3')} style={infoStyle} theme={theme} />
{/* @ts-ignore */}
<Markdown msg={I18n.t('E2E_How_It_Works_info4')} style={infoStyle} theme={theme} /> <Markdown msg={I18n.t('E2E_How_It_Works_info4')} style={infoStyle} theme={theme} />
</SafeAreaView> </SafeAreaView>
); );

View File

@ -93,8 +93,7 @@ class InviteUsersView extends React.Component<IInviteUsersViewProps, any> {
renderExpiration = () => { renderExpiration = () => {
const { theme } = this.props; const { theme } = this.props;
const expirationMessage = this.linkExpirationText(); const expirationMessage = this.linkExpirationText();
// @ts-ignore return <Markdown msg={expirationMessage} theme={theme} />;
return <Markdown msg={expirationMessage} username='' baseUrl='' theme={theme} />;
}; };
render() { render() {

View File

@ -21,6 +21,7 @@ import getThreadName from '../../lib/methods/getThreadName';
import styles from './styles'; import styles from './styles';
import { ChatsStackParamList } from '../../stacks/types'; import { ChatsStackParamList } from '../../stacks/types';
import { ISubscription, SubscriptionType } from '../../definitions/ISubscription'; import { ISubscription, SubscriptionType } from '../../definitions/ISubscription';
import { IEmoji } from '../../definitions/IEmoji';
interface IMessagesViewProps { interface IMessagesViewProps {
user: { user: {
@ -34,7 +35,7 @@ interface IMessagesViewProps {
StackNavigationProp<MasterDetailInsideStackParamList> StackNavigationProp<MasterDetailInsideStackParamList>
>; >;
route: RouteProp<ChatsStackParamList, 'MessagesView'>; route: RouteProp<ChatsStackParamList, 'MessagesView'>;
customEmojis: { [key: string]: string }; customEmojis: { [key: string]: IEmoji };
theme: string; theme: string;
showActionSheet: Function; showActionSheet: Function;
useRealName: boolean; useRealName: boolean;

View File

@ -21,7 +21,7 @@ import StatusBar from '../../containers/StatusBar';
import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors'; import { SWITCH_TRACK_COLOR, themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import Markdown from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { showConfirmationAlert, showErrorAlert } from '../../utils/info'; import { showConfirmationAlert, showErrorAlert } from '../../utils/info';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import { E2E_ROOM_TYPES } from '../../lib/encryption/constants'; import { E2E_ROOM_TYPES } from '../../lib/encryption/constants';
@ -723,20 +723,14 @@ class RoomActionsView extends React.Component {
</Text> </Text>
</View> </View>
)} )}
<Markdown <MarkdownPreview
preview
msg={t === 'd' ? `@${name}` : topic} msg={t === 'd' ? `@${name}` : topic}
style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]}
numberOfLines={1}
theme={theme}
/> />
{room.t === 'd' && ( {room.t === 'd' && (
<Markdown <MarkdownPreview
msg={member.statusText} msg={member.statusText}
style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]} style={[styles.roomDescription, { color: themes[theme].auxiliaryText }]}
preview
theme={theme}
numberOfLines={1}
/> />
)} )}
</View> </View>

View File

@ -18,7 +18,7 @@ import StatusBar from '../../containers/StatusBar';
import log, { events, logEvent } from '../../utils/log'; import log, { events, logEvent } from '../../utils/log';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import Markdown from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { LISTENER } from '../../containers/Toast'; import { LISTENER } from '../../containers/Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
@ -42,12 +42,7 @@ const getRoomTitle = (room, type, name, username, statusText, theme) =>
)} )}
{!!statusText && ( {!!statusText && (
<View testID='room-info-view-custom-status'> <View testID='room-info-view-custom-status'>
<Markdown <MarkdownPreview msg={statusText} style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]} />
msg={statusText}
style={[styles.roomUsername, { color: themes[theme].auxiliaryText }]}
preview
theme={theme}
/>
</View> </View>
)} )}
</> </>

View File

@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
import { BorderlessButton, ScrollView } from 'react-native-gesture-handler'; import { BorderlessButton, ScrollView } from 'react-native-gesture-handler';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import Markdown from '../../containers/markdown'; import Markdown, { MarkdownPreview } from '../../containers/markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
@ -22,7 +22,7 @@ const Banner = React.memo(
style={[styles.bannerContainer, { backgroundColor: themes[theme].bannerBackground }]} style={[styles.bannerContainer, { backgroundColor: themes[theme].bannerBackground }]}
testID='room-view-banner' testID='room-view-banner'
onPress={toggleModal}> onPress={toggleModal}>
<Markdown msg={text} theme={theme} numberOfLines={1} style={[styles.bannerText]} preview /> <MarkdownPreview msg={text} style={[styles.bannerText]} />
<BorderlessButton onPress={closeBanner}> <BorderlessButton onPress={closeBanner}>
<CustomIcon color={themes[theme].auxiliaryText} name='close' size={20} /> <CustomIcon color={themes[theme].auxiliaryText} name='close' size={20} />
</BorderlessButton> </BorderlessButton>

File diff suppressed because one or more lines are too long

View File

@ -32,6 +32,7 @@ import { isIOS } from '../../utils/deviceInfo';
import { compareServerVersion } from '../../lib/utils'; import { compareServerVersion } from '../../lib/utils';
import styles from './styles'; import styles from './styles';
import { InsideStackParamList, ChatsStackParamList } from '../../stacks/types'; import { InsideStackParamList, ChatsStackParamList } from '../../stacks/types';
import { IEmoji } from '../../definitions/IEmoji';
const QUERY_SIZE = 50; const QUERY_SIZE = 50;
@ -66,10 +67,7 @@ interface ISearchMessagesViewProps extends INavigationOption {
baseUrl: string; baseUrl: string;
serverVersion: string; serverVersion: string;
customEmojis: { customEmojis: {
[key: string]: { [key: string]: IEmoji;
name: string;
extension: string;
};
}; };
theme: string; theme: string;
useRealName: boolean; useRealName: boolean;
@ -312,8 +310,7 @@ class SearchMessagesView extends React.Component<ISearchMessagesViewProps, ISear
testID='search-message-view-input' testID='search-message-view-input'
theme={theme} theme={theme}
/> />
{/* @ts-ignore */} <Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} theme={theme} />
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' theme={theme} />
<View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} /> <View style={[styles.divider, { backgroundColor: themes[theme].separatorColor }]} />
</View> </View>
{this.renderList()} {this.renderList()}

View File

@ -6,7 +6,7 @@ import { useTheme } from '../../theme';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import sharedStyles from '../Styles'; import sharedStyles from '../Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import Markdown from '../../containers/markdown'; import { MarkdownPreview } from '../../containers/markdown';
import { formatDateThreads, makeThreadName } from '../../utils/room'; import { formatDateThreads, makeThreadName } from '../../utils/room';
import ThreadDetails from '../../containers/ThreadDetails'; import ThreadDetails from '../../containers/ThreadDetails';
import { TThreadModel } from '../../definitions/IThread'; import { TThreadModel } from '../../definitions/IThread';
@ -58,7 +58,6 @@ const styles = StyleSheet.create({
interface IItem { interface IItem {
item: TThreadModel; item: TThreadModel;
baseUrl: string;
useRealName: boolean; useRealName: boolean;
user: any; user: any;
badgeColor?: string; badgeColor?: string;
@ -66,7 +65,7 @@ interface IItem {
toggleFollowThread: (isFollowing: boolean, id: string) => void; toggleFollowThread: (isFollowing: boolean, id: string) => void;
} }
const Item = ({ item, baseUrl, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => { const Item = ({ item, useRealName, user, badgeColor, onPress, toggleFollowThread }: IItem) => {
const { theme } = useTheme(); const { theme } = useTheme();
const username = (useRealName && item?.u?.name) || item?.u?.username; const username = (useRealName && item?.u?.name) || item?.u?.username;
let time; let time;
@ -89,18 +88,7 @@ const Item = ({ item, baseUrl, useRealName, user, badgeColor, onPress, toggleFol
<Text style={[styles.time, { color: themes[theme!].auxiliaryText }]}>{time}</Text> <Text style={[styles.time, { color: themes[theme!].auxiliaryText }]}>{time}</Text>
</View> </View>
<View style={styles.messageContainer}> <View style={styles.messageContainer}>
{makeThreadName(item) && username ? ( <MarkdownPreview msg={makeThreadName(item)} numberOfLines={2} style={[styles.markdown]} />
/* @ts-ignore */
<Markdown
msg={makeThreadName(item)}
baseUrl={baseUrl}
username={username}
theme={theme}
numberOfLines={2}
style={[styles.markdown]}
preview
/>
) : null}
{badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null} {badgeColor ? <View style={[styles.badge, { backgroundColor: badgeColor }]} /> : null}
</View> </View>
<ThreadDetails item={item} user={user} toggleFollowThread={toggleFollowThread} style={styles.threadDetails} /> <ThreadDetails item={item} user={user} toggleFollowThread={toggleFollowThread} style={styles.threadDetails} />

View File

@ -462,7 +462,7 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
}; };
renderItem = ({ item }: { item: TThreadModel }) => { renderItem = ({ item }: { item: TThreadModel }) => {
const { user, navigation, baseUrl, useRealName } = this.props; const { user, navigation, useRealName } = this.props;
const badgeColor = this.getBadgeColor(item); const badgeColor = this.getBadgeColor(item);
return ( return (
<Item <Item
@ -470,7 +470,6 @@ class ThreadMessagesView extends React.Component<IThreadMessagesViewProps, IThre
item, item,
user, user,
navigation, navigation,
baseUrl,
useRealName, useRealName,
badgeColor badgeColor
}} }}

View File

@ -1,10 +1,10 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, StyleSheet, View } from 'react-native';
import { storiesOf } from '@storybook/react-native'; import { storiesOf } from '@storybook/react-native';
import Markdown from '../../app/containers/markdown'; import Markdown, { MarkdownPreview } from '../../app/containers/markdown';
import { themes } from '../../app/constants/colors'; import { themes } from '../../app/constants/colors';
import { TGetCustomEmoji, IEmoji } from '../../app/definitions/IEmoji';
const theme = 'light'; const theme = 'light';
@ -33,12 +33,12 @@ d
e`; e`;
const sequentialEmptySpacesText = 'a b c'; const sequentialEmptySpacesText = 'a b c';
const getCustomEmoji = content => { const getCustomEmoji: TGetCustomEmoji = content => {
const customEmoji = { const customEmoji = {
marioparty: { name: content, extension: 'gif' }, marioparty: { name: content, extension: 'gif' },
react_rocket: { name: content, extension: 'png' }, react_rocket: { name: content, extension: 'png' },
nyan_rocket: { name: content, extension: 'png' } nyan_rocket: { name: content, extension: 'png' }
}[content]; }[content] as IEmoji;
return customEmoji; return customEmoji;
}; };
@ -62,42 +62,12 @@ stories.add('Edited', () => (
stories.add('Preview', () => ( stories.add('Preview', () => (
<View style={styles.container}> <View style={styles.container}>
<Markdown msg={longText} theme={theme} numberOfLines={1} preview /> <MarkdownPreview msg={longText} />
<Markdown msg={lineBreakText} theme={theme} numberOfLines={1} preview /> <MarkdownPreview msg={lineBreakText} />
<Markdown msg={sequentialEmptySpacesText} theme={theme} numberOfLines={1} preview /> <MarkdownPreview msg={sequentialEmptySpacesText} />
<Markdown <MarkdownPreview msg='@rocket.cat @name1 @all @here @unknown #general #unknown' />
msg='@rocket.cat @name1 @all @here @unknown #general #unknown' <MarkdownPreview msg='Testing: 😃 :+1: :marioparty:' />
theme={theme} <MarkdownPreview msg='Fallback from new md to old' />
numberOfLines={1}
preview
mentions={[
{ _id: 'random', name: 'Rocket Cat', username: 'rocket.cat' },
{ _id: 'random2', name: 'Name', username: 'name1' },
{ _id: 'here', username: 'here' },
{ _id: 'all', username: 'all' }
]}
channels={[{ _id: '123', name: 'test-channel' }]}
username='rocket.cat'
/>
<Markdown msg='Testing: 😃 :+1: :marioparty:' getCustomEmoji={getCustomEmoji} theme={theme} numberOfLines={1} preview />
<Markdown
msg='Fallback from new md to old'
getCustomEmoji={getCustomEmoji}
theme={theme}
numberOfLines={1}
preview
md={[
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'This is Rocket.Chat'
}
]
}
]}
/>
</View> </View>
)); ));

File diff suppressed because one or more lines are too long