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.keepawake.KeepAwakePackage(),
new expo.modules.localauthentication.LocalAuthenticationPackage(),
new expo.modules.permissions.PermissionsPackage(),
new expo.modules.videothumbnails.VideoThumbnailsPackage(),
new expo.modules.webbrowser.WebBrowserPackage()
);

View File

@ -1,5 +1,5 @@
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 sharedStyles from '../views/Styles';
@ -10,10 +10,10 @@ import AppVersion from './AppVersion';
import { isTablet } from '../utils/deviceInfo';
import SafeAreaView from './SafeAreaView';
interface IFormContainer {
interface IFormContainer extends ScrollViewProps {
theme: string;
testID: string;
children: JSX.Element;
children: React.ReactNode;
}
const styles = StyleSheet.create({

View File

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

View File

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

View File

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

View File

@ -3,6 +3,7 @@ import { Image, Text } from 'react-native';
import { Node, Parser } from 'commonmark';
import Renderer from 'commonmark-react-renderer';
import removeMarkdown from 'remove-markdown';
import { MarkdownAST } from '@rocket.chat/message-parser';
import shortnameToUnicode from '../../utils/shortnameToUnicode';
import I18n from '../../i18n';
@ -20,9 +21,20 @@ import MarkdownTableCell from './TableCell';
import mergeTextNodes from './mergeTextNodes';
import styles from './styles';
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 {
msg: string;
md: MarkdownAST;
mentions: UserMention[];
getCustomEmoji: Function;
baseUrl: string;
username: string;
@ -35,7 +47,7 @@ interface IMarkdownProps {
name: string;
_id: number;
}[];
mentions: object[];
enableMessageParser: boolean;
navToRoomInfo: Function;
preview: boolean;
theme: string;
@ -97,7 +109,9 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
constructor(props: IMarkdownProps) {
super(props);
this.renderer = this.createRenderer();
if (!this.isNewMarkdown) {
this.renderer = this.createRenderer();
}
}
createRenderer = () =>
@ -139,6 +153,11 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
renderParagraphsInLists: true
});
get isNewMarkdown(): boolean {
const { md, enableMessageParser } = this.props;
return enableMessageParser && !!md;
}
editedMessage = (ast: any) => {
const { isEdited } = this.props;
if (isEdited) {
@ -227,12 +246,12 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
};
renderHashtag = ({ hashtag }: { hashtag: string }) => {
const { channels, navToRoomInfo, style, theme } = this.props;
return <MarkdownHashtag hashtag={hashtag} channels={channels} navToRoomInfo={navToRoomInfo} theme={theme} style={style} />;
const { channels, navToRoomInfo, style } = this.props;
return <MarkdownHashtag hashtag={hashtag} channels={channels} navToRoomInfo={navToRoomInfo} style={style} />;
};
renderAtMention = ({ mentionName }: { mentionName: string }) => {
const { username, mentions, navToRoomInfo, useRealName, style, theme } = this.props;
const { username, mentions, navToRoomInfo, useRealName, style } = this.props;
return (
<MarkdownAtMention
mentions={mentions}
@ -240,7 +259,6 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
useRealName={useRealName}
username={username}
navToRoomInfo={navToRoomInfo}
theme={theme}
style={style}
/>
);
@ -329,12 +347,44 @@ class Markdown extends PureComponent<IMarkdownProps, any> {
};
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) {
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);
// 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: {
textDecorationLine: 'line-through'
},
plainText: {
fontSize: 16,
flexShrink: 1
},
text: {
fontSize: 16,
...sharedStyles.textRegular
@ -70,12 +74,16 @@ export default StyleSheet.create<any>({
resizeMode: 'contain'
},
codeInline: {
fontSize: 16,
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,
borderRadius: 4
borderRadius: 4,
paddingLeft: 2,
paddingTop: 2
},
codeBlock: {
fontSize: 16,
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -679,5 +679,7 @@
"Use": "Use",
"Shortcut": "Atalho",
"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('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('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 }]
})
]
},
{
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';
export default appSchema({
version: 13,
version: 14,
tables: [
tableSchema({
name: 'subscriptions',
@ -115,7 +115,8 @@ export default appSchema({
{ name: 'tmsg', type: 'string', isOptional: true },
{ name: 'blocks', 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({

View File

@ -1,7 +1,7 @@
import { appSchema, tableSchema } from '@nozbe/watermelondb';
export default appSchema({
version: 11,
version: 12,
tables: [
tableSchema({
name: 'users',
@ -16,7 +16,8 @@ export default appSchema({
{ name: 'login_email_password', type: 'boolean', isOptional: true },
{ name: 'show_message_in_main_thread', type: 'boolean', 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({

View File

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

View File

@ -1,5 +1,5 @@
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';
@ -10,7 +10,7 @@ const styles = StyleSheet.create({
});
interface IThemedTextInput extends TextInputProps {
style: object;
style: StyleProp<TextStyle>;
theme: string;
}

View File

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

View File

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

View File

@ -3,14 +3,14 @@ import hoistNonReactStatics from 'hoist-non-react-statics';
interface IThemeContextProps {
theme: string;
themePreferences: {
themePreferences?: {
currentTheme: 'automatic' | 'light';
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 {
const ThemedComponent = (props: any) => (

View File

@ -11,6 +11,9 @@ export default {
FP_FORGOT_PASSWORD: 'fp_forgot_password',
FP_FORGOT_PASSWORD_F: 'fp_forgot_password_f',
// SEND EMAIL CONFIRMATION VIEW
SEC_SEND_EMAIL_CONFIRMATION: 'sec_send_email_confirmation',
// REGISTER VIEW
REGISTER_DEFAULT_SIGN_UP: 'register_default_sign_up',
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 PropTypes from 'prop-types';
import { WebView } from 'react-native-webview';
import { WebView, WebViewNavigation } from 'react-native-webview';
import { connect } from 'react-redux';
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 { isIOS } from '../utils/deviceInfo';
@ -40,17 +41,44 @@ window.addEventListener('popstate', function() {
});
`;
class AuthenticationWebView extends React.PureComponent {
static propTypes = {
navigation: PropTypes.object,
route: PropTypes.object,
server: PropTypes.string,
Accounts_Iframe_api_url: PropTypes.bool,
Accounts_Iframe_api_method: PropTypes.bool,
theme: PropTypes.string
interface IRoute {
params: {
authType: string;
url: string;
ssoToken?: 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);
this.state = {
logging: false,
@ -71,7 +99,7 @@ class AuthenticationWebView extends React.PureComponent {
navigation.pop();
};
login = params => {
login = (params: any) => {
const { logging } = this.state;
if (logging) {
return;
@ -89,7 +117,7 @@ class AuthenticationWebView extends React.PureComponent {
};
// 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(
async () => {
@ -104,7 +132,7 @@ class AuthenticationWebView extends React.PureComponent {
true
);
onNavigationStateChange = webViewState => {
onNavigationStateChange = (webViewState: WebViewNavigation | WebViewMessage) => {
const url = decodeURIComponent(webViewState.url);
const { route } = this.props;
const { authType } = route.params;
@ -180,18 +208,10 @@ class AuthenticationWebView extends React.PureComponent {
}
}
const mapStateToProps = state => ({
const mapStateToProps = (state: any) => ({
server: state.server.server,
Accounts_Iframe_api_url: state.settings.Accounts_Iframe_api_url,
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));

View File

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

View File

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

View File

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

View File

@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import PropTypes from 'prop-types';
import TextInput from '../../../containers/TextInput';
import * as List from '../../../containers/List';
import { themes } from '../../../constants/colors';
import I18n from '../../../i18n';
import Item from './Item';
import { IServer } from '../index';
const styles = StyleSheet.create({
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);
return (
<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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -120,6 +120,7 @@ class RoomView extends React.Component {
Message_Read_Receipt_Enabled: PropTypes.bool,
Hide_System_Messages: PropTypes.array,
baseUrl: PropTypes.string,
serverVersion: PropTypes.string,
customEmojis: PropTypes.object,
isMasterDetail: PropTypes.bool,
theme: PropTypes.string,
@ -1154,7 +1155,7 @@ class RoomView extends React.Component {
render() {
console.count(`${this.constructor.name}.render calls`);
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;
return (
@ -1182,6 +1183,7 @@ class RoomView extends React.Component {
navigation={navigation}
hideSystemMessages={Array.isArray(sysMes) ? sysMes : Hide_System_Messages}
showMessageInMainThread={user.showMessageInMainThread}
serverVersion={serverVersion}
/>
{this.renderFooter()}
{this.renderActions()}
@ -1220,6 +1222,7 @@ const mapStateToProps = state => ({
Message_TimeFormat: state.settings.Message_TimeFormat,
customEmojis: state.customEmojis,
baseUrl: state.server.server,
serverVersion: state.server.version,
Message_Read_Receipt_Enabled: state.settings.Message_Read_Receipt_Enabled,
Hide_System_Messages: state.settings.Hide_System_Messages
});

View File

@ -613,6 +613,8 @@ class RoomsListView extends React.Component {
isRead = item => RocketChat.isRead(item);
isSwipeEnabled = item => !(item?.search || item?.joinCodeRequired || item?.outside);
getUserPresence = uid => RocketChat.getUserPresence(uid);
getUidDirectMessage = room => RocketChat.getUidDirectMessage(room);
@ -928,6 +930,7 @@ class RoomsListView extends React.Component {
displayMode
} = this.props;
const id = this.getUidDirectMessage(item);
const swipeEnabled = this.isSwipeEnabled(item);
return (
<RoomItem
@ -950,6 +953,7 @@ class RoomsListView extends React.Component {
getIsRead={this.isRead}
visitor={item.visitor}
isFocused={currentItem?.rid === item.rid}
swipeEnabled={swipeEnabled}
showAvatar={showAvatar}
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 { Clipboard, Linking, Share } from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import FastImage from '@rocket.chat/react-native-fast-image';
import CookieManager from '@react-native-cookies/cookies';
import { StackNavigationProp } from '@react-navigation/stack';
import { logout as logoutAction } from '../../actions/login';
import { selectServerRequest as selectServerRequestAction } from '../../actions/server';
@ -29,8 +29,25 @@ import database from '../../lib/database';
import { isFDroidBuild } from '../../constants/environment';
import { getUserSelector } from '../../selectors/login';
class SettingsView extends React.Component {
static navigationOptions = ({ navigation, isMasterDetail }) => ({
interface IProps {
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: () =>
isMasterDetail ? (
<HeaderButton.CloseModal navigation={navigation} testID='settings-view-close' />
@ -40,26 +57,12 @@ class SettingsView extends React.Component {
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 () => {
const { logout, user } = this.props;
const db = database.servers;
const usersCollection = db.get('users');
try {
const userRecord = await usersCollection.find(user.id);
const userRecord: any = await usersCollection.find(user.id);
if (userRecord.isFromWebView) {
showConfirmationAlert({
title: I18n.t('Clear_cookies_alert'),
@ -84,6 +87,7 @@ class SettingsView extends React.Component {
handleLogout = () => {
logEvent(events.SE_LOG_OUT);
// @ts-ignore
showConfirmationAlert({
message: I18n.t('You_will_be_logged_out_of_this_application'),
confirmationText: I18n.t('Logout'),
@ -93,6 +97,7 @@ class SettingsView extends React.Component {
handleClearCache = () => {
logEvent(events.SE_CLEAR_LOCAL_SERVER_CACHE);
/* @ts-ignore */
showConfirmationAlert({
message: I18n.t('This_will_clear_all_your_offline_data'),
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()}`]);
const { navigation } = this.props;
navigation.navigate(screen);
@ -160,7 +166,7 @@ class SettingsView extends React.Component {
this.saveToClipboard(getReadableVersion);
};
saveToClipboard = async content => {
saveToClipboard = async (content: string) => {
await Clipboard.setString(content);
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,
user: getUserSelector(state),
isMasterDetail: state.app.isMasterDetail
});
const mapDispatchToProps = dispatch => ({
const mapDispatchToProps = (dispatch: any) => ({
logout: () => dispatch(logoutAction()),
selectServerRequest: params => dispatch(selectServerRequestAction(params)),
appStart: params => dispatch(appStartAction(params))
selectServerRequest: (params: any) => dispatch(selectServerRequestAction(params)),
appStart: (params: any) => dispatch(appStartAction(params))
});
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(SettingsView));

View File

@ -1,46 +1,75 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Switch } from 'react-native';
import PropTypes from 'prop-types';
import { useSelector } from 'react-redux';
import I18n from '../../i18n';
import { events, logEvent } from '../../utils/log';
import log, { logEvent, events } from '../../utils/log';
import SafeAreaView from '../../containers/SafeAreaView';
import StatusBar from '../../containers/StatusBar';
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 {
static navigationOptions = () => ({
title: I18n.t('Preferences')
});
const UserPreferencesView = ({ navigation }) => {
const user = useSelector(state => getUserSelector(state));
const [enableParser, setEnableParser] = useState(user.enableMessageParserEarlyAdoption);
static propTypes = {
navigation: PropTypes.object
};
useEffect(() => {
navigation.setOptions({
title: I18n.t('Preferences')
});
}, []);
navigateToScreen = (screen, params) => {
const navigateToScreen = (screen, params) => {
logEvent(events[`SE_GO_${screen.replace('View', '').toUpperCase()}`]);
const { navigation } = this.props;
navigation.navigate(screen, params);
};
render() {
return (
<SafeAreaView testID='preferences-view'>
<StatusBar />
<List.Container>
<List.Section>
<List.Separator />
<List.Item
title='Notifications'
onPress={() => this.navigateToScreen('UserNotificationPrefView')}
showActionIndicator
testID='preferences-view-notifications'
/>
<List.Separator />
</List.Section>
</List.Container>
</SafeAreaView>
);
}
}
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 (
<SafeAreaView testID='preferences-view'>
<StatusBar />
<List.Container>
<List.Section>
<List.Separator />
<List.Item
title='Notifications'
onPress={() => navigateToScreen('UserNotificationPrefView')}
showActionIndicator
testID='preferences-view-notifications'
/>
<List.Separator />
</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>
</SafeAreaView>
);
};
UserPreferencesView.propTypes = {
navigation: PropTypes.object
};
export default UserPreferencesView;

View File

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

View File

@ -47,23 +47,25 @@
"@react-navigation/drawer": "5.12.5",
"@react-navigation/native": "5.9.4",
"@react-navigation/stack": "5.14.5",
"@rocket.chat/message-parser": "0.30.0",
"@rocket.chat/react-native-fast-image": "^8.2.0",
"@rocket.chat/sdk": "RocketChat/Rocket.Chat.js.SDK#mobile",
"@rocket.chat/ui-kit": "0.13.0",
"@types/url-parse": "^1.4.4",
"bytebuffer": "^5.0.1",
"color2k": "1.2.4",
"commonmark": "git+https://github.com/RocketChat/commonmark.js.git",
"commonmark-react-renderer": "git+https://github.com/RocketChat/commonmark-react-renderer.git",
"dequal": "^2.0.2",
"ejson": "2.2.1",
"expo-apple-authentication": "^2.2.1",
"expo-av": "8.2.1",
"expo-file-system": "9.0.1",
"expo-haptics": "8.2.1",
"expo-keep-awake": "8.2.1",
"expo-local-authentication": "9.2.0",
"expo-video-thumbnails": "5.1.0",
"expo-web-browser": "8.3.1",
"expo-apple-authentication": "3.2.1",
"expo-av": "9.2.3",
"expo-file-system": "11.1.3",
"expo-haptics": "10.1.0",
"expo-keep-awake": "9.2.0",
"expo-local-authentication": "11.1.1",
"expo-video-thumbnails": "5.2.1",
"expo-web-browser": "9.2.0",
"hoist-non-react-statics": "3.3.2",
"i18n-js": "3.8.0",
"js-base64": "3.6.1",
@ -112,7 +114,7 @@
"react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.0",
"react-native-slowlog": "^1.0.2",
"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-webview": "10.3.2",
"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 '../../app/views/ThreadMessagesView/Item.stories.js';
import './Avatar';
import './NewMarkdown';
import '../../app/containers/BackgroundContainer/index.stories.js';
import '../../app/containers/RoomHeader/RoomHeader.stories.js';
import '../../app/views/RoomView/LoadMore/LoadMore.stories';

1008
yarn.lock

File diff suppressed because it is too large Load Diff