Merge branch 'develop' into new.add-discusions-roomactionsview

This commit is contained in:
Gerzon Z 2021-10-20 18:10:30 -04:00
commit 5dc0a0a05f
70 changed files with 5068 additions and 1462 deletions

File diff suppressed because it is too large Load Diff

View File

@ -14,7 +14,6 @@ public class BasePackageList {
new expo.modules.imageloader.ImageLoaderPackage(), new expo.modules.imageloader.ImageLoaderPackage(),
new expo.modules.keepawake.KeepAwakePackage(), new expo.modules.keepawake.KeepAwakePackage(),
new expo.modules.localauthentication.LocalAuthenticationPackage(), new expo.modules.localauthentication.LocalAuthenticationPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.videothumbnails.VideoThumbnailsPackage(), new expo.modules.videothumbnails.VideoThumbnailsPackage(),
new expo.modules.webbrowser.WebBrowserPackage() new expo.modules.webbrowser.WebBrowserPackage()
); );

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet, View } from 'react-native'; import { ScrollView, ScrollViewProps, StyleSheet, View } from 'react-native';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -10,10 +10,10 @@ import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo'; import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView'; import SafeAreaView from './SafeAreaView';
interface IFormContainer { interface IFormContainer extends ScrollViewProps {
theme: string; theme: string;
testID: string; testID: string;
children: JSX.Element; children: React.ReactNode;
} }
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleProp, StyleSheet, Text, TextInputProps, TextStyle, View, ViewStyle } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
@ -50,23 +50,21 @@ const styles = StyleSheet.create({
} }
}); });
interface IRCTextInputProps { interface IRCTextInputProps extends TextInputProps {
label: string; label?: string;
error: { error?: {
error: any; error: any;
reason: any; reason: any;
}; };
loading: boolean; loading?: boolean;
secureTextEntry: boolean; containerStyle?: StyleProp<ViewStyle>;
containerStyle: any; inputStyle?: TextStyle;
inputStyle: object; inputRef?: React.Ref<unknown>;
inputRef: any; testID?: string;
testID: string; iconLeft?: string;
iconLeft: string; iconRight?: string;
iconRight: string; left?: JSX.Element;
placeholder: string; onIconRightPress?(): void;
left: JSX.Element;
onIconRightPress(): void;
theme: string; theme: string;
} }
@ -148,17 +146,10 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
return ( return (
<View style={[styles.inputContainer, containerStyle]}> <View style={[styles.inputContainer, containerStyle]}>
{label ? ( {label ? (
<Text <Text style={[styles.label, { color: themes[theme].titleText }, error?.error && { color: dangerColor }]}>{label}</Text>
contentDescription={null}
// @ts-ignore
accessibilityLabel={null}
style={[styles.label, { color: themes[theme].titleText }, error.error && { color: dangerColor }]}>
{label}
</Text>
) : null} ) : null}
<View style={styles.wrap}> <View style={styles.wrap}>
<TextInput <TextInput
/* @ts-ignore*/
style={[ style={[
styles.input, styles.input,
iconLeft && styles.inputIconLeft, iconLeft && styles.inputIconLeft,
@ -168,14 +159,13 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
borderColor: themes[theme].separatorColor, borderColor: themes[theme].separatorColor,
color: themes[theme].titleText color: themes[theme].titleText
}, },
error.error && { error?.error && {
color: dangerColor, color: dangerColor,
borderColor: dangerColor borderColor: dangerColor
}, },
inputStyle inputStyle
]} ]}
ref={inputRef} ref={inputRef}
/* @ts-ignore*/
autoCorrect={false} autoCorrect={false}
autoCapitalize='none' autoCapitalize='none'
underlineColorAndroid='transparent' underlineColorAndroid='transparent'
@ -183,8 +173,6 @@ export default class RCTextInput extends React.PureComponent<IRCTextInputProps,
testID={testID} testID={testID}
accessibilityLabel={placeholder} accessibilityLabel={placeholder}
placeholder={placeholder} placeholder={placeholder}
/* @ts-ignore*/
contentDescription={placeholder}
theme={theme} theme={theme}
{...inputProps} {...inputProps}
/> />

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import { useTheme } from '../../theme';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import styles from './styles'; import styles from './styles';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
@ -9,20 +10,20 @@ interface IAtMention {
mention: string; mention: string;
username: string; username: string;
navToRoomInfo: Function; navToRoomInfo: Function;
style: any; style?: any;
useRealName: boolean; useRealName: boolean;
theme: string;
mentions: any; mentions: any;
} }
const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, style = [], useRealName, theme }: IAtMention) => { const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, style = [], useRealName }: IAtMention) => {
const { theme } = useTheme();
if (mention === 'all' || mention === 'here') { if (mention === 'all' || mention === 'here') {
return ( return (
<Text <Text
style={[ style={[
styles.mention, styles.mention,
{ {
color: themes[theme].mentionGroupColor color: themes[theme!].mentionGroupColor
}, },
...style ...style
]}> ]}>
@ -34,11 +35,11 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
let mentionStyle = {}; let mentionStyle = {};
if (mention === username) { if (mention === username) {
mentionStyle = { mentionStyle = {
color: themes[theme].mentionMeColor color: themes[theme!].mentionMeColor
}; };
} else { } else {
mentionStyle = { mentionStyle = {
color: themes[theme].mentionOtherColor color: themes[theme!].mentionOtherColor
}; };
} }
@ -61,7 +62,7 @@ const AtMention = React.memo(({ mention, mentions, username, navToRoomInfo, styl
); );
} }
return <Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>{`@${mention}`}</Text>; return <Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]}>{`@${mention}`}</Text>;
}); });
export default AtMention; export default AtMention;

View File

@ -1,23 +1,26 @@
import React from 'react'; import React from 'react';
import { Text } from 'react-native'; import { Text, TextStyle } from 'react-native';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { useTheme } from '../../theme';
import styles from './styles'; import styles from './styles';
interface IHashtag { interface IHashtag {
hashtag: string; hashtag: string;
navToRoomInfo: Function; navToRoomInfo: Function;
style: []; style?: TextStyle[];
theme: string;
channels: { channels: {
[index: number]: string | number;
name: string; name: string;
_id: number; _id: number;
}[]; }[];
} }
const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [], theme }: IHashtag) => { const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [] }: IHashtag) => {
const { theme } = useTheme();
const handlePress = () => { const handlePress = () => {
const index = channels.findIndex(channel => channel.name === hashtag); const index = channels?.findIndex(channel => channel.name === hashtag);
const navParam = { const navParam = {
t: 'c', t: 'c',
rid: channels[index]._id rid: channels[index]._id
@ -31,7 +34,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [], them
style={[ style={[
styles.mention, styles.mention,
{ {
color: themes[theme].mentionOtherColor color: themes[theme!].mentionOtherColor
}, },
...style ...style
]} ]}
@ -40,7 +43,7 @@ const Hashtag = React.memo(({ hashtag, channels, navToRoomInfo, style = [], them
</Text> </Text>
); );
} }
return <Text style={[styles.text, { color: themes[theme].bodyText }, ...style]}>{`#${hashtag}`}</Text>; return <Text style={[styles.text, { color: themes[theme!].bodyText }, ...style]}>{`#${hashtag}`}</Text>;
}); });
export default Hashtag; export default Hashtag;

View File

@ -3,6 +3,7 @@ import { Image, Text } 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 removeMarkdown from 'remove-markdown';
import { MarkdownAST } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n'; import I18n from '../../i18n';
@ -20,9 +21,20 @@ import MarkdownTableCell from './TableCell';
import mergeTextNodes from './mergeTextNodes'; 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';
interface IUser {
_id: string;
username: string;
name: string;
}
type UserMention = Pick<IUser, '_id' | 'username' | 'name'>;
interface IMarkdownProps { interface IMarkdownProps {
msg: string; msg: string;
md: MarkdownAST;
mentions: UserMention[];
getCustomEmoji: Function; getCustomEmoji: Function;
baseUrl: string; baseUrl: string;
username: string; username: string;
@ -35,7 +47,7 @@ interface IMarkdownProps {
name: string; name: string;
_id: number; _id: number;
}[]; }[];
mentions: object[]; enableMessageParser: boolean;
navToRoomInfo: Function; navToRoomInfo: Function;
preview: boolean; preview: boolean;
theme: string; theme: string;
@ -97,8 +109,10 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
constructor(props: IMarkdownProps) { constructor(props: IMarkdownProps) {
super(props); super(props);
if (!this.isNewMarkdown) {
this.renderer = this.createRenderer(); this.renderer = this.createRenderer();
} }
}
createRenderer = () => createRenderer = () =>
new Renderer({ new Renderer({
@ -139,6 +153,11 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
renderParagraphsInLists: true renderParagraphsInLists: true
}); });
get isNewMarkdown(): boolean {
const { md, enableMessageParser } = this.props;
return enableMessageParser && !!md;
}
editedMessage = (ast: any) => { editedMessage = (ast: any) => {
const { isEdited } = this.props; const { isEdited } = this.props;
if (isEdited) { if (isEdited) {
@ -227,12 +246,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
renderHashtag = ({ hashtag }: { hashtag: string }) => { renderHashtag = ({ hashtag }: { hashtag: string }) => {
const { channels, navToRoomInfo, style, theme } = this.props; const { channels, navToRoomInfo, style } = this.props;
return <MarkdownHashtag hashtag={hashtag} channels={channels} navToRoomInfo={navToRoomInfo} theme={theme} style={style} />; return <MarkdownHashtag hashtag={hashtag} channels={channels} navToRoomInfo={navToRoomInfo} style={style} />;
}; };
renderAtMention = ({ mentionName }: { mentionName: string }) => { renderAtMention = ({ mentionName }: { mentionName: string }) => {
const { username, mentions, navToRoomInfo, useRealName, style, theme } = this.props; const { username, mentions, navToRoomInfo, useRealName, style } = this.props;
return ( return (
<MarkdownAtMention <MarkdownAtMention
mentions={mentions} mentions={mentions}
@ -240,7 +259,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
useRealName={useRealName} useRealName={useRealName}
username={username} username={username}
navToRoomInfo={navToRoomInfo} navToRoomInfo={navToRoomInfo}
theme={theme}
style={style} style={style}
/> />
); );
@ -329,12 +347,44 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
}; };
render() { render() {
const { msg, numberOfLines, preview = false, theme, style = [], testID } = this.props; const {
msg,
md,
numberOfLines,
preview = false,
theme,
style = [],
testID,
mentions,
channels,
navToRoomInfo,
useRealName,
username,
getCustomEmoji,
baseUrl,
onLinkPress
} = this.props;
if (!msg) { if (!msg) {
return null; return null;
} }
if (this.isNewMarkdown) {
return (
<NewMarkdown
username={username}
baseUrl={baseUrl}
getCustomEmoji={getCustomEmoji}
useRealName={useRealName}
tokens={md}
mentions={mentions}
channels={channels}
navToRoomInfo={navToRoomInfo}
onLinkPress={onLinkPress}
/>
);
}
let m = formatText(msg); let m = formatText(msg);
// Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test' // Ex: '[ ](https://open.rocket.chat/group/test?msg=abcdef) Test'

View File

@ -0,0 +1,25 @@
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { BigEmoji as BigEmojiProps } from '@rocket.chat/message-parser';
import Emoji from './Emoji';
interface IBigEmojiProps {
value: BigEmojiProps['value'];
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row'
}
});
const BigEmoji = ({ value }: IBigEmojiProps): JSX.Element => (
<View style={styles.container}>
{value.map(block => (
<Emoji value={block.value} isBigEmoji />
))}
</View>
);
export default BigEmoji;

View File

@ -0,0 +1,40 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { Bold as BoldProps } from '@rocket.chat/message-parser';
import sharedStyles from '../../../views/Styles';
import Strike from './Strike';
import Italic from './Italic';
import Plain from './Plain';
import Link from './Link';
interface IBoldProps {
value: BoldProps['value'];
}
const styles = StyleSheet.create({
text: {
...sharedStyles.textBold
}
});
const Bold = ({ value }: IBoldProps): JSX.Element => (
<Text style={styles.text}>
{value.map(block => {
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
default:
return null;
}
})}
</Text>
);
export default Bold;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { Text } from 'react-native';
import { Code as CodeProps } from '@rocket.chat/message-parser';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
import CodeLine from './CodeLine';
interface ICodeProps {
value: CodeProps['value'];
}
const Code = ({ value }: ICodeProps): JSX.Element => {
const { theme } = useTheme();
return (
<Text
style={[
styles.codeBlock,
{
color: themes[theme!].bodyText,
backgroundColor: themes[theme!].bannerBackground,
borderColor: themes[theme!].borderColor
}
]}>
{value.map(block => {
switch (block.type) {
case 'CODE_LINE':
return <CodeLine value={block.value} />;
default:
return null;
}
})}
</Text>
);
};
export default Code;

View File

@ -0,0 +1,17 @@
import React from 'react';
import { Text } from 'react-native';
import { CodeLine as CodeLineProps } from '@rocket.chat/message-parser';
interface ICodeLineProps {
value: CodeLineProps['value'];
}
const CodeLine = ({ value }: ICodeLineProps): JSX.Element | null => {
if (value.type !== 'PLAIN_TEXT') {
return null;
}
return <Text>{value.value}</Text>;
};
export default CodeLine;

View File

@ -0,0 +1,29 @@
import React, { useContext } from 'react';
import { Text } from 'react-native';
import { Emoji as EmojiProps } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../../utils/shortnameToUnicode';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
import styles from '../styles';
import CustomEmoji from '../../EmojiPicker/CustomEmoji';
import MarkdownContext from './MarkdownContext';
interface IEmojiProps {
value: EmojiProps['value'];
isBigEmoji?: boolean;
}
const Emoji = ({ value, isBigEmoji }: IEmojiProps): JSX.Element => {
const { theme } = useTheme();
const { baseUrl, getCustomEmoji } = useContext(MarkdownContext);
const emojiUnicode = shortnameToUnicode(`:${value.value}:`);
const emoji = getCustomEmoji?.(value.value);
if (emoji) {
return <CustomEmoji baseUrl={baseUrl} style={[isBigEmoji ? styles.customEmojiBig : styles.customEmoji]} emoji={emoji} />;
}
return <Text style={[{ color: themes[theme!].bodyText }, isBigEmoji ? styles.textBig : styles.text]}>{emojiUnicode}</Text>;
};
export default Emoji;

View File

@ -0,0 +1,32 @@
import React from 'react';
import { Text } from 'react-native';
import { Heading as HeadingProps } from '@rocket.chat/message-parser';
import { themes } from '../../../constants/colors';
import styles from '../styles';
import { useTheme } from '../../../theme';
interface IHeadingProps {
value: HeadingProps['value'];
level: HeadingProps['level'];
}
const Heading = ({ value, level }: IHeadingProps): JSX.Element => {
const { theme } = useTheme();
const textStyle = styles[`heading${level}`];
return (
<Text style={[textStyle, { color: themes[theme!].bodyText }]}>
{value.map(block => {
switch (block.type) {
case 'PLAIN_TEXT':
return block.value;
default:
return null;
}
})}
</Text>
);
};
export default Heading;

View File

@ -0,0 +1,41 @@
import React from 'react';
import { Image as ImageProps } from '@rocket.chat/message-parser';
import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress';
import FastImage from '@rocket.chat/react-native-fast-image';
import { useTheme } from '../../../theme';
import { themes } from '../../../constants/colors';
import styles from '../../message/styles';
interface IImageProps {
value: ImageProps['value'];
}
type TMessageImage = {
img: string;
theme: string;
};
const ImageProgress = createImageProgress(FastImage);
const MessageImage = ({ img, theme }: TMessageImage) => (
<ImageProgress
style={[styles.inlineImage, { borderColor: themes[theme].borderColor }]}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
indicator={Progress.Pie}
indicatorProps={{
color: themes[theme].actionTintColor
}}
/>
);
const Image = ({ value }: IImageProps): JSX.Element => {
const { theme } = useTheme();
const { src } = value;
return <MessageImage img={src.value} theme={theme!} />;
};
export default Image;

View File

@ -0,0 +1,62 @@
import React, { useContext } from 'react';
import { Paragraph as ParagraphProps } from '@rocket.chat/message-parser';
import Hashtag from '../Hashtag';
import AtMention from '../AtMention';
import Link from './Link';
import Plain from './Plain';
import Bold from './Bold';
import Strike from './Strike';
import Italic from './Italic';
import Emoji from './Emoji';
import InlineCode from './InlineCode';
import Image from './Image';
import MarkdownContext from './MarkdownContext';
interface IParagraphProps {
value: ParagraphProps['value'];
}
const Inline = ({ value }: IParagraphProps): JSX.Element => {
const { useRealName, username, navToRoomInfo, mentions, channels } = useContext(MarkdownContext);
return (
<>
{value.map(block => {
switch (block.type) {
case 'IMAGE':
return <Image value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
case 'LINK':
return <Link value={block.value} />;
case 'MENTION_USER':
return (
<AtMention
mention={block.value.value}
useRealName={useRealName}
username={username}
navToRoomInfo={navToRoomInfo}
mentions={mentions}
/>
);
case 'EMOJI':
return <Emoji value={block.value} />;
case 'MENTION_CHANNEL':
return <Hashtag hashtag={block.value.value} navToRoomInfo={navToRoomInfo} channels={channels} />;
case 'INLINE_CODE':
return <InlineCode value={block.value} />;
default:
return null;
}
})}
</>
);
};
export default Inline;

View File

@ -0,0 +1,38 @@
import React from 'react';
import { Text } from 'react-native';
import { InlineCode as InlineCodeProps } from '@rocket.chat/message-parser';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IInlineCodeProps {
value: InlineCodeProps['value'];
}
const InlineCode = ({ value }: IInlineCodeProps): JSX.Element => {
const { theme } = useTheme();
return (
<Text
style={[
styles.codeInline,
{
color: themes[theme!].bodyText,
backgroundColor: themes[theme!].bannerBackground,
borderColor: themes[theme!].borderColor
}
]}>
{(block => {
switch (block.type) {
case 'PLAIN_TEXT':
return <Text>{block.value}</Text>;
default:
return null;
}
})(value)}
</Text>
);
};
export default InlineCode;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { Italic as ItalicProps } from '@rocket.chat/message-parser';
import Strike from './Strike';
import Bold from './Bold';
import Plain from './Plain';
import Link from './Link';
interface IItalicProps {
value: ItalicProps['value'];
}
const styles = StyleSheet.create({
text: {
fontStyle: 'italic'
}
});
const Italic = ({ value }: IItalicProps): JSX.Element => (
<Text style={styles.text}>
{value.map(block => {
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
case 'STRIKE':
return <Strike value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
default:
return null;
}
})}
</Text>
);
export default Italic;

View File

@ -0,0 +1,60 @@
import React, { useContext } from 'react';
import { Text, Clipboard } from 'react-native';
import { Link as LinkProps } from '@rocket.chat/message-parser';
import styles from '../styles';
import I18n from '../../../i18n';
import { LISTENER } from '../../Toast';
import { useTheme } from '../../../theme';
import openLink from '../../../utils/openLink';
import EventEmitter from '../../../utils/events';
import { themes } from '../../../constants/colors';
import Strike from './Strike';
import Italic from './Italic';
import Bold from './Bold';
import MarkdownContext from './MarkdownContext';
interface ILinkProps {
value: LinkProps['value'];
}
const Link = ({ value }: ILinkProps): JSX.Element => {
const { theme } = useTheme();
const { onLinkPress } = useContext(MarkdownContext);
const { src, label } = value;
const handlePress = () => {
if (!src.value) {
return;
}
if (onLinkPress) {
return onLinkPress(src.value);
}
openLink(src.value, theme);
};
const onLongPress = () => {
Clipboard.setString(src.value);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
};
return (
<Text onPress={handlePress} onLongPress={onLongPress} style={[styles.link, { color: themes[theme!].actionTintColor }]}>
{(block => {
switch (block.type) {
case 'PLAIN_TEXT':
return block.value;
case 'STRIKE':
return <Strike value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
default:
return null;
}
})(label)}
</Text>
);
};
export default Link;

View File

@ -0,0 +1,29 @@
import React from 'react';
import { UserMention } from '../../message/interfaces';
interface IMarkdownContext {
mentions: UserMention[];
channels: {
name: string;
_id: number;
}[];
useRealName: boolean;
username: string;
baseUrl: string;
navToRoomInfo: Function;
getCustomEmoji?: Function;
onLinkPress?: Function;
}
const defaultState = {
mentions: [],
channels: [],
useRealName: false,
username: '',
baseUrl: '',
navToRoomInfo: () => {}
};
const MarkdownContext = React.createContext<IMarkdownContext>(defaultState);
export default MarkdownContext;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { View, Text } from 'react-native';
import { OrderedList as OrderedListProps } from '@rocket.chat/message-parser';
import Inline from './Inline';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IOrderedListProps {
value: OrderedListProps['value'];
}
const OrderedList = ({ value }: IOrderedListProps): JSX.Element => {
const { theme } = useTheme();
return (
<View>
{value.map((item, index) => (
<View style={styles.row}>
<Text style={[styles.text, { color: themes[theme!].bodyText }]}>{index + 1}. </Text>
<Inline value={item.value} />
</View>
))}
</View>
);
};
export default OrderedList;

View File

@ -0,0 +1,23 @@
import React from 'react';
import { Text } from 'react-native';
import { Paragraph as ParagraphProps } from '@rocket.chat/message-parser';
import Inline from './Inline';
import styles from '../styles';
import { useTheme } from '../../../theme';
import { themes } from '../../../constants/colors';
interface IParagraphProps {
value: ParagraphProps['value'];
}
const Paragraph = ({ value }: IParagraphProps): JSX.Element => {
const { theme } = useTheme();
return (
<Text style={[styles.text, { color: themes[theme!].bodyText }]}>
<Inline value={value} />
</Text>
);
};
export default Paragraph;

View File

@ -0,0 +1,22 @@
import React from 'react';
import { Text } from 'react-native';
import { Plain as PlainProps } from '@rocket.chat/message-parser';
import styles from '../styles';
import { useTheme } from '../../../theme';
import { themes } from '../../../constants/colors';
interface IPlainProps {
value: PlainProps['value'];
}
const Plain = ({ value }: IPlainProps): JSX.Element => {
const { theme } = useTheme();
return (
<Text accessibilityLabel={value} style={[styles.plainText, { color: themes[theme!].bodyText }]}>
{value}
</Text>
);
};
export default Plain;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { View } from 'react-native';
import { Quote as QuoteProps } from '@rocket.chat/message-parser';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
import styles from '../styles';
import Paragraph from './Paragraph';
interface IQuoteProps {
value: QuoteProps['value'];
}
const Quote = ({ value }: IQuoteProps): JSX.Element => {
const { theme } = useTheme();
return (
<View style={styles.container}>
<View style={[styles.quote, { backgroundColor: themes[theme!].borderColor }]} />
<View style={styles.childContainer}>
{value.map(item => (
<Paragraph value={item.value} />
))}
</View>
</View>
);
};
export default Quote;

View File

@ -0,0 +1,39 @@
import React from 'react';
import { StyleSheet, Text } from 'react-native';
import { Strike as StrikeProps } from '@rocket.chat/message-parser';
import Bold from './Bold';
import Italic from './Italic';
import Plain from './Plain';
import Link from './Link';
interface IStrikeProps {
value: StrikeProps['value'];
}
const styles = StyleSheet.create({
text: {
textDecorationLine: 'line-through'
}
});
const Strike = ({ value }: IStrikeProps): JSX.Element => (
<Text style={styles.text}>
{value.map(block => {
switch (block.type) {
case 'LINK':
return <Link value={block.value} />;
case 'PLAIN_TEXT':
return <Plain value={block.value} />;
case 'BOLD':
return <Bold value={block.value} />;
case 'ITALIC':
return <Italic value={block.value} />;
default:
return null;
}
})}
</Text>
);
export default Strike;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { Text, View } from 'react-native';
import { Tasks as TasksProps } from '@rocket.chat/message-parser';
import Inline from './Inline';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface ITasksProps {
value: TasksProps['value'];
}
const TaskList = ({ value = [] }: ITasksProps): JSX.Element => {
const { theme } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: themes[theme!].bodyText }]}>{item.status ? '- [x] ' : '- [ ] '}</Text>
<Inline value={item.value} />
</View>
))}
</View>
);
};
export default TaskList;

View File

@ -0,0 +1,28 @@
import React from 'react';
import { UnorderedList as UnorderedListProps } from '@rocket.chat/message-parser';
import { View, Text } from 'react-native';
import Inline from './Inline';
import styles from '../styles';
import { themes } from '../../../constants/colors';
import { useTheme } from '../../../theme';
interface IUnorderedListProps {
value: UnorderedListProps['value'];
}
const UnorderedList = ({ value }: IUnorderedListProps): JSX.Element => {
const { theme } = useTheme();
return (
<View>
{value.map(item => (
<View style={styles.row}>
<Text style={[styles.text, { color: themes[theme!].bodyText }]}>- </Text>
<Inline value={item.value} />
</View>
))}
</View>
);
};
export default UnorderedList;

View File

@ -0,0 +1,77 @@
import React from 'react';
import { MarkdownAST } from '@rocket.chat/message-parser';
import Quote from './Quote';
import Paragraph from './Paragraph';
import Heading from './Heading';
import Code from './Code';
import BigEmoji from './BigEmoji';
import OrderedList from './OrderedList';
import UnorderedList from './UnorderedList';
import { UserMention } from '../../message/interfaces';
import TaskList from './TaskList';
import MarkdownContext from './MarkdownContext';
interface IBodyProps {
tokens: MarkdownAST;
mentions: UserMention[];
channels: {
name: string;
_id: number;
}[];
getCustomEmoji?: Function;
onLinkPress?: Function;
navToRoomInfo: Function;
useRealName: boolean;
username: string;
baseUrl: string;
}
const Body = ({
tokens,
mentions,
channels,
useRealName,
username,
navToRoomInfo,
getCustomEmoji,
baseUrl,
onLinkPress
}: IBodyProps): JSX.Element => (
<MarkdownContext.Provider
value={{
mentions,
channels,
useRealName,
username,
navToRoomInfo,
getCustomEmoji,
baseUrl,
onLinkPress
}}>
{tokens.map(block => {
switch (block.type) {
case 'BIG_EMOJI':
return <BigEmoji value={block.value} />;
case 'UNORDERED_LIST':
return <UnorderedList value={block.value} />;
case 'ORDERED_LIST':
return <OrderedList value={block.value} />;
case 'TASKS':
return <TaskList value={block.value} />;
case 'QUOTE':
return <Quote value={block.value} />;
case 'PARAGRAPH':
return <Paragraph value={block.value} />;
case 'CODE':
return <Code value={block.value} />;
case 'HEADING':
return <Heading value={block.value} level={block.level} />;
default:
return null;
}
})}
</MarkdownContext.Provider>
);
export default Body;

View File

@ -30,6 +30,10 @@ export default StyleSheet.create<any>({
del: { del: {
textDecorationLine: 'line-through' textDecorationLine: 'line-through'
}, },
plainText: {
fontSize: 16,
flexShrink: 1
},
text: { text: {
fontSize: 16, fontSize: 16,
...sharedStyles.textRegular ...sharedStyles.textRegular
@ -70,12 +74,16 @@ export default StyleSheet.create<any>({
resizeMode: 'contain' resizeMode: 'contain'
}, },
codeInline: { codeInline: {
fontSize: 16,
...sharedStyles.textRegular, ...sharedStyles.textRegular,
...codeFontFamily, ...codeFontFamily,
borderWidth: 1, borderWidth: 1,
borderRadius: 4 borderRadius: 4,
paddingLeft: 2,
paddingTop: 2
}, },
codeBlock: { codeBlock: {
fontSize: 16,
...sharedStyles.textRegular, ...sharedStyles.textRegular,
...codeFontFamily, ...codeFontFamily,
borderWidth: 1, borderWidth: 1,

View File

@ -51,8 +51,10 @@ const Content = React.memo(
// @ts-ignore // @ts-ignore
<Markdown <Markdown
msg={props.msg} msg={props.msg}
md={props.md}
baseUrl={baseUrl} baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
enableMessageParser={user.enableMessageParserEarlyAdoption}
username={user.username} username={user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={isPreview ? 1 : 0} numberOfLines={isPreview ? 1 : 0}
@ -103,6 +105,9 @@ const Content = React.memo(
if (prevProps.isIgnored !== nextProps.isIgnored) { if (prevProps.isIgnored !== nextProps.isIgnored) {
return false; return false;
} }
if (!dequal(prevProps.md, nextProps.md)) {
return false;
}
if (!dequal(prevProps.mentions, nextProps.mentions)) { if (!dequal(prevProps.mentions, nextProps.mentions)) {
return false; return false;
} }

View File

@ -357,7 +357,8 @@ class MessageContainer extends React.Component<IMessageContainerProps, any> {
unread, unread,
blocks, blocks,
autoTranslate: autoTranslateMessage, autoTranslate: autoTranslateMessage,
replies replies,
md
} = item; } = item;
let message = msg; let message = msg;
@ -391,6 +392,7 @@ class MessageContainer extends React.Component<IMessageContainerProps, any> {
<Message <Message
id={id} id={id}
msg={message} msg={message}
md={md}
rid={rid} rid={rid}
author={u} author={u}
ts={ts} ts={ts}

View File

@ -1,3 +1,5 @@
import { MarkdownAST } from '@rocket.chat/message-parser';
export interface IMessageAttachments { export interface IMessageAttachments {
attachments: any; attachments: any;
timeFormat: string; timeFormat: string;
@ -48,12 +50,21 @@ export interface IMessageCallButton {
callJitsi: Function; callJitsi: Function;
} }
export interface IUser {
_id: string;
username: string;
name: string;
}
export type UserMention = Pick<IUser, '_id' | 'username' | 'name'>;
export interface IMessageContent { export interface IMessageContent {
isTemp: boolean; isTemp: boolean;
isInfo: boolean; isInfo: boolean;
tmid: string; tmid: string;
isThreadRoom: boolean; isThreadRoom: boolean;
msg: string; msg: string;
md: MarkdownAST;
theme: string; theme: string;
isEdited: boolean; isEdited: boolean;
isEncrypted: boolean; isEncrypted: boolean;
@ -62,7 +73,7 @@ export interface IMessageContent {
name: string; name: string;
_id: number; _id: number;
}[]; }[];
mentions: object[]; mentions: UserMention[];
navToRoomInfo: Function; navToRoomInfo: Function;
useRealName: boolean; useRealName: boolean;
isIgnored: boolean; isIgnored: boolean;

View File

@ -106,7 +106,6 @@ export default StyleSheet.create<any>({
}, },
image: { image: {
width: '100%', width: '100%',
// maxWidth: 400,
minHeight: isTablet ? 300 : 200, minHeight: isTablet ? 300 : 200,
borderRadius: 4, borderRadius: 4,
borderWidth: 1, borderWidth: 1,

View File

@ -81,7 +81,6 @@
"error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.", "error-you-are-last-owner": "You are the last owner. Please set new owner before leaving the room.",
"error-status-not-allowed": "Invisible status is disabled", "error-status-not-allowed": "Invisible status is disabled",
"Actions": "Actions", "Actions": "Actions",
"activity": "activity",
"Activity": "Activity", "Activity": "Activity",
"Add_Reaction": "Add Reaction", "Add_Reaction": "Add Reaction",
"Add_Server": "Add Server", "Add_Server": "Add Server",
@ -232,7 +231,6 @@
"Everyone_can_access_this_team": "Everyone can access this team", "Everyone_can_access_this_team": "Everyone can access this team",
"Error_uploading": "Error uploading", "Error_uploading": "Error uploading",
"Expiration_Days": "Expiration (Days)", "Expiration_Days": "Expiration (Days)",
"Favorite": "Favorite",
"Favorites": "Favorites", "Favorites": "Favorites",
"Files": "Files", "Files": "Files",
"File_description": "File description", "File_description": "File description",
@ -249,9 +247,6 @@
"Forward_to_user": "Forward to user", "Forward_to_user": "Forward to user",
"Full_table": "Click to see full table", "Full_table": "Click to see full table",
"Generate_New_Link": "Generate New Link", "Generate_New_Link": "Generate New Link",
"Group_by_favorites": "Group favorites",
"Group_by_type": "Group by type",
"Hide": "Hide",
"Has_joined_the_channel": "has joined the channel", "Has_joined_the_channel": "has joined the channel",
"Has_joined_the_conversation": "has joined the conversation", "Has_joined_the_conversation": "has joined the conversation",
"Has_left_the_channel": "has left the channel", "Has_left_the_channel": "has left the channel",
@ -334,7 +329,6 @@
"N_people_reacted": "{{n}} people reacted", "N_people_reacted": "{{n}} people reacted",
"N_users": "{{n}} users", "N_users": "{{n}} users",
"N_channels": "{{n}} channels", "N_channels": "{{n}} channels",
"name": "name",
"Name": "Name", "Name": "Name",
"Navigation_history": "Navigation history", "Navigation_history": "Navigation history",
"Never": "Never", "Never": "Never",
@ -412,7 +406,6 @@
"Reactions_are_disabled": "Reactions are disabled", "Reactions_are_disabled": "Reactions are disabled",
"Reactions_are_enabled": "Reactions are enabled", "Reactions_are_enabled": "Reactions are enabled",
"Reactions": "Reactions", "Reactions": "Reactions",
"Read": "Read",
"Read_External_Permission_Message": "Rocket.Chat needs to access photos, media, and files on your device", "Read_External_Permission_Message": "Rocket.Chat needs to access photos, media, and files on your device",
"Read_External_Permission": "Read Media Permission", "Read_External_Permission": "Read Media Permission",
"Read_Only_Channel": "Read Only Channel", "Read_Only_Channel": "Read Only Channel",
@ -507,7 +500,6 @@
"Sign_in_your_server": "Sign in your server", "Sign_in_your_server": "Sign in your server",
"Sign_Up": "Sign Up", "Sign_Up": "Sign Up",
"Some_field_is_invalid_or_empty": "Some field is invalid or empty", "Some_field_is_invalid_or_empty": "Some field is invalid or empty",
"Sorting_by": "Sorting by {{key}}",
"Sound": "Sound", "Sound": "Sound",
"Star_room": "Star room", "Star_room": "Star room",
"Star": "Star", "Star": "Star",
@ -546,7 +538,6 @@
"unarchive": "unarchive", "unarchive": "unarchive",
"UNARCHIVE": "UNARCHIVE", "UNARCHIVE": "UNARCHIVE",
"Unblock_user": "Unblock user", "Unblock_user": "Unblock user",
"Unfavorite": "Unfavorite",
"Unfollowed_thread": "Unfollowed thread", "Unfollowed_thread": "Unfollowed thread",
"Unmute": "Unmute", "Unmute": "Unmute",
"unmuted": "unmuted", "unmuted": "unmuted",
@ -688,7 +679,6 @@
"No_threads": "There are no threads", "No_threads": "There are no threads",
"No_threads_following": "You are not following any threads", "No_threads_following": "You are not following any threads",
"No_threads_unread": "There are no unread threads", "No_threads_unread": "There are no unread threads",
"No_discussions": "There are no discussions",
"Messagebox_Send_to_channel": "Send to channel", "Messagebox_Send_to_channel": "Send to channel",
"Leader": "Leader", "Leader": "Leader",
"Moderator": "Moderator", "Moderator": "Moderator",
@ -783,11 +773,15 @@
"creating_discussion": "creating discussion", "creating_discussion": "creating discussion",
"Canned_Responses": "Canned Responses", "Canned_Responses": "Canned Responses",
"No_match_found": "No match found.", "No_match_found": "No match found.",
"No_discussions": "No discussions",
"Check_canned_responses": "Check on canned responses.", "Check_canned_responses": "Check on canned responses.",
"Searching": "Searching", "Searching": "Searching",
"Use": "Use", "Use": "Use",
"Shortcut": "Shortcut", "Shortcut": "Shortcut",
"Content": "Content", "Content": "Content",
"Sharing": "Sharing", "Sharing": "Sharing",
"No_canned_responses": "No canned responses" "No_canned_responses": "No canned responses",
"Send_email_confirmation": "Send email confirmation",
"sending_email_confirmation": "sending email confirmation",
"Enable_Message_Parser": "Enable Message Parser"
} }

View File

@ -679,5 +679,7 @@
"Use": "Use", "Use": "Use",
"Shortcut": "Atalho", "Shortcut": "Atalho",
"Content": "Conteúdo", "Content": "Conteúdo",
"No_canned_responses": "Não há respostas predefinidas" "No_canned_responses": "Não há respostas predefinidas",
"Send_email_confirmation": "Enviar email de confirmação",
"sending_email_confirmation": "enviando email de confirmação"
} }

View File

@ -81,4 +81,6 @@ export default class Message extends Model {
@field('e2e') e2e; @field('e2e') e2e;
@field('tshow') tshow; @field('tshow') tshow;
@json('md', sanitizer) md;
} }

View File

@ -190,6 +190,15 @@ export default schemaMigrations({
] ]
}) })
] ]
},
{
toVersion: 14,
steps: [
addColumns({
table: 'messages',
columns: [{ name: 'md', type: 'string', isOptional: true }]
})
]
} }
] ]
}); });

View File

@ -25,4 +25,6 @@ export default class User extends Model {
@field('show_message_in_main_thread') showMessageInMainThread; @field('show_message_in_main_thread') showMessageInMainThread;
@field('is_from_webview') isFromWebView; @field('is_from_webview') isFromWebView;
@field('enable_message_parser_early_adoption') enableMessageParserEarlyAdoption;
} }

View File

@ -94,6 +94,15 @@ export default schemaMigrations({
columns: [{ name: 'is_from_webview', type: 'boolean', isOptional: true }] columns: [{ name: 'is_from_webview', type: 'boolean', isOptional: true }]
}) })
] ]
},
{
toVersion: 12,
steps: [
addColumns({
table: 'users',
columns: [{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }]
})
]
} }
] ]
}); });

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 13, version: 14,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'subscriptions', name: 'subscriptions',
@ -115,7 +115,8 @@ export default appSchema({
{ name: 'tmsg', type: 'string', isOptional: true }, { name: 'tmsg', type: 'string', isOptional: true },
{ name: 'blocks', type: 'string', isOptional: true }, { name: 'blocks', type: 'string', isOptional: true },
{ name: 'e2e', type: 'string', isOptional: true }, { name: 'e2e', type: 'string', isOptional: true },
{ name: 'tshow', type: 'boolean', isOptional: true } { name: 'tshow', type: 'boolean', isOptional: true },
{ name: 'md', type: 'string', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb'; import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({ export default appSchema({
version: 11, version: 12,
tables: [ tables: [
tableSchema({ tableSchema({
name: 'users', name: 'users',
@ -16,7 +16,8 @@ export default appSchema({
{ name: 'login_email_password', type: 'boolean', isOptional: true }, { name: 'login_email_password', type: 'boolean', isOptional: true },
{ name: 'show_message_in_main_thread', type: 'boolean', isOptional: true }, { name: 'show_message_in_main_thread', type: 'boolean', isOptional: true },
{ name: 'avatar_etag', type: 'string', isOptional: true }, { name: 'avatar_etag', type: 'string', isOptional: true },
{ name: 'is_from_webview', type: 'boolean', isOptional: true } { name: 'is_from_webview', type: 'boolean', isOptional: true },
{ name: 'enable_message_parser_early_adoption', type: 'boolean', isOptional: true }
] ]
}), }),
tableSchema({ tableSchema({

View File

@ -5,6 +5,7 @@ import { Q } from '@nozbe/watermelondb';
import AsyncStorage from '@react-native-community/async-storage'; import AsyncStorage from '@react-native-community/async-storage';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord'; import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
import RNFetchBlob from 'rn-fetch-blob'; import RNFetchBlob from 'rn-fetch-blob';
import isEmpty from 'lodash/isEmpty';
import defaultSettings from '../constants/settings'; import defaultSettings from '../constants/settings';
import log from '../utils/log'; import log from '../utils/log';
@ -530,6 +531,10 @@ const RocketChat = {
return this.post('users.forgotPassword', { email }, false); return this.post('users.forgotPassword', { email }, false);
}, },
sendConfirmationEmail(email) {
return this.methodCallWrapper('sendConfirmationEmail', email);
},
loginTOTP(params, loginEmailPassword, isFromWebView = false) { loginTOTP(params, loginEmailPassword, isFromWebView = false) {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {
@ -622,7 +627,8 @@ const RocketChat = {
roles: result.me.roles, roles: result.me.roles,
avatarETag: result.me.avatarETag, avatarETag: result.me.avatarETag,
isFromWebView, isFromWebView,
showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true showMessageInMainThread: result.me.settings?.preferences?.showMessageInMainThread ?? true,
enableMessageParserEarlyAdoption: result.me.settings?.preferences?.enableMessageParserEarlyAdoption ?? true
}; };
return user; return user;
}, },
@ -1069,8 +1075,12 @@ const RocketChat = {
}, },
methodCallWrapper(method, ...params) { methodCallWrapper(method, ...params) {
const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings; const { API_Use_REST_For_DDP_Calls } = reduxStore.getState().settings;
const { user } = reduxStore.getState().login;
if (API_Use_REST_For_DDP_Calls) { if (API_Use_REST_For_DDP_Calls) {
return this.post(`method.call/${method}`, { message: EJSON.stringify({ method, params }) }); const url = isEmpty(user) ? 'method.callAnon' : 'method.call';
return this.post(`${url}/${method}`, {
message: EJSON.stringify({ method, params })
});
} }
const parsedParams = params.map(param => { const parsedParams = params.map(param => {
if (param instanceof Date) { if (param instanceof Date) {

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { I18nManager, StyleSheet, TextInput, TextInputProps } from 'react-native'; import { I18nManager, StyleProp, StyleSheet, TextInput, TextInputProps, TextStyle } from 'react-native';
import { themes } from '../constants/colors'; import { themes } from '../constants/colors';
@ -10,7 +10,7 @@ const styles = StyleSheet.create({
}); });
interface IThemedTextInput extends TextInputProps { interface IThemedTextInput extends TextInputProps {
style: object; style: StyleProp<TextStyle>;
theme: string; theme: string;
} }

View File

@ -20,7 +20,6 @@ import { encryptionInit, encryptionStop } from '../actions/encryption';
import UserPreferences from '../lib/userPreferences'; import UserPreferences from '../lib/userPreferences';
import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry'; import { inquiryRequest, inquiryReset } from '../ee/omnichannel/actions/inquiry';
import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib'; import { isOmnichannelStatusAvailable } from '../ee/omnichannel/lib';
import Navigation from '../lib/Navigation';
const getServer = state => state.server.server; const getServer = state => state.server.server;
const loginWithPasswordCall = args => RocketChat.loginWithPassword(args); const loginWithPasswordCall = args => RocketChat.loginWithPassword(args);
@ -191,8 +190,6 @@ const handleLogout = function* handleLogout({ forcedByServer }) {
yield put(appStart({ root: ROOT_OUTSIDE })); yield put(appStart({ root: ROOT_OUTSIDE }));
showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops')); showErrorAlert(I18n.t('Logged_out_by_server'), I18n.t('Oops'));
yield delay(300); yield delay(300);
Navigation.navigate('NewServerView');
yield delay(300);
EventEmitter.emit('NewServer', { server }); EventEmitter.emit('NewServer', { server });
} else { } else {
const serversDB = database.servers; const serversDB = database.servers;

View File

@ -10,6 +10,7 @@ import NewServerView from '../views/NewServerView';
import WorkspaceView from '../views/WorkspaceView'; import WorkspaceView from '../views/WorkspaceView';
import LoginView from '../views/LoginView'; import LoginView from '../views/LoginView';
import ForgotPasswordView from '../views/ForgotPasswordView'; import ForgotPasswordView from '../views/ForgotPasswordView';
import SendEmailConfirmationView from '../views/SendEmailConfirmationView';
import RegisterView from '../views/RegisterView'; import RegisterView from '../views/RegisterView';
import LegalView from '../views/LegalView'; import LegalView from '../views/LegalView';
import AuthenticationWebView from '../views/AuthenticationWebView'; import AuthenticationWebView from '../views/AuthenticationWebView';
@ -25,6 +26,11 @@ const _OutsideStack = () => {
<Outside.Screen name='WorkspaceView' component={WorkspaceView} options={WorkspaceView.navigationOptions} /> <Outside.Screen name='WorkspaceView' component={WorkspaceView} options={WorkspaceView.navigationOptions} />
<Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} /> <Outside.Screen name='LoginView' component={LoginView} options={LoginView.navigationOptions} />
<Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} options={ForgotPasswordView.navigationOptions} /> <Outside.Screen name='ForgotPasswordView' component={ForgotPasswordView} options={ForgotPasswordView.navigationOptions} />
<Outside.Screen
name='SendEmailConfirmationView'
component={SendEmailConfirmationView}
options={SendEmailConfirmationView.navigationOptions}
/>
<Outside.Screen name='RegisterView' component={RegisterView} options={RegisterView.navigationOptions} /> <Outside.Screen name='RegisterView' component={RegisterView} options={RegisterView.navigationOptions} />
<Outside.Screen name='LegalView' component={LegalView} options={LegalView.navigationOptions} /> <Outside.Screen name='LegalView' component={LegalView} options={LegalView.navigationOptions} />
</Outside.Navigator> </Outside.Navigator>

View File

@ -3,14 +3,14 @@ import hoistNonReactStatics from 'hoist-non-react-statics';
interface IThemeContextProps { interface IThemeContextProps {
theme: string; theme: string;
themePreferences: { themePreferences?: {
currentTheme: 'automatic' | 'light'; currentTheme: 'automatic' | 'light';
darkLevel: string; darkLevel: string;
}; };
setTheme: (newTheme?: {}) => void; setTheme?: (newTheme?: {}) => void;
} }
export const ThemeContext = React.createContext<Partial<IThemeContextProps>>({ theme: 'light' }); export const ThemeContext = React.createContext<IThemeContextProps>({ theme: 'light' });
export function withTheme(Component: React.ComponentType<any>): (props: any) => JSX.Element { export function withTheme(Component: React.ComponentType<any>): (props: any) => JSX.Element {
const ThemedComponent = (props: any) => ( const ThemedComponent = (props: any) => (

View File

@ -11,6 +11,9 @@ export default {
FP_FORGOT_PASSWORD: 'fp_forgot_password', FP_FORGOT_PASSWORD: 'fp_forgot_password',
FP_FORGOT_PASSWORD_F: 'fp_forgot_password_f', FP_FORGOT_PASSWORD_F: 'fp_forgot_password_f',
// SEND EMAIL CONFIRMATION VIEW
SEC_SEND_EMAIL_CONFIRMATION: 'sec_send_email_confirmation',
// REGISTER VIEW // REGISTER VIEW
REGISTER_DEFAULT_SIGN_UP: 'register_default_sign_up', REGISTER_DEFAULT_SIGN_UP: 'register_default_sign_up',
REGISTER_DEFAULT_SIGN_UP_F: 'register_default_sign_up_f', REGISTER_DEFAULT_SIGN_UP_F: 'register_default_sign_up_f',

View File

@ -1,11 +0,0 @@
import { isTablet } from './deviceInfo';
const guidelineBaseWidth = isTablet ? 600 : 375;
const guidelineBaseHeight = isTablet ? 800 : 667;
// TODO: we need to refactor this
const scale = (size, width) => (width / guidelineBaseWidth) * size;
const verticalScale = (size, height) => (height / guidelineBaseHeight) * size;
const moderateScale = (size, factor = 0.5, width) => size + (scale(size, width) - size) * factor;
export { scale, verticalScale, moderateScale };

16
app/utils/scaling.ts Normal file
View File

@ -0,0 +1,16 @@
import { isTablet } from './deviceInfo';
const guidelineBaseWidth = isTablet ? 600 : 375;
const guidelineBaseHeight = isTablet ? 800 : 667;
function scale({ size, width }: { size: number; width: number }): number {
return (width / guidelineBaseWidth) * size;
}
function verticalScale({ size, height }: { size: number; height: number }): number {
return (height / guidelineBaseHeight) * size;
}
function moderateScale({ size, factor = 0.5, width }: { size: number; factor?: number; width: number }): number {
return size + (scale({ size, width }) - size) * factor;
}
export { scale, verticalScale, moderateScale };

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import { WebView, WebViewNavigation } from 'react-native-webview';
import { WebView } from 'react-native-webview';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import parse from 'url-parse'; import parse from 'url-parse';
import { StackNavigationProp } from '@react-navigation/stack';
import { WebViewMessage } from 'react-native-webview/lib/WebViewTypes';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import { isIOS } from '../utils/deviceInfo'; import { isIOS } from '../utils/deviceInfo';
@ -40,17 +41,44 @@ window.addEventListener('popstate', function() {
}); });
`; `;
class AuthenticationWebView extends React.PureComponent { interface IRoute {
static propTypes = { params: {
navigation: PropTypes.object, authType: string;
route: PropTypes.object, url: string;
server: PropTypes.string, ssoToken?: string;
Accounts_Iframe_api_url: PropTypes.bool, };
Accounts_Iframe_api_method: PropTypes.bool, }
theme: PropTypes.string
interface INavigationOption {
navigation: StackNavigationProp<any, 'AuthenticationWebView'>;
route: IRoute;
}
interface IAuthenticationWebView extends INavigationOption {
server: string;
Accounts_Iframe_api_url: string;
Accounts_Iframe_api_method: string;
theme: string;
}
interface IState {
logging: boolean;
loading: boolean;
}
class AuthenticationWebView extends React.PureComponent<IAuthenticationWebView, IState> {
private oauthRedirectRegex: RegExp;
private iframeRedirectRegex: RegExp;
static navigationOptions = ({ route, navigation }: INavigationOption) => {
const { authType } = route.params;
return {
headerLeft: () => <HeaderButton.CloseModal navigation={navigation} />,
title: ['saml', 'cas', 'iframe'].includes(authType) ? 'SSO' : 'OAuth'
};
}; };
constructor(props) { constructor(props: IAuthenticationWebView) {
super(props); super(props);
this.state = { this.state = {
logging: false, logging: false,
@ -71,7 +99,7 @@ class AuthenticationWebView extends React.PureComponent {
navigation.pop(); navigation.pop();
}; };
login = params => { login = (params: any) => {
const { logging } = this.state; const { logging } = this.state;
if (logging) { if (logging) {
return; return;
@ -89,7 +117,7 @@ class AuthenticationWebView extends React.PureComponent {
}; };
// Force 3s delay so the server has time to evaluate the token // Force 3s delay so the server has time to evaluate the token
debouncedLogin = debounce(params => this.login(params), 3000); debouncedLogin = debounce((params: any) => this.login(params), 3000);
tryLogin = debounce( tryLogin = debounce(
async () => { async () => {
@ -104,7 +132,7 @@ class AuthenticationWebView extends React.PureComponent {
true true
); );
onNavigationStateChange = webViewState => { onNavigationStateChange = (webViewState: WebViewNavigation | WebViewMessage) => {
const url = decodeURIComponent(webViewState.url); const url = decodeURIComponent(webViewState.url);
const { route } = this.props; const { route } = this.props;
const { authType } = route.params; const { authType } = route.params;
@ -180,18 +208,10 @@ class AuthenticationWebView extends React.PureComponent {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url, Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url,
Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method Accounts_Iframe_api_method: state.settings.Accounts_Iframe_api_method
}); });
AuthenticationWebView.navigationOptions = ({ route, navigation }) => {
const { authType } = route.params;
return {
headerLeft: () => <HeaderButton.CloseModal navigation={navigation} />,
title: ['saml', 'cas', 'iframe'].includes(authType) ? 'SSO' : 'OAuth'
};
};
export default connect(mapStateToProps)(withTheme(AuthenticationWebView)); export default connect(mapStateToProps)(withTheme(AuthenticationWebView));

View File

@ -1,8 +1,10 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { ScrollView, Share, View } from 'react-native'; import { ScrollView, Share, View } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { StackNavigationProp, StackNavigationOptions } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import { Dispatch } from 'redux';
import { import {
inviteLinksClear as inviteLinksClearAction, inviteLinksClear as inviteLinksClearAction,
@ -20,22 +22,28 @@ import SafeAreaView from '../../containers/SafeAreaView';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
import styles from './styles'; import styles from './styles';
class InviteUsersView extends React.Component { interface IInviteUsersView {
static navigationOptions = () => ({ navigation: StackNavigationProp<any, 'InviteUsersView'>;
title: I18n.t('Invite_users') route: RouteProp<any, 'InviteUsersView'>;
}); theme: string;
timeDateFormat: string;
invite: {
url: string;
expires: number;
maxUses: number;
uses: number;
};
createInviteLink(rid: string): void;
clearInviteLink(): void;
}
class InviteUsersView extends React.Component<IInviteUsersView, any> {
private rid: string;
static propTypes = { static navigationOptions: StackNavigationOptions = {
navigation: PropTypes.object, title: I18n.t('Invite_users')
route: PropTypes.object,
theme: PropTypes.string,
timeDateFormat: PropTypes.string,
invite: PropTypes.object,
createInviteLink: PropTypes.func,
clearInviteLink: PropTypes.func
}; };
constructor(props) { constructor(props: IInviteUsersView) {
super(props); super(props);
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
} }
@ -97,6 +105,7 @@ class InviteUsersView extends React.Component {
renderExpiration = () => { renderExpiration = () => {
const { theme } = this.props; const { theme } = this.props;
const expirationMessage = this.linkExpirationText(); const expirationMessage = this.linkExpirationText();
// @ts-ignore
return <Markdown msg={expirationMessage} username='' baseUrl='' theme={theme} />; return <Markdown msg={expirationMessage} username='' baseUrl='' theme={theme} />;
}; };
@ -104,10 +113,10 @@ class InviteUsersView extends React.Component {
const { theme, invite } = this.props; const { theme, invite } = this.props;
return ( return (
<SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }}> <SafeAreaView style={{ backgroundColor: themes[theme].backgroundColor }}>
{/* @ts-ignore*/}
<ScrollView <ScrollView
{...scrollPersistTaps} {...scrollPersistTaps}
style={{ backgroundColor: themes[theme].auxiliaryBackground }} style={{ backgroundColor: themes[theme].auxiliaryBackground }}
contentContainerStyle={styles.contentContainer}
showsVerticalScrollIndicator={false}> showsVerticalScrollIndicator={false}>
<StatusBar /> <StatusBar />
<View style={styles.innerContainer}> <View style={styles.innerContainer}>
@ -123,15 +132,15 @@ class InviteUsersView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
timeDateFormat: state.settings.Message_TimeAndDateFormat, timeDateFormat: state.settings.Message_TimeAndDateFormat,
days: state.inviteLinks.days, days: state.inviteLinks.days,
maxUses: state.inviteLinks.maxUses, maxUses: state.inviteLinks.maxUses,
invite: state.inviteLinks.invite invite: state.inviteLinks.invite
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
createInviteLink: rid => dispatch(inviteLinksCreateAction(rid)), createInviteLink: (rid: string) => dispatch(inviteLinksCreateAction(rid)),
clearInviteLink: () => dispatch(inviteLinksClearAction()) clearInviteLink: () => dispatch(inviteLinksClearAction())
}); });

View File

@ -1,8 +1,9 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Alert, Keyboard, StyleSheet, Text, View } from 'react-native'; import { Alert, Keyboard, StyleSheet, Text, View } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import Button from '../containers/Button'; import Button from '../containers/Button';
import I18n from '../i18n'; import I18n from '../i18n';
@ -46,31 +47,35 @@ const styles = StyleSheet.create({
} }
}); });
class LoginView extends React.Component { interface IProps {
static navigationOptions = ({ route, navigation }) => ({ navigation: StackNavigationProp<any>;
title: route.params?.title ?? 'Rocket.Chat', route: RouteProp<any, 'RegisterView'>;
Site_Name: string;
Accounts_RegistrationForm: string;
Accounts_RegistrationForm_LinkReplacementText: string;
Accounts_EmailOrUsernamePlaceholder: string;
Accounts_PasswordPlaceholder: string;
Accounts_PasswordReset: boolean;
Accounts_ShowFormLogin: boolean;
isFetching: boolean;
error: {
error: string;
};
failure: boolean;
theme: string;
loginRequest: Function;
inviteLinkToken: string;
}
class LoginView extends React.Component<IProps, any> {
private passwordInput: any;
static navigationOptions = ({ route, navigation }: Partial<IProps>) => ({
title: route?.params?.title ?? 'Rocket.Chat',
headerRight: () => <HeaderButton.Legal testID='login-view-more' navigation={navigation} /> headerRight: () => <HeaderButton.Legal testID='login-view-more' navigation={navigation} />
}); });
static propTypes = { constructor(props: IProps) {
navigation: PropTypes.object,
route: PropTypes.object,
Site_Name: PropTypes.string,
Accounts_RegistrationForm: PropTypes.string,
Accounts_RegistrationForm_LinkReplacementText: PropTypes.string,
Accounts_EmailOrUsernamePlaceholder: PropTypes.string,
Accounts_PasswordPlaceholder: PropTypes.string,
Accounts_PasswordReset: PropTypes.bool,
Accounts_ShowFormLogin: PropTypes.bool,
isFetching: PropTypes.bool,
error: PropTypes.object,
failure: PropTypes.bool,
theme: PropTypes.string,
loginRequest: PropTypes.func,
inviteLinkToken: PropTypes.string
};
constructor(props) {
super(props); super(props);
this.state = { this.state = {
user: props.route.params?.username ?? '', user: props.route.params?.username ?? '',
@ -78,12 +83,16 @@ class LoginView extends React.Component {
}; };
} }
UNSAFE_componentWillReceiveProps(nextProps) { UNSAFE_componentWillReceiveProps(nextProps: IProps) {
const { error } = this.props; const { error } = this.props;
if (nextProps.failure && !dequal(error, nextProps.error)) { if (nextProps.failure && !dequal(error, nextProps.error)) {
if (nextProps.error?.error === 'error-invalid-email') {
this.resendEmailConfirmation();
} else {
Alert.alert(I18n.t('Oops'), I18n.t('Login_error')); Alert.alert(I18n.t('Oops'), I18n.t('Login_error'));
} }
} }
}
get showRegistrationButton() { get showRegistrationButton() {
const { Accounts_RegistrationForm, inviteLinkToken } = this.props; const { Accounts_RegistrationForm, inviteLinkToken } = this.props;
@ -105,6 +114,12 @@ class LoginView extends React.Component {
navigation.navigate('ForgotPasswordView', { title: Site_Name }); navigation.navigate('ForgotPasswordView', { title: Site_Name });
}; };
resendEmailConfirmation = () => {
const { user } = this.state;
const { navigation } = this.props;
navigation.navigate('SendEmailConfirmationView', { user });
};
valid = () => { valid = () => {
const { user, password } = this.state; const { user, password } = this.state;
return user.trim() && password.trim(); return user.trim() && password.trim();
@ -146,7 +161,7 @@ class LoginView extends React.Component {
placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Username_or_email')} placeholder={Accounts_EmailOrUsernamePlaceholder || I18n.t('Username_or_email')}
keyboardType='email-address' keyboardType='email-address'
returnKeyType='next' returnKeyType='next'
onChangeText={value => this.setState({ user: value })} onChangeText={(value: string) => this.setState({ user: value })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.passwordInput.focus(); this.passwordInput.focus();
}} }}
@ -166,7 +181,7 @@ class LoginView extends React.Component {
returnKeyType='send' returnKeyType='send'
secureTextEntry secureTextEntry
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
onChangeText={value => this.setState({ password: value })} onChangeText={(value: string) => this.setState({ password: value })}
testID='login-view-password' testID='login-view-password'
textContentType='password' textContentType='password'
autoCompleteType='password' autoCompleteType='password'
@ -227,7 +242,7 @@ class LoginView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Site_Name: state.settings.Site_Name, Site_Name: state.settings.Site_Name,
Accounts_ShowFormLogin: state.settings.Accounts_ShowFormLogin, Accounts_ShowFormLogin: state.settings.Accounts_ShowFormLogin,
@ -242,8 +257,8 @@ const mapStateToProps = state => ({
inviteLinkToken: state.inviteLinks.token inviteLinkToken: state.inviteLinks.token
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: any) => ({
loginRequest: params => dispatch(loginRequestAction(params)) loginRequest: (params: any) => dispatch(loginRequestAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(LoginView));

View File

@ -1,12 +1,12 @@
import React from 'react'; import React from 'react';
import { StyleSheet, Text, View } from 'react-native'; import { StyleSheet, Text, View } from 'react-native';
import PropTypes from 'prop-types';
import { BorderlessButton } from 'react-native-gesture-handler'; import { BorderlessButton } from 'react-native-gesture-handler';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import { CustomIcon } from '../../../lib/Icons'; import { CustomIcon } from '../../../lib/Icons';
import sharedStyles from '../../Styles'; import sharedStyles from '../../Styles';
import Touch from '../../../utils/touch'; import Touch from '../../../utils/touch';
import { IServer } from '../index';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -27,13 +27,20 @@ const styles = StyleSheet.create({
} }
}); });
const Item = ({ item, theme, onPress, onDelete }) => ( interface IItem {
item: IServer;
theme: string;
onPress(url: string): void;
onDelete(item: IServer): void;
}
const Item = ({ item, theme, onPress, onDelete }: IItem): JSX.Element => (
<Touch style={styles.container} onPress={() => onPress(item.url)} theme={theme} testID={`server-history-${item.url}`}> <Touch style={styles.container} onPress={() => onPress(item.url)} theme={theme} testID={`server-history-${item.url}`}>
<View style={styles.content}> <View style={styles.content}>
<Text numberOfLines={1} style={[styles.server, { color: themes[theme].bodyText }]}> <Text numberOfLines={1} style={[styles.server, { color: themes[theme].bodyText }]}>
{item.url} {item.url}
</Text> </Text>
<Text numberOfLines={1} style={[styles.username, { color: themes[theme].auxiliaryText }]}> <Text numberOfLines={1} style={{ color: themes[theme].auxiliaryText }}>
{item.username} {item.username}
</Text> </Text>
</View> </View>
@ -43,11 +50,4 @@ const Item = ({ item, theme, onPress, onDelete }) => (
</Touch> </Touch>
); );
Item.propTypes = {
item: PropTypes.object,
theme: PropTypes.string,
onPress: PropTypes.func,
onDelete: PropTypes.func
};
export default Item; export default Item;

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native'; import { FlatList, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import TextInput from '../../../containers/TextInput'; import TextInput from '../../../containers/TextInput';
import * as List from '../../../containers/List'; import * as List from '../../../containers/List';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import I18n from '../../../i18n'; import I18n from '../../../i18n';
import Item from './Item'; import Item from './Item';
import { IServer } from '../index';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -28,7 +28,25 @@ const styles = StyleSheet.create({
} }
}); });
const ServerInput = ({ text, theme, serversHistory, onChangeText, onSubmit, onDelete, onPressServerHistory }) => { interface IServerInput {
text: string;
theme: string;
serversHistory: any[];
onChangeText(text: string): void;
onSubmit(): void;
onDelete(item: IServer): void;
onPressServerHistory(serverHistory: IServer): void;
}
const ServerInput = ({
text,
theme,
serversHistory,
onChangeText,
onSubmit,
onDelete,
onPressServerHistory
}: IServerInput): JSX.Element => {
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
return ( return (
<View style={styles.container}> <View style={styles.container}>
@ -68,14 +86,4 @@ const ServerInput = ({ text, theme, serversHistory, onChangeText, onSubmit, onDe
); );
}; };
ServerInput.propTypes = {
text: PropTypes.string,
theme: PropTypes.string,
serversHistory: PropTypes.array,
onChangeText: PropTypes.func,
onSubmit: PropTypes.func,
onDelete: PropTypes.func,
onPressServerHistory: PropTypes.func
};
export default ServerInput; export default ServerInput;

View File

@ -1,5 +1,4 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Text, Keyboard, StyleSheet, View, BackHandler, Image } from 'react-native'; import { Text, Keyboard, StyleSheet, View, BackHandler, Image } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Base64 } from 'js-base64'; import { Base64 } from 'js-base64';
@ -7,6 +6,9 @@ import parse from 'url-parse';
import { Q } from '@nozbe/watermelondb'; import { Q } from '@nozbe/watermelondb';
import { TouchableOpacity } from 'react-native-gesture-handler'; import { TouchableOpacity } from 'react-native-gesture-handler';
import Orientation from 'react-native-orientation-locker'; import Orientation from 'react-native-orientation-locker';
import { StackNavigationProp } from '@react-navigation/stack';
import { Dispatch } from 'redux';
import Model from '@nozbe/watermelondb/Model';
import UserPreferences from '../../lib/userPreferences'; import UserPreferences from '../../lib/userPreferences';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
@ -19,7 +21,6 @@ import FormContainer, { FormContainerInner } from '../../containers/FormContaine
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { events, logEvent } from '../../utils/log'; import { events, logEvent } from '../../utils/log';
import { animateNextTransition } from '../../utils/layoutAnimation';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import { BASIC_AUTH_KEY, setBasicAuth } from '../../utils/fetch'; import { BASIC_AUTH_KEY, setBasicAuth } from '../../utils/fetch';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
@ -66,19 +67,32 @@ const styles = StyleSheet.create({
} }
}); });
class NewServerView extends React.Component { export interface IServer extends Model {
static propTypes = { url: string;
navigation: PropTypes.object, username: string;
theme: PropTypes.string, }
connecting: PropTypes.bool.isRequired, interface INewServerView {
connectServer: PropTypes.func.isRequired, navigation: StackNavigationProp<any, 'NewServerView'>;
selectServer: PropTypes.func.isRequired, theme: string;
previousServer: PropTypes.string, connecting: boolean;
inviteLinksClear: PropTypes.func, connectServer(server: string, username?: string, fromServerHistory?: boolean): void;
serverFinishAdd: PropTypes.func selectServer(server: string): void;
}; previousServer: string;
inviteLinksClear(): void;
serverFinishAdd(): void;
width: number;
height: number;
}
constructor(props) { interface IState {
text: string;
connectingOpen: boolean;
certificate: any;
serversHistory: IServer[];
}
class NewServerView extends React.Component<INewServerView, IState> {
constructor(props: INewServerView) {
super(props); super(props);
if (!isTablet) { if (!isTablet) {
Orientation.lockToPortrait(); Orientation.lockToPortrait();
@ -131,21 +145,21 @@ class NewServerView extends React.Component {
return false; return false;
}; };
onChangeText = text => { onChangeText = (text: string) => {
this.setState({ text }); this.setState({ text });
this.queryServerHistory(text); this.queryServerHistory(text);
}; };
queryServerHistory = async text => { queryServerHistory = async (text?: string) => {
const db = database.servers; const db = database.servers;
try { try {
const serversHistoryCollection = db.get('servers_history'); const serversHistoryCollection = db.get('servers_history');
let whereClause = [Q.where('username', Q.notEq(null)), Q.experimentalSortBy('updated_at', Q.desc), Q.experimentalTake(3)]; let whereClause = [Q.where('username', Q.notEq(null)), Q.experimentalSortBy('updated_at', Q.desc), Q.experimentalTake(3)];
const likeString = sanitizeLikeString(text);
if (text) { if (text) {
const likeString = sanitizeLikeString(text);
whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))]; whereClause = [...whereClause, Q.where('url', Q.like(`%${likeString}%`))];
} }
const serversHistory = await serversHistoryCollection.query(...whereClause).fetch(); const serversHistory = (await serversHistoryCollection.query(...whereClause).fetch()) as IServer[];
this.setState({ serversHistory }); this.setState({ serversHistory });
} catch { } catch {
// Do nothing // Do nothing
@ -158,7 +172,7 @@ class NewServerView extends React.Component {
selectServer(previousServer); selectServer(previousServer);
}; };
handleNewServerEvent = event => { handleNewServerEvent = (event: { server: string }) => {
let { server } = event; let { server } = event;
if (!server) { if (!server) {
return; return;
@ -169,13 +183,11 @@ class NewServerView extends React.Component {
connectServer(server); connectServer(server);
}; };
onPressServerHistory = serverHistory => { onPressServerHistory = (serverHistory: IServer) => {
this.setState({ text: serverHistory?.url }, () => this.setState({ text: serverHistory.url }, () => this.submit(true, serverHistory?.username));
this.submit({ fromServerHistory: true, username: serverHistory?.username })
);
}; };
submit = async ({ fromServerHistory = false, username }) => { submit = async (fromServerHistory?: boolean, username?: string) => {
logEvent(events.NS_CONNECT_TO_WORKSPACE); logEvent(events.NS_CONNECT_TO_WORKSPACE);
const { text, certificate } = this.state; const { text, certificate } = this.state;
const { connectServer } = this.props; const { connectServer } = this.props;
@ -207,7 +219,7 @@ class NewServerView extends React.Component {
connectServer('https://open.rocket.chat'); connectServer('https://open.rocket.chat');
}; };
basicAuth = async (server, text) => { basicAuth = async (server: string, text: string) => {
try { try {
const parsedUrl = parse(text, true); const parsedUrl = parse(text, true);
if (parsedUrl.auth.length) { if (parsedUrl.auth.length) {
@ -222,14 +234,14 @@ class NewServerView extends React.Component {
chooseCertificate = async () => { chooseCertificate = async () => {
try { try {
const certificate = await SSLPinning.pickCertificate(); const certificate = await SSLPinning?.pickCertificate();
this.setState({ certificate }); this.setState({ certificate });
} catch { } catch {
// Do nothing // Do nothing
} }
}; };
completeUrl = url => { completeUrl = (url: string) => {
const parsedUrl = parse(url, true); const parsedUrl = parse(url, true);
if (parsedUrl.auth.length) { if (parsedUrl.auth.length) {
url = parsedUrl.origin; url = parsedUrl.origin;
@ -252,14 +264,11 @@ class NewServerView extends React.Component {
return url.replace(/\/+$/, '').replace(/\\/g, '/'); return url.replace(/\/+$/, '').replace(/\\/g, '/');
}; };
uriToPath = uri => uri.replace('file://', ''); uriToPath = (uri: string) => uri.replace('file://', '');
saveCertificate = certificate => {
animateNextTransition();
this.setState({ certificate });
};
handleRemove = () => { handleRemove = () => {
// TODO: Remove ts-ignore when migrate the showConfirmationAlert
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_unset_a_certificate_for_this_server'), message: I18n.t('You_will_unset_a_certificate_for_this_server'),
confirmationText: I18n.t('Remove'), confirmationText: I18n.t('Remove'),
@ -267,14 +276,15 @@ class NewServerView extends React.Component {
}); });
}; };
deleteServerHistory = async item => { deleteServerHistory = async (item: IServer) => {
const { serversHistory } = this.state;
const db = database.servers; const db = database.servers;
try { try {
await db.action(async () => { await db.write(async () => {
await item.destroyPermanently(); await item.destroyPermanently();
}); });
this.setState({ serversHistory: serversHistory.filter(server => server.id !== item.id) }); this.setState((prevstate: IState) => ({
serversHistory: prevstate.serversHistory.filter((server: IServer) => server.id !== item.id)
}));
} catch { } catch {
// Nothing // Nothing
} }
@ -288,20 +298,21 @@ class NewServerView extends React.Component {
style={[ style={[
styles.certificatePicker, styles.certificatePicker,
{ {
marginBottom: verticalScale(previousServer && !isTablet ? 10 : 30, height) marginBottom: verticalScale({ size: previousServer && !isTablet ? 10 : 30, height })
} }
]}> ]}>
<Text <Text
style={[ style={[
styles.chooseCertificateTitle, styles.chooseCertificateTitle,
{ color: themes[theme].auxiliaryText, fontSize: moderateScale(13, null, width) } { color: themes[theme].auxiliaryText, fontSize: moderateScale({ size: 13, width }) }
]}> ]}>
{certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')} {certificate ? I18n.t('Your_certificate') : I18n.t('Do_you_have_a_certificate')}
</Text> </Text>
<TouchableOpacity <TouchableOpacity
onPress={certificate ? this.handleRemove : this.chooseCertificate} onPress={certificate ? this.handleRemove : this.chooseCertificate}
testID='new-server-choose-certificate'> testID='new-server-choose-certificate'>
<Text style={[styles.chooseCertificate, { color: themes[theme].tintColor, fontSize: moderateScale(13, null, width) }]}> <Text
style={[styles.chooseCertificate, { color: themes[theme].tintColor, fontSize: moderateScale({ size: 13, width }) }]}>
{certificate ?? I18n.t('Apply_Your_Certificate')} {certificate ?? I18n.t('Apply_Your_Certificate')}
</Text> </Text>
</TouchableOpacity> </TouchableOpacity>
@ -321,10 +332,10 @@ class NewServerView extends React.Component {
style={[ style={[
styles.onboardingImage, styles.onboardingImage,
{ {
marginBottom: verticalScale(10, height), marginBottom: verticalScale({ size: 10, height }),
marginTop: isTablet ? 0 : verticalScale(marginTop, height), marginTop: isTablet ? 0 : verticalScale({ size: marginTop, height }),
width: verticalScale(100, height), width: verticalScale({ size: 100, height }),
height: verticalScale(100, height) height: verticalScale({ size: 100, height })
} }
]} ]}
source={require('../../static/images/logo.png')} source={require('../../static/images/logo.png')}
@ -335,8 +346,8 @@ class NewServerView extends React.Component {
styles.title, styles.title,
{ {
color: themes[theme].titleText, color: themes[theme].titleText,
fontSize: moderateScale(22, null, width), fontSize: moderateScale({ size: 22, width }),
marginBottom: verticalScale(8, height) marginBottom: verticalScale({ size: 8, height })
} }
]}> ]}>
Rocket.Chat Rocket.Chat
@ -346,8 +357,8 @@ class NewServerView extends React.Component {
styles.subtitle, styles.subtitle,
{ {
color: themes[theme].controlText, color: themes[theme].controlText,
fontSize: moderateScale(16, null, width), fontSize: moderateScale({ size: 16, width }),
marginBottom: verticalScale(30, height) marginBottom: verticalScale({ size: 30, height })
} }
]}> ]}>
{I18n.t('Onboarding_subtitle')} {I18n.t('Onboarding_subtitle')}
@ -367,7 +378,7 @@ class NewServerView extends React.Component {
onPress={this.submit} onPress={this.submit}
disabled={!text || connecting} disabled={!text || connecting}
loading={!connectingOpen && connecting} loading={!connectingOpen && connecting}
style={[styles.connectButton, { marginTop: verticalScale(16, height) }]} style={[styles.connectButton, { marginTop: verticalScale({ size: 16, height }) }]}
theme={theme} theme={theme}
testID='new-server-view-button' testID='new-server-view-button'
/> />
@ -377,8 +388,8 @@ class NewServerView extends React.Component {
styles.description, styles.description,
{ {
color: themes[theme].auxiliaryText, color: themes[theme].auxiliaryText,
fontSize: moderateScale(14, null, width), fontSize: moderateScale({ size: 14, width }),
marginBottom: verticalScale(16, height) marginBottom: verticalScale({ size: 16, height })
} }
]}> ]}>
{I18n.t('Onboarding_join_open_description')} {I18n.t('Onboarding_join_open_description')}
@ -400,14 +411,15 @@ class NewServerView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
connecting: state.server.connecting, connecting: state.server.connecting,
previousServer: state.server.previousServer previousServer: state.server.previousServer
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: Dispatch) => ({
connectServer: (...params) => dispatch(serverRequest(...params)), connectServer: (server: string, username: string & null, fromServerHistory?: boolean) =>
selectServer: server => dispatch(selectServerRequest(server)), dispatch(serverRequest(server, username, fromServerHistory)),
selectServer: (server: string) => dispatch(selectServerRequest(server)),
inviteLinksClear: () => dispatch(inviteLinksClearAction()), inviteLinksClear: () => dispatch(inviteLinksClearAction()),
serverFinishAdd: () => dispatch(serverFinishAddAction()) serverFinishAdd: () => dispatch(serverFinishAddAction())
}); });

View File

@ -1,13 +1,12 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FlatList, Text, View } from 'react-native'; import { FlatList, Text, View, RefreshControl } from 'react-native';
import { dequal } from 'dequal'; import { dequal } from 'dequal';
import moment from 'moment'; import moment from 'moment';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import * as List from '../../containers/List'; import * as List from '../../containers/List';
import Avatar from '../../containers/Avatar'; import Avatar from '../../containers/Avatar';
import ActivityIndicator from '../../containers/ActivityIndicator';
import * as HeaderButton from '../../containers/HeaderButton'; import * as HeaderButton from '../../containers/HeaderButton';
import I18n from '../../i18n'; import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
@ -85,12 +84,16 @@ class ReadReceiptView extends React.Component {
}; };
renderEmpty = () => { renderEmpty = () => {
const { loading } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (loading) {
return null;
}
return ( return (
<View <View
style={[styles.listEmptyContainer, { backgroundColor: themes[theme].chatComponentBackground }]} style={[styles.listEmptyContainer, { backgroundColor: themes[theme].chatComponentBackground }]}
testID='read-receipt-view'> testID='read-receipt-view'>
<Text style={{ color: themes[theme].titleText }}>{I18n.t('No_Read_Receipts')}</Text> <Text style={[styles.emptyText, { color: themes[theme].auxiliaryTintColor }]}>{I18n.t('No_Read_Receipts')}</Text>
</View> </View>
); );
}; };
@ -107,9 +110,15 @@ class ReadReceiptView extends React.Component {
<View style={styles.infoContainer}> <View style={styles.infoContainer}>
<View style={styles.item}> <View style={styles.item}>
<Text style={[styles.name, { color: themes[theme].titleText }]}>{item?.user?.name}</Text> <Text style={[styles.name, { color: themes[theme].titleText }]}>{item?.user?.name}</Text>
<Text style={{ color: themes[theme].auxiliaryText }}>{time}</Text> <Text style={[styles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
</View> </View>
<Text style={{ color: themes[theme].auxiliaryText }}>{`@${item.user.username}`}</Text> <Text
style={[
styles.username,
{
color: themes[theme].auxiliaryText
}
]}>{`@${item.user.username}`}</Text>
</View> </View>
</View> </View>
); );
@ -119,20 +128,15 @@ class ReadReceiptView extends React.Component {
const { receipts, loading } = this.state; const { receipts, loading } = this.state;
const { theme } = this.props; const { theme } = this.props;
if (!loading && receipts.length === 0) {
return this.renderEmpty();
}
return ( return (
<SafeAreaView testID='read-receipt-view'> <SafeAreaView testID='read-receipt-view'>
<StatusBar /> <StatusBar />
{loading ? (
<ActivityIndicator theme={theme} />
) : (
<FlatList <FlatList
data={receipts} data={receipts}
renderItem={this.renderItem} renderItem={this.renderItem}
ItemSeparatorComponent={List.Separator} ItemSeparatorComponent={List.Separator}
ListEmptyComponent={this.renderEmpty}
contentContainerStyle={List.styles.contentContainerStyleFlatList}
style={[ style={[
styles.list, styles.list,
{ {
@ -140,9 +144,9 @@ class ReadReceiptView extends React.Component {
borderColor: themes[theme].separatorColor borderColor: themes[theme].separatorColor
} }
]} ]}
refreshControl={<RefreshControl refreshing={loading} onRefresh={this.load} tintColor={themes[theme].auxiliaryText} />}
keyExtractor={item => item._id} keyExtractor={item => item._id}
/> />
)}
</SafeAreaView> </SafeAreaView>
); );
} }

View File

@ -8,9 +8,14 @@ export default StyleSheet.create({
alignItems: 'center', alignItems: 'center',
justifyContent: 'center' justifyContent: 'center'
}, },
emptyText: {
fontSize: 16,
...sharedStyles.textRegular
},
item: { item: {
flex: 1, flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between' justifyContent: 'space-between'
}, },
separator: { separator: {
@ -20,6 +25,14 @@ export default StyleSheet.create({
...sharedStyles.textRegular, ...sharedStyles.textRegular,
fontSize: 17 fontSize: 17
}, },
username: {
...sharedStyles.textMedium,
fontSize: 14
},
time: {
...sharedStyles.textRegular,
fontSize: 12
},
infoContainer: { infoContainer: {
flex: 1, flex: 1,
marginLeft: 10 marginLeft: 10

View File

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types';
import { Keyboard, StyleSheet, Text, View } from 'react-native'; import { Keyboard, StyleSheet, Text, View } from 'react-native';
import { StackNavigationProp } from '@react-navigation/stack';
import { RouteProp } from '@react-navigation/core';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import RNPickerSelect from 'react-native-picker-select'; import RNPickerSelect from 'react-native-picker-select';
@ -49,27 +50,37 @@ const styles = StyleSheet.create({
} }
}); });
class RegisterView extends React.Component { interface IProps {
static navigationOptions = ({ route, navigation }) => ({ navigation: StackNavigationProp<any>;
title: route.params?.title ?? 'Rocket.Chat', route: RouteProp<any, 'RegisterView'>;
server: string;
Site_Name: string;
Gitlab_URL: string;
CAS_enabled: boolean;
CAS_login_url: string;
Accounts_CustomFields: string;
Accounts_EmailVerification: boolean;
Accounts_ManuallyApproveNewUsers: boolean;
showLoginButton: boolean;
loginRequest: Function;
theme: string;
}
class RegisterView extends React.Component<IProps, any> {
private parsedCustomFields: any;
private usernameInput: any;
private passwordInput: any;
private emailInput: any;
private avatarUrl: any;
static navigationOptions = ({ route, navigation }: Partial<IProps>) => ({
title: route?.params?.title ?? 'Rocket.Chat',
headerRight: () => <HeaderButton.Legal testID='register-view-more' navigation={navigation} /> headerRight: () => <HeaderButton.Legal testID='register-view-more' navigation={navigation} />
}); });
static propTypes = { constructor(props: IProps) {
navigation: PropTypes.object,
server: PropTypes.string,
Accounts_CustomFields: PropTypes.string,
Accounts_EmailVerification: PropTypes.bool,
Accounts_ManuallyApproveNewUsers: PropTypes.bool,
theme: PropTypes.string,
Site_Name: PropTypes.string,
loginRequest: PropTypes.func,
showLoginButton: PropTypes.bool
};
constructor(props) {
super(props); super(props);
const customFields = {}; const customFields: any = {};
this.parsedCustomFields = {}; this.parsedCustomFields = {};
if (props.Accounts_CustomFields) { if (props.Accounts_CustomFields) {
try { try {
@ -78,7 +89,7 @@ class RegisterView extends React.Component {
log(e); log(e);
} }
} }
Object.keys(this.parsedCustomFields).forEach(key => { Object.keys(this.parsedCustomFields).forEach((key: string) => {
if (this.parsedCustomFields[key].defaultValue) { if (this.parsedCustomFields[key].defaultValue) {
customFields[key] = this.parsedCustomFields[key].defaultValue; customFields[key] = this.parsedCustomFields[key].defaultValue;
} }
@ -101,7 +112,7 @@ class RegisterView extends React.Component {
valid = () => { valid = () => {
const { name, email, password, username, customFields } = this.state; const { name, email, password, username, customFields } = this.state;
let requiredCheck = true; let requiredCheck = true;
Object.keys(this.parsedCustomFields).forEach(key => { Object.keys(this.parsedCustomFields).forEach((key: string) => {
if (this.parsedCustomFields[key].required) { if (this.parsedCustomFields[key].required) {
requiredCheck = requiredCheck && customFields[key] && Boolean(customFields[key].trim()); requiredCheck = requiredCheck && customFields[key] && Boolean(customFields[key].trim());
} }
@ -138,7 +149,7 @@ class RegisterView extends React.Component {
} else { } else {
await loginRequest({ user: email, password }); await loginRequest({ user: email, password });
} }
} catch (e) { } catch (e: any) {
if (e.data?.errorType === 'username-invalid') { if (e.data?.errorType === 'username-invalid') {
return loginRequest({ user: email, password }); return loginRequest({ user: email, password });
} }
@ -150,7 +161,7 @@ class RegisterView extends React.Component {
this.setState({ saving: false }); this.setState({ saving: false });
}; };
openContract = route => { openContract = (route: string) => {
const { server, theme } = this.props; const { server, theme } = this.props;
if (!server) { if (!server) {
return; return;
@ -167,19 +178,20 @@ class RegisterView extends React.Component {
try { try {
return Object.keys(this.parsedCustomFields).map((key, index, array) => { return Object.keys(this.parsedCustomFields).map((key, index, array) => {
if (this.parsedCustomFields[key].type === 'select') { if (this.parsedCustomFields[key].type === 'select') {
const options = this.parsedCustomFields[key].options.map(option => ({ label: option, value: option })); const options = this.parsedCustomFields[key].options.map((option: string) => ({ label: option, value: option }));
return ( return (
<RNPickerSelect <RNPickerSelect
key={key} key={key}
items={options} items={options}
onValueChange={value => { onValueChange={value => {
const newValue = {}; const newValue: { [key: string]: string | number } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
value={customFields[key]}> value={customFields[key]}>
<TextInput <TextInput
inputRef={e => { inputRef={(e: any) => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
placeholder={key} placeholder={key}
@ -193,20 +205,22 @@ class RegisterView extends React.Component {
return ( return (
<TextInput <TextInput
inputRef={e => { inputRef={(e: any) => {
// @ts-ignore
this[key] = e; this[key] = e;
}} }}
key={key} key={key}
label={key} label={key}
placeholder={key} placeholder={key}
value={customFields[key]} value={customFields[key]}
onChangeText={value => { onChangeText={(value: string) => {
const newValue = {}; const newValue: { [key: string]: string | number } = {};
newValue[key] = value; newValue[key] = value;
this.setState({ customFields: { ...customFields, ...newValue } }); this.setState({ customFields: { ...customFields, ...newValue } });
}} }}
onSubmitEditing={() => { onSubmitEditing={() => {
if (array.length - 1 > index) { if (array.length - 1 > index) {
// @ts-ignore
return this[array[index + 1]].focus(); return this[array[index + 1]].focus();
} }
this.avatarUrl.focus(); this.avatarUrl.focus();
@ -234,7 +248,7 @@ class RegisterView extends React.Component {
containerStyle={styles.inputContainer} containerStyle={styles.inputContainer}
placeholder={I18n.t('Name')} placeholder={I18n.t('Name')}
returnKeyType='next' returnKeyType='next'
onChangeText={name => this.setState({ name })} onChangeText={(name: string) => this.setState({ name })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.usernameInput.focus(); this.usernameInput.focus();
}} }}
@ -249,7 +263,7 @@ class RegisterView extends React.Component {
}} }}
placeholder={I18n.t('Username')} placeholder={I18n.t('Username')}
returnKeyType='next' returnKeyType='next'
onChangeText={username => this.setState({ username })} onChangeText={(username: string) => this.setState({ username })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.emailInput.focus(); this.emailInput.focus();
}} }}
@ -265,7 +279,7 @@ class RegisterView extends React.Component {
placeholder={I18n.t('Email')} placeholder={I18n.t('Email')}
returnKeyType='next' returnKeyType='next'
keyboardType='email-address' keyboardType='email-address'
onChangeText={email => this.setState({ email })} onChangeText={(email: string) => this.setState({ email })}
onSubmitEditing={() => { onSubmitEditing={() => {
this.passwordInput.focus(); this.passwordInput.focus();
}} }}
@ -281,7 +295,7 @@ class RegisterView extends React.Component {
placeholder={I18n.t('Password')} placeholder={I18n.t('Password')}
returnKeyType='send' returnKeyType='send'
secureTextEntry secureTextEntry
onChangeText={value => this.setState({ password: value })} onChangeText={(value: string) => this.setState({ password: value })}
onSubmitEditing={this.submit} onSubmitEditing={this.submit}
testID='register-view-password' testID='register-view-password'
theme={theme} theme={theme}
@ -334,7 +348,7 @@ class RegisterView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server.server, server: state.server.server,
Site_Name: state.settings.Site_Name, Site_Name: state.settings.Site_Name,
Gitlab_URL: state.settings.API_Gitlab_URL, Gitlab_URL: state.settings.API_Gitlab_URL,
@ -346,8 +360,8 @@ const mapStateToProps = state => ({
showLoginButton: getShowLoginButton(state) showLoginButton: getShowLoginButton(state)
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: any) => ({
loginRequest: params => dispatch(loginRequestAction(params)) loginRequest: (params: any) => dispatch(loginRequestAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RegisterView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(RegisterView));

View File

@ -14,6 +14,7 @@ import { animateNextTransition } from '../../../utils/layoutAnimation';
import ActivityIndicator from '../../../containers/ActivityIndicator'; import ActivityIndicator from '../../../containers/ActivityIndicator';
import { themes } from '../../../constants/colors'; import { themes } from '../../../constants/colors';
import debounce from '../../../utils/debounce'; import debounce from '../../../utils/debounce';
import { compareServerVersion, methods } from '../../../lib/utils';
import List from './List'; import List from './List';
import NavBottomFAB from './NavBottomFAB'; import NavBottomFAB from './NavBottomFAB';
@ -43,7 +44,8 @@ class ListContainer extends React.Component {
tunread: PropTypes.array, tunread: PropTypes.array,
ignored: PropTypes.array, ignored: PropTypes.array,
navigation: PropTypes.object, navigation: PropTypes.object,
showMessageInMainThread: PropTypes.bool showMessageInMainThread: PropTypes.bool,
serverVersion: PropTypes.string
}; };
constructor(props) { constructor(props) {
@ -131,7 +133,7 @@ class ListContainer extends React.Component {
query = async () => { query = async () => {
this.count += QUERY_SIZE; this.count += QUERY_SIZE;
const { rid, tmid, showMessageInMainThread } = this.props; const { rid, tmid, showMessageInMainThread, serverVersion } = this.props;
const db = database.active; const db = database.active;
// handle servers with version < 3.0.0 // handle servers with version < 3.0.0
@ -172,7 +174,14 @@ class ListContainer extends React.Component {
if (tmid && this.thread) { if (tmid && this.thread) {
messages = [...messages, this.thread]; messages = [...messages, this.thread];
} }
/**
* Since 3.16.0 server version, the backend don't response with messages if
* hide system message is enabled
*/
if (compareServerVersion(serverVersion, '3.16.0', methods.lowerThan) || hideSystemMessages.length) {
messages = messages.filter(m => !m.t || !hideSystemMessages?.includes(m.t)); messages = messages.filter(m => !m.t || !hideSystemMessages?.includes(m.t));
}
if (this.mounted) { if (this.mounted) {
this.setState({ messages }, () => this.update()); this.setState({ messages }, () => this.update());

View File

@ -120,6 +120,7 @@ class RoomView extends React.Component {
Message_Read_Receipt_Enabled: PropTypes.bool, Message_Read_Receipt_Enabled: PropTypes.bool,
Hide_System_Messages: PropTypes.array, Hide_System_Messages: PropTypes.array,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
serverVersion: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
isMasterDetail: PropTypes.bool, isMasterDetail: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string,
@ -1154,7 +1155,7 @@ class RoomView extends React.Component {
render() { render() {
console.count(`${this.constructor.name}.render calls`); console.count(`${this.constructor.name}.render calls`);
const { room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader } = this.state; const { room, reactionsModalVisible, selectedMessage, loading, reacting, showingBlockingLoader } = this.state;
const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height } = this.props; const { user, baseUrl, theme, navigation, Hide_System_Messages, width, height, serverVersion } = this.props;
const { rid, t, sysMes, bannerClosed, announcement } = room; const { rid, t, sysMes, bannerClosed, announcement } = room;
return ( return (
@ -1182,6 +1183,7 @@ class RoomView extends React.Component {
navigation={navigation} navigation={navigation}
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages} hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
showMessageInMainThread={user.showMessageInMainThread} showMessageInMainThread={user.showMessageInMainThread}
serverVersion={serverVersion}
/> />
{this.renderFooter()} {this.renderFooter()}
{this.renderActions()} {this.renderActions()}
@ -1220,6 +1222,7 @@ const mapStateToProps = state => ({
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
customEmojis: state.customEmojis, customEmojis: state.customEmojis,
baseUrl: state.server.server, baseUrl: state.server.server,
serverVersion: state.server.version,
Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled, Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled,
Hide_System_Messages: state.settings.Hide_System_Messages Hide_System_Messages: state.settings.Hide_System_Messages
}); });

View File

@ -613,6 +613,8 @@ class RoomsListView extends React.Component {
isRead = item => RocketChat.isRead(item); isRead = item => RocketChat.isRead(item);
isSwipeEnabled = item => !(item?.search || item?.joinCodeRequired || item?.outside);
getUserPresence = uid => RocketChat.getUserPresence(uid); getUserPresence = uid => RocketChat.getUserPresence(uid);
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room); getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
@ -928,6 +930,7 @@ class RoomsListView extends React.Component {
displayMode displayMode
} = this.props; } = this.props;
const id = this.getUidDirectMessage(item); const id = this.getUidDirectMessage(item);
const swipeEnabled = this.isSwipeEnabled(item);
return ( return (
<RoomItem <RoomItem
@ -950,6 +953,7 @@ class RoomsListView extends React.Component {
getIsRead={this.isRead} getIsRead={this.isRead}
visitor={item.visitor} visitor={item.visitor}
isFocused={currentItem?.rid === item.rid} isFocused={currentItem?.rid === item.rid}
swipeEnabled={swipeEnabled}
showAvatar={showAvatar} showAvatar={showAvatar}
displayMode={displayMode} displayMode={displayMode}
/> />

View File

@ -0,0 +1,94 @@
import React, { useEffect, useState } from 'react';
import { StackNavigationProp } from '@react-navigation/stack';
import TextInput from '../containers/TextInput';
import Button from '../containers/Button';
import { showErrorAlert } from '../utils/info';
import isValidEmail from '../utils/isValidEmail';
import I18n from '../i18n';
import RocketChat from '../lib/rocketchat';
import { useTheme } from '../theme';
import FormContainer, { FormContainerInner } from '../containers/FormContainer';
import log, { events, logEvent } from '../utils/log';
import sharedStyles from './Styles';
interface ISendEmailConfirmationView {
navigation: StackNavigationProp<any, 'SendEmailConfirmationView'>;
route: {
params: {
user?: string;
};
};
}
const SendEmailConfirmationView = ({ navigation, route }: ISendEmailConfirmationView): JSX.Element => {
const [email, setEmail] = useState('');
const [invalidEmail, setInvalidEmail] = useState(true);
const [isFetching, setIsFetching] = useState(false);
const { theme } = useTheme();
const validate = (val: string) => {
const isInvalidEmail = !isValidEmail(val);
setEmail(val);
setInvalidEmail(isInvalidEmail);
};
const resendConfirmationEmail = async () => {
logEvent(events.SEC_SEND_EMAIL_CONFIRMATION);
if (invalidEmail || !email) {
return;
}
try {
setIsFetching(true);
const result = await RocketChat.sendConfirmationEmail(email);
if (result.success) {
navigation.pop();
showErrorAlert(I18n.t('Verify_email_desc'));
}
} catch (e: any) {
log(e);
const msg = e?.data?.error || I18n.t('There_was_an_error_while_action', { action: I18n.t('sending_email_confirmation') });
showErrorAlert(msg, I18n.t('Alert'));
}
setIsFetching(false);
};
useEffect(() => {
navigation.setOptions({
title: 'Rocket.Chat'
});
if (route.params?.user) {
validate(route.params.user);
}
}, []);
return (
<FormContainer theme={theme} testID='send-email-confirmation-view'>
<FormContainerInner>
<TextInput
autoFocus
placeholder={I18n.t('Email')}
keyboardType='email-address'
returnKeyType='send'
onChangeText={(email: string) => validate(email)}
onSubmitEditing={resendConfirmationEmail}
testID='send-email-confirmation-view-email'
containerStyle={sharedStyles.inputLastChild}
theme={theme}
value={email}
/>
<Button
title={I18n.t('Send_email_confirmation')}
type='primary'
onPress={resendConfirmationEmail}
testID='send-email-confirmation-view-submit'
loading={isFetching}
disabled={invalidEmail}
theme={theme}
/>
</FormContainerInner>
</FormContainer>
);
};
export default SendEmailConfirmationView;

View File

@ -1,9 +1,9 @@
import React from 'react'; import React from 'react';
import { Clipboard, Linking, Share } from 'react-native'; import { Clipboard, Linking, Share } from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import FastImage from '@rocket.chat/react-native-fast-image'; import FastImage from '@rocket.chat/react-native-fast-image';
import CookieManager from '@react-native-cookies/cookies'; import CookieManager from '@react-native-cookies/cookies';
import { StackNavigationProp } from '@react-navigation/stack';
import { logout as logoutAction } from '../../actions/login'; import { logout as logoutAction } from '../../actions/login';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server'; import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
@ -29,8 +29,25 @@ import database from '../../lib/database';
import { isFDroidBuild } from '../../constants/environment'; import { isFDroidBuild } from '../../constants/environment';
import { getUserSelector } from '../../selectors/login'; import { getUserSelector } from '../../selectors/login';
class SettingsView extends React.Component { interface IProps {
static navigationOptions = ({ navigation, isMasterDetail }) => ({ navigation: StackNavigationProp<any, 'SettingsView'>;
server: {
version: string;
server: string;
};
theme: string;
isMasterDetail: boolean;
logout: Function;
selectServerRequest: Function;
user: {
roles: [];
id: string;
};
appStart: Function;
}
class SettingsView extends React.Component<IProps, any> {
static navigationOptions = ({ navigation, isMasterDetail }: Partial<IProps>) => ({
headerLeft: () => headerLeft: () =>
isMasterDetail ? ( isMasterDetail ? (
<HeaderButton.CloseModal navigation={navigation} testID='settings-view-close' /> <HeaderButton.CloseModal navigation={navigation} testID='settings-view-close' />
@ -40,26 +57,12 @@ class SettingsView extends React.Component {
title: I18n.t('Settings') title: I18n.t('Settings')
}); });
static propTypes = {
navigation: PropTypes.object,
server: PropTypes.object,
theme: PropTypes.string,
isMasterDetail: PropTypes.bool,
logout: PropTypes.func.isRequired,
selectServerRequest: PropTypes.func,
user: PropTypes.shape({
roles: PropTypes.array,
id: PropTypes.string
}),
appStart: PropTypes.func
};
checkCookiesAndLogout = async () => { checkCookiesAndLogout = async () => {
const { logout, user } = this.props; const { logout, user } = this.props;
const db = database.servers; const db = database.servers;
const usersCollection = db.get('users'); const usersCollection = db.get('users');
try { try {
const userRecord = await usersCollection.find(user.id); const userRecord: any = await usersCollection.find(user.id);
if (userRecord.isFromWebView) { if (userRecord.isFromWebView) {
showConfirmationAlert({ showConfirmationAlert({
title: I18n.t('Clear_cookies_alert'), title: I18n.t('Clear_cookies_alert'),
@ -84,6 +87,7 @@ class SettingsView extends React.Component {
handleLogout = () => { handleLogout = () => {
logEvent(events.SE_LOG_OUT); logEvent(events.SE_LOG_OUT);
// @ts-ignore
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('You_will_be_logged_out_of_this_application'), message: I18n.t('You_will_be_logged_out_of_this_application'),
confirmationText: I18n.t('Logout'), confirmationText: I18n.t('Logout'),
@ -93,6 +97,7 @@ class SettingsView extends React.Component {
handleClearCache = () => { handleClearCache = () => {
logEvent(events.SE_CLEAR_LOCAL_SERVER_CACHE); logEvent(events.SE_CLEAR_LOCAL_SERVER_CACHE);
/* @ts-ignore */
showConfirmationAlert({ showConfirmationAlert({
message: I18n.t('This_will_clear_all_your_offline_data'), message: I18n.t('This_will_clear_all_your_offline_data'),
confirmationText: I18n.t('Clear'), confirmationText: I18n.t('Clear'),
@ -112,7 +117,8 @@ class SettingsView extends React.Component {
}); });
}; };
navigateToScreen = screen => { navigateToScreen = (screen: string) => {
/* @ts-ignore */
logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]);
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate(screen); navigation.navigate(screen);
@ -160,7 +166,7 @@ class SettingsView extends React.Component {
this.saveToClipboard(getReadableVersion); this.saveToClipboard(getReadableVersion);
}; };
saveToClipboard = async content => { saveToClipboard = async (content: string) => {
await Clipboard.setString(content); await Clipboard.setString(content);
EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') }); EventEmitter.emit(LISTENER, { message: I18n.t('Copied_to_clipboard') });
}; };
@ -293,16 +299,16 @@ class SettingsView extends React.Component {
} }
} }
const mapStateToProps = state => ({ const mapStateToProps = (state: any) => ({
server: state.server, server: state.server,
user: getUserSelector(state), user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail isMasterDetail: state.app.isMasterDetail
}); });
const mapDispatchToProps = dispatch => ({ const mapDispatchToProps = (dispatch: any) => ({
logout: () => dispatch(logoutAction()), logout: () => dispatch(logoutAction()),
selectServerRequest: params => dispatch(selectServerRequestAction(params)), selectServerRequest: (params: any) => dispatch(selectServerRequestAction(params)),
appStart: params => dispatch(appStartAction(params)) appStart: (params: any) => dispatch(appStartAction(params))
}); });
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SettingsView)); export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SettingsView));

View File

@ -1,28 +1,45 @@
import React from 'react'; import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { events, logEvent } from '../../utils/log'; import log, { logEvent, events } from '../../utils/log';
import SafeAreaView from '../../containers/SafeAreaView'; import SafeAreaView from '../../containers/SafeAreaView';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import * as List from '../../containers/List'; import * as List from '../../containers/List';
import { SWITCH_TRACK_COLOR } from '../../constants/colors';
import { getUserSelector } from '../../selectors/login';
import RocketChat from '../../lib/rocketchat';
class UserPreferencesView extends React.Component { const UserPreferencesView = ({ navigation }) => {
static navigationOptions = () => ({ const user = useSelector(state => getUserSelector(state));
const [enableParser, setEnableParser] = useState(user.enableMessageParserEarlyAdoption);
useEffect(() => {
navigation.setOptions({
title: I18n.t('Preferences') title: I18n.t('Preferences')
}); });
}, []);
static propTypes = { const navigateToScreen = (screen, params) => {
navigation: PropTypes.object
};
navigateToScreen = (screen, params) => {
logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]); logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]);
const { navigation } = this.props;
navigation.navigate(screen, params); navigation.navigate(screen, params);
}; };
render() { const toggleMessageParser = async value => {
try {
await RocketChat.saveUserPreferences({ id: user.id, enableMessageParserEarlyAdoption: value });
setEnableParser(value);
} catch (e) {
log(e);
}
};
const renderMessageParserSwitch = () => (
<Switch value={enableParser} trackColor={SWITCH_TRACK_COLOR} onValueChange={toggleMessageParser} />
);
return ( return (
<SafeAreaView testID='preferences-view'> <SafeAreaView testID='preferences-view'>
<StatusBar /> <StatusBar />
@ -31,16 +48,28 @@ class UserPreferencesView extends React.Component {
<List.Separator /> <List.Separator />
<List.Item <List.Item
title='Notifications' title='Notifications'
onPress={() => this.navigateToScreen('UserNotificationPrefView')} onPress={() => navigateToScreen('UserNotificationPrefView')}
showActionIndicator showActionIndicator
testID='preferences-view-notifications' testID='preferences-view-notifications'
/> />
<List.Separator /> <List.Separator />
</List.Section> </List.Section>
<List.Section>
<List.Separator />
<List.Item
title='Enable_Message_Parser'
testID='preferences-view-enable-message-parser'
right={() => renderMessageParserSwitch()}
/>
<List.Separator />
</List.Section>
</List.Container> </List.Container>
</SafeAreaView> </SafeAreaView>
); );
} };
}
UserPreferencesView.propTypes = {
navigation: PropTypes.object
};
export default UserPreferencesView; export default UserPreferencesView;

View File

@ -4,36 +4,40 @@ PODS:
- React-Core - React-Core
- CocoaAsyncSocket (7.6.5) - CocoaAsyncSocket (7.6.5)
- DoubleConversion (1.1.6) - DoubleConversion (1.1.6)
- EXAppleAuthentication (2.2.1): - EXAppleAuthentication (3.2.1):
- UMCore - UMCore
- EXAV (8.2.1): - EXAV (9.2.3):
- ExpoModulesCore
- UMCore - UMCore
- UMFileSystemInterface - EXConstants (11.0.2):
- UMPermissionsInterface - ExpoModulesCore
- EXConstants (9.1.1):
- UMConstantsInterface
- UMCore - UMCore
- EXFileSystem (9.0.1): - EXFileSystem (11.1.3):
- ExpoModulesCore
- UMCore - UMCore
- UMFileSystemInterface - EXHaptics (10.1.0):
- EXHaptics (8.2.1):
- UMCore - UMCore
- EXImageLoader (1.1.1): - EXImageLoader (2.2.0):
- ExpoModulesCore
- React-Core - React-Core
- UMCore - UMCore
- UMImageLoaderInterface - EXKeepAwake (9.2.0):
- EXKeepAwake (8.2.1):
- UMCore - UMCore
- EXLocalAuthentication (9.2.0): - EXLocalAuthentication (11.1.1):
- UMConstantsInterface
- UMCore - UMCore
- EXPermissions (9.0.1): - ExpoModulesCore (0.2.0):
- ExpoModulesCore/Core (= 0.2.0)
- ExpoModulesCore/Interfaces (= 0.2.0)
- UMCore - UMCore
- UMPermissionsInterface - ExpoModulesCore/Core (0.2.0):
- EXVideoThumbnails (5.1.0):
- UMCore - UMCore
- UMFileSystemInterface - ExpoModulesCore/Interfaces (0.2.0):
- EXWebBrowser (8.3.1): - ExpoModulesCore/Core
- UMCore
- EXVideoThumbnails (5.2.1):
- ExpoModulesCore
- UMCore
- EXWebBrowser (9.2.0):
- UMCore - UMCore
- FBLazyVector (0.64.2) - FBLazyVector (0.64.2)
- FBReactNativeSpec (0.64.2): - FBReactNativeSpec (0.64.2):
@ -584,23 +588,14 @@ PODS:
- SDWebImage/Core (~> 5.5) - SDWebImage/Core (~> 5.5)
- simdjson (0.9.6-fix2) - simdjson (0.9.6-fix2)
- TOCropViewController (2.5.3) - TOCropViewController (2.5.3)
- UMAppLoader (1.2.0) - UMAppLoader (2.2.0)
- UMBarCodeScannerInterface (5.2.1) - UMCore (7.1.2)
- UMCameraInterface (5.2.1) - UMReactNativeAdapter (6.3.9):
- UMConstantsInterface (5.2.1) - ExpoModulesCore
- UMCore (5.3.0)
- UMFaceDetectorInterface (5.2.1)
- UMFileSystemInterface (5.2.1)
- UMFontInterface (5.2.1)
- UMImageLoaderInterface (5.2.1)
- UMPermissionsInterface (5.2.1):
- UMCore
- UMReactNativeAdapter (5.4.0):
- React-Core - React-Core
- UMCore - UMCore
- UMFontInterface - UMTaskManagerInterface (6.2.0):
- UMSensorsInterface (5.2.1) - UMCore
- UMTaskManagerInterface (5.2.1)
- WatermelonDB (0.23.0): - WatermelonDB (0.23.0):
- React - React
- React-jsi - React-jsi
@ -619,7 +614,7 @@ DEPENDENCIES:
- EXImageLoader (from `../node_modules/expo-image-loader/ios`) - EXImageLoader (from `../node_modules/expo-image-loader/ios`)
- EXKeepAwake (from `../node_modules/expo-keep-awake/ios`) - EXKeepAwake (from `../node_modules/expo-keep-awake/ios`)
- EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`) - EXLocalAuthentication (from `../node_modules/expo-local-authentication/ios`)
- EXPermissions (from `../node_modules/expo-permissions/ios`) - ExpoModulesCore (from `../node_modules/expo-modules-core/ios`)
- EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`) - EXVideoThumbnails (from `../node_modules/expo-video-thumbnails/ios`)
- EXWebBrowser (from `../node_modules/expo-web-browser/ios`) - EXWebBrowser (from `../node_modules/expo-web-browser/ios`)
- FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`) - FBLazyVector (from `../node_modules/react-native/Libraries/FBLazyVector`)
@ -714,17 +709,8 @@ DEPENDENCIES:
- RNVectorIcons (from `../node_modules/react-native-vector-icons`) - RNVectorIcons (from `../node_modules/react-native-vector-icons`)
- "simdjson (from `../node_modules/@nozbe/simdjson`)" - "simdjson (from `../node_modules/@nozbe/simdjson`)"
- UMAppLoader (from `../node_modules/unimodules-app-loader/ios`) - UMAppLoader (from `../node_modules/unimodules-app-loader/ios`)
- UMBarCodeScannerInterface (from `../node_modules/unimodules-barcode-scanner-interface/ios`)
- UMCameraInterface (from `../node_modules/unimodules-camera-interface/ios`)
- UMConstantsInterface (from `../node_modules/unimodules-constants-interface/ios`)
- "UMCore (from `../node_modules/@unimodules/core/ios`)" - "UMCore (from `../node_modules/@unimodules/core/ios`)"
- UMFaceDetectorInterface (from `../node_modules/unimodules-face-detector-interface/ios`)
- UMFileSystemInterface (from `../node_modules/unimodules-file-system-interface/ios`)
- UMFontInterface (from `../node_modules/unimodules-font-interface/ios`)
- UMImageLoaderInterface (from `../node_modules/unimodules-image-loader-interface/ios`)
- UMPermissionsInterface (from `../node_modules/unimodules-permissions-interface/ios`)
- "UMReactNativeAdapter (from `../node_modules/@unimodules/react-native-adapter/ios`)" - "UMReactNativeAdapter (from `../node_modules/@unimodules/react-native-adapter/ios`)"
- UMSensorsInterface (from `../node_modules/unimodules-sensors-interface/ios`)
- UMTaskManagerInterface (from `../node_modules/unimodules-task-manager-interface/ios`) - UMTaskManagerInterface (from `../node_modules/unimodules-task-manager-interface/ios`)
- "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)" - "WatermelonDB (from `../node_modules/@nozbe/watermelondb`)"
- Yoga (from `../node_modules/react-native/ReactCommon/yoga`) - Yoga (from `../node_modules/react-native/ReactCommon/yoga`)
@ -784,8 +770,8 @@ EXTERNAL SOURCES:
:path: "../node_modules/expo-keep-awake/ios" :path: "../node_modules/expo-keep-awake/ios"
EXLocalAuthentication: EXLocalAuthentication:
:path: "../node_modules/expo-local-authentication/ios" :path: "../node_modules/expo-local-authentication/ios"
EXPermissions: ExpoModulesCore:
:path: "../node_modules/expo-permissions/ios" :path: "../node_modules/expo-modules-core/ios"
EXVideoThumbnails: EXVideoThumbnails:
:path: "../node_modules/expo-video-thumbnails/ios" :path: "../node_modules/expo-video-thumbnails/ios"
EXWebBrowser: EXWebBrowser:
@ -926,28 +912,10 @@ EXTERNAL SOURCES:
:path: "../node_modules/@nozbe/simdjson" :path: "../node_modules/@nozbe/simdjson"
UMAppLoader: UMAppLoader:
:path: "../node_modules/unimodules-app-loader/ios" :path: "../node_modules/unimodules-app-loader/ios"
UMBarCodeScannerInterface:
:path: "../node_modules/unimodules-barcode-scanner-interface/ios"
UMCameraInterface:
:path: "../node_modules/unimodules-camera-interface/ios"
UMConstantsInterface:
:path: "../node_modules/unimodules-constants-interface/ios"
UMCore: UMCore:
:path: "../node_modules/@unimodules/core/ios" :path: "../node_modules/@unimodules/core/ios"
UMFaceDetectorInterface:
:path: "../node_modules/unimodules-face-detector-interface/ios"
UMFileSystemInterface:
:path: "../node_modules/unimodules-file-system-interface/ios"
UMFontInterface:
:path: "../node_modules/unimodules-font-interface/ios"
UMImageLoaderInterface:
:path: "../node_modules/unimodules-image-loader-interface/ios"
UMPermissionsInterface:
:path: "../node_modules/unimodules-permissions-interface/ios"
UMReactNativeAdapter: UMReactNativeAdapter:
:path: "../node_modules/@unimodules/react-native-adapter/ios" :path: "../node_modules/@unimodules/react-native-adapter/ios"
UMSensorsInterface:
:path: "../node_modules/unimodules-sensors-interface/ios"
UMTaskManagerInterface: UMTaskManagerInterface:
:path: "../node_modules/unimodules-task-manager-interface/ios" :path: "../node_modules/unimodules-task-manager-interface/ios"
WatermelonDB: WatermelonDB:
@ -965,17 +933,17 @@ SPEC CHECKSUMS:
BugsnagReactNative: a97b3132c1854fd7bf92350fabd505e3ebdd7829 BugsnagReactNative: a97b3132c1854fd7bf92350fabd505e3ebdd7829
CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99
DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de DoubleConversion: cf9b38bf0b2d048436d9a82ad2abe1404f11e7de
EXAppleAuthentication: 5b3da71bada29e2423d8ea27e5538ef0d75aba62 EXAppleAuthentication: e8c537fcbe80670dd76fde7a07acb94af70ada00
EXAV: 86344030966e0da7e00556fbb97269d9ad16071d EXAV: 67bcc1d0afeb1fab854b206c84b9f2afbd61d0cd
EXConstants: f907b3b6ce16e20d1750f22af1e095e924574bcb EXConstants: 4cb52b6d8f636c767104a44bf7db3873e9c01a6f
EXFileSystem: 76875135b61708b9afa7e6a89b72a60ba0fdfa20 EXFileSystem: 0a04aba8da751b9ac954065911bcf166503f8267
EXHaptics: 5428b344a216ca5d9df6ca8f65720b2a1ad9f109 EXHaptics: 6dc4307ab0794fe7a87ec8d7d1c299cf103d6cb3
EXImageLoader: 02ca02c9cd5cc8a97b423207a73a791e0a86bea5 EXImageLoader: d3531a3fe530b22925c19977cb53bb43e3821fe6
EXKeepAwake: 8b0f68242f036b971f9f8976341823cbe6f50812 EXKeepAwake: f4105ef469be7b283f66ce2d7234bb71ac80cd26
EXLocalAuthentication: 985c65e08a6eb84f8f98b51f7435df138b18b9e8 EXLocalAuthentication: 88a1a69ea66c4934387d1eb503628170c853caef
EXPermissions: 80ac3acbdb145930079810fe5b08c022b3428aa8 ExpoModulesCore: 2734852616127a6c1fc23012197890a6f3763dc7
EXVideoThumbnails: cd257fc6e07884a704a5674d362a6410933acb68 EXVideoThumbnails: 442c3abadb51a81551a3b53705b7560de390e6f7
EXWebBrowser: d37a5ffdea1b65947352bc001dd9f732463725d4 EXWebBrowser: 76783ba5dcb8699237746ecf41a9643d428a4cc5
FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b FBLazyVector: e686045572151edef46010a6f819ade377dfeb4b
FBReactNativeSpec: 110d69378fce79af38271c39894b59fec7890221 FBReactNativeSpec: 110d69378fce79af38271c39894b59fec7890221
Firebase: 919186c8e119dd9372a45fd1dd17a8a942bc1892 Firebase: 919186c8e119dd9372a45fd1dd17a8a942bc1892
@ -1071,19 +1039,10 @@ SPEC CHECKSUMS:
SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8 SDWebImageWebPCoder: 36f8f47bd9879a8aea6044765c1351120fd8e3a8
simdjson: 85016870cd17207312b718ef6652eb6a1cd6a2b0 simdjson: 85016870cd17207312b718ef6652eb6a1cd6a2b0
TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6 TOCropViewController: 20a14b6a7a098308bf369e7c8d700dc983a974e6
UMAppLoader: 61049c8d55590b74e9ae1d5429bf68d96b4a2528 UMAppLoader: 21af63390e55c82e037fb9752d93114a80ecf16e
UMBarCodeScannerInterface: e5e4c87797d3d01214e25cd1618866caf5d4f17f UMCore: ce3a4faa010239063b8343895b29a6d97b01069d
UMCameraInterface: 415ac060034edecacdbbaa739c223e3f276e0056 UMReactNativeAdapter: d03cefd0e4e4179ab8c490408589f1c8a6c8b785
UMConstantsInterface: 1a52f2d884c95e8829439da13e36b7669a1a8fb4 UMTaskManagerInterface: 2be431101b73604e64fbfffcf759336f9d8fccbb
UMCore: d98083b522b08c0a8ba3992bc263c624ae5d887c
UMFaceDetectorInterface: 67c6c82451338da01a4bc00ec46365a2a8ea9057
UMFileSystemInterface: 303d696ede28102a7e11d111808bd2ed2c5eb62f
UMFontInterface: 6edf1ee8bc55d2030766f8cf0a7b20a5d5a913b0
UMImageLoaderInterface: 9cdbf3bab6a513bddd88505cb2340fe02d6a11c0
UMPermissionsInterface: 019170ad655f464e3f8d23d2a8bcbda2e645cde4
UMReactNativeAdapter: 538efe92e781b5d7678cf95b34c46f2d0989a557
UMSensorsInterface: cb5bf31d52c4349f0ff9e3c049bbe4df0d80d383
UMTaskManagerInterface: 80653f25c55d9e6d79d6a0a65589fa213feaee11
WatermelonDB: 577c61fceff16e9f9103b59d14aee4850c0307b6 WatermelonDB: 577c61fceff16e9f9103b59d14aee4850c0307b6
Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac Yoga: 575c581c63e0d35c9a83f4b46d01d63abc1100ac
YogaKit: f782866e155069a2cca2517aafea43200b01fd5a YogaKit: f782866e155069a2cca2517aafea43200b01fd5a

View File

@ -47,23 +47,25 @@
"@react-navigation/drawer": "5.12.5", "@react-navigation/drawer": "5.12.5",
"@react-navigation/native": "5.9.4", "@react-navigation/native": "5.9.4",
"@react-navigation/stack": "5.14.5", "@react-navigation/stack": "5.14.5",
"@rocket.chat/message-parser": "0.30.0",
"@rocket.chat/react-native-fast-image": "^8.2.0", "@rocket.chat/react-native-fast-image": "^8.2.0",
"@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile", "@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile",
"@rocket.chat/ui-kit": "0.13.0", "@rocket.chat/ui-kit": "0.13.0",
"@types/url-parse": "^1.4.4",
"bytebuffer": "^5.0.1", "bytebuffer": "^5.0.1",
"color2k": "1.2.4", "color2k": "1.2.4",
"commonmark": "git+https://github.com/RocketChat/commonmark.js.git", "commonmark": "git+https://github.com/RocketChat/commonmark.js.git",
"commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git", "commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git",
"dequal": "^2.0.2", "dequal": "^2.0.2",
"ejson": "2.2.1", "ejson": "2.2.1",
"expo-apple-authentication": "^2.2.1", "expo-apple-authentication": "3.2.1",
"expo-av": "8.2.1", "expo-av": "9.2.3",
"expo-file-system": "9.0.1", "expo-file-system": "11.1.3",
"expo-haptics": "8.2.1", "expo-haptics": "10.1.0",
"expo-keep-awake": "8.2.1", "expo-keep-awake": "9.2.0",
"expo-local-authentication": "9.2.0", "expo-local-authentication": "11.1.1",
"expo-video-thumbnails": "5.1.0", "expo-video-thumbnails": "5.2.1",
"expo-web-browser": "8.3.1", "expo-web-browser": "9.2.0",
"hoist-non-react-statics": "3.3.2", "hoist-non-react-statics": "3.3.2",
"i18n-js": "3.8.0", "i18n-js": "3.8.0",
"js-base64": "3.6.1", "js-base64": "3.6.1",
@ -112,7 +114,7 @@
"react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.0", "react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.0",
"react-native-slowlog": "^1.0.2", "react-native-slowlog": "^1.0.2",
"react-native-ui-lib": "RocketChat/react-native-ui-lib#minor-improvements", "react-native-ui-lib": "RocketChat/react-native-ui-lib#minor-improvements",
"react-native-unimodules": "0.10.1", "react-native-unimodules": "^0.14.8",
"react-native-vector-icons": "8.1.0", "react-native-vector-icons": "8.1.0",
"react-native-webview": "10.3.2", "react-native-webview": "10.3.2",
"react-redux": "7.2.4", "react-redux": "7.2.4",

View File

@ -0,0 +1,627 @@
/* eslint-disable import/no-extraneous-dependencies */
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { storiesOf } from '@storybook/react-native';
import NewMarkdown from '../../app/containers/markdown/new';
import { themes } from '../../app/constants/colors';
const stories = storiesOf('NewMarkdown', module);
const theme = 'light';
const styles = StyleSheet.create({
container: {
marginHorizontal: 15,
backgroundColor: themes[theme].backgroundColor,
marginVertical: 50
},
separator: {
marginHorizontal: 10,
marginVertical: 10
}
});
const getCustomEmoji = content => {
const customEmoji = {
marioparty: { name: content, extension: 'gif' },
nyan_rocket: { name: content, extension: 'png' }
}[content];
return customEmoji;
};
const baseUrl = 'https://open.rocket.chat';
const simpleTextMsg = [
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'This is Rocket.Chat'
}
]
}
];
const longTextMsg = [
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value:
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
}
]
}
];
const lineBreakMsg = [
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'a'
},
{
type: 'PLAIN_TEXT',
value: 'b'
},
{
type: 'PLAIN_TEXT',
value: 'c'
},
{
type: 'PLAIN_TEXT',
value: ''
},
{
type: 'PLAIN_TEXT',
value: 'd'
},
{
type: 'PLAIN_TEXT',
value: ''
},
{
type: 'PLAIN_TEXT',
value: ''
},
{
type: 'PLAIN_TEXT',
value: 'e'
}
]
}
];
const sequentialEmptySpacesMsg = [
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'a b c'
}
]
}
];
const boldOrUnderscoreMsg = [
{
type: 'PARAGRAPH',
value: [
{
type: 'BOLD',
value: [
{
type: 'PLAIN_TEXT',
value: 'This is bold'
}
]
},
{
type: 'PLAIN_TEXT',
value: ' and '
},
{
type: 'ITALIC',
value: [
{
type: 'PLAIN_TEXT',
value: 'this is italic'
}
]
}
]
}
];
stories.add('Text', () => (
<View style={styles.container}>
<NewMarkdown tokens={simpleTextMsg} />
<NewMarkdown tokens={longTextMsg} />
<NewMarkdown tokens={lineBreakMsg} />
<NewMarkdown tokens={sequentialEmptySpacesMsg} />
<NewMarkdown tokens={boldOrUnderscoreMsg} />
</View>
));
const allMentionTokens = [
{
type: 'PARAGRAPH',
value: [
{
type: 'MENTION_USER',
value: {
type: 'PLAIN_TEXT',
value: 'rocket.cat'
}
}
]
}
];
const multipleMentionTokens = [
{
type: 'PARAGRAPH',
value: [
{
type: 'MENTION_USER',
value: {
type: 'PLAIN_TEXT',
value: 'name'
}
},
{
type: 'PLAIN_TEXT',
value: ' '
},
{
type: 'MENTION_USER',
value: {
type: 'PLAIN_TEXT',
value: 'rocket.cat'
}
},
{
type: 'PLAIN_TEXT',
value: ' '
},
{
type: 'MENTION_USER',
value: {
type: 'PLAIN_TEXT',
value: 'here'
}
},
{
type: 'PLAIN_TEXT',
value: ' '
},
{
type: 'MENTION_USER',
value: {
type: 'PLAIN_TEXT',
value: 'all'
}
}
]
}
];
const allMentions = [
{
_id: 'rocket.cat',
username: 'rocket.cat'
}
];
const multipleMentions = [
{
_id: 'name',
username: 'name'
},
{
_id: 'rocket.cat',
username: 'rocket.cat'
},
{
_id: 'here',
username: 'here'
},
{
_id: 'all',
username: 'all'
}
];
stories.add('Mentions', () => (
<View style={styles.container}>
<NewMarkdown tokens={allMentionTokens} mentions={allMentions} navToRoomInfo={() => {}} style={[]} />
<NewMarkdown
tokens={multipleMentionTokens}
mentions={multipleMentions}
navToRoomInfo={() => {}}
style={[]}
username='rocket.cat'
/>
</View>
));
const channelTokens = [
{
type: 'PARAGRAPH',
value: [
{
type: 'MENTION_CHANNEL',
value: {
type: 'PLAIN_TEXT',
value: 'text_channel'
}
},
{
type: 'PLAIN_TEXT',
value: ' and '
},
{
type: 'MENTION_CHANNEL',
value: {
type: 'PLAIN_TEXT',
value: 'not_a_channel'
}
}
]
}
];
const channelMention = [
{
_id: 'text_channel',
name: 'text_channel'
}
];
stories.add('Hashtag', () => (
<View style={styles.container}>
<NewMarkdown tokens={channelTokens} channels={channelMention} navToRoomInfo={() => {}} />
</View>
));
const bigEmojiTokens = [
{
type: 'BIG_EMOJI',
value: [
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'green_heart'
}
},
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'joy'
}
},
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'grin'
}
}
]
}
];
const emojiTokens = [
{
type: 'PARAGRAPH',
value: [
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'rocket'
}
},
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'facepalm'
}
},
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'nyan_rocket'
}
},
{
type: 'EMOJI',
value: {
type: 'PLAIN_TEXT',
value: 'marioparty'
}
}
]
}
];
stories.add('Emoji', () => (
<View style={styles.container}>
<NewMarkdown tokens={bigEmojiTokens} />
<NewMarkdown tokens={emojiTokens} getCustomEmoji={getCustomEmoji} baseUrl={baseUrl} />
</View>
));
const blockQuoteTokens = [
{
type: 'QUOTE',
value: [
{
type: 'PARAGRAPH',
value: [
{
type: 'PLAIN_TEXT',
value: 'Rocket.Chat to the moon'
}
]
}
]
}
];
stories.add('Block quote', () => (
<View style={styles.container}>
<NewMarkdown tokens={blockQuoteTokens} />
</View>
));
const rocketChatLink = [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
},
label: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
}
}
}
]
}
];
const markdownLink = [
{
type: 'PARAGRAPH',
value: [
{
type: 'LINK',
value: {
src: {
type: 'PLAIN_TEXT',
value: 'https://rocket.chat'
},
label: {
type: 'PLAIN_TEXT',
value: 'Markdown link'
}
}
}
]
}
];
stories.add('Links', () => (
<View style={styles.container}>
<NewMarkdown tokens={rocketChatLink} />
<NewMarkdown tokens={markdownLink} />
</View>
));
stories.add('Headers', () => (
<View style={styles.container}>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '# Header 1'
}
],
level: 1
}
]}
/>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '## Header 2'
}
],
level: 2
}
]}
/>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '### Header 3'
}
],
level: 3
}
]}
/>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '#### Header 4'
}
],
level: 4
}
]}
/>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '##### Header 5'
}
],
level: 5
}
]}
/>
<NewMarkdown
tokens={[
{
type: 'HEADING',
value: [
{
type: 'PLAIN_TEXT',
value: '###### Header 6'
}
],
level: 6
}
]}
/>
</View>
));
const inlineCodeToken = [
{
type: 'PARAGRAPH',
value: [
{
type: 'INLINE_CODE',
value: {
type: 'PLAIN_TEXT',
value: 'inline code'
}
}
]
}
];
const multilineCodeToken = [
{
type: 'CODE',
language: 'none',
value: [
{
type: 'CODE_LINE',
value: {
type: 'PLAIN_TEXT',
value: 'Multi line '
}
},
{
type: 'CODE_LINE',
value: {
type: 'PLAIN_TEXT',
value: 'Code'
}
}
]
}
];
stories.add('Code', () => (
<View style={styles.container}>
<NewMarkdown tokens={inlineCodeToken} style={[]} />
<NewMarkdown tokens={multilineCodeToken} style={[]} />
</View>
));
const unorederedListToken = [
{
type: 'UNORDERED_LIST',
value: [
{
type: 'LIST_ITEM',
value: [
{
type: 'PLAIN_TEXT',
value: 'Open Source'
}
]
},
{
type: 'LIST_ITEM',
value: [
{
type: 'PLAIN_TEXT',
value: 'Rocket.Chat'
}
]
}
]
}
];
const orderedListToken = [
{
type: 'ORDERED_LIST',
value: [
{
type: 'LIST_ITEM',
value: [
{
type: 'PLAIN_TEXT',
value: 'Open Source'
}
]
},
{
type: 'LIST_ITEM',
value: [
{
type: 'PLAIN_TEXT',
value: 'Rocket.Chat'
}
]
}
]
}
];
stories.add('Lists', () => (
<View style={styles.container}>
<NewMarkdown tokens={unorederedListToken} />
<NewMarkdown tokens={orderedListToken} />
</View>
));

View File

@ -12,6 +12,7 @@ import './HeaderButtons';
import './UnreadBadge'; import './UnreadBadge';
import '../../app/views/ThreadMessagesView/Item.stories.js'; import '../../app/views/ThreadMessagesView/Item.stories.js';
import './Avatar'; import './Avatar';
import './NewMarkdown';
import '../../app/containers/BackgroundContainer/index.stories.js'; import '../../app/containers/BackgroundContainer/index.stories.js';
import '../../app/containers/RoomHeader/RoomHeader.stories.js'; import '../../app/containers/RoomHeader/RoomHeader.stories.js';
import '../../app/views/RoomView/LoadMore/LoadMore.stories'; import '../../app/views/RoomView/LoadMore/LoadMore.stories';

1008
yarn.lock

File diff suppressed because it is too large Load Diff