[IMPROVEMENT] Message Touchable (#2082)

* [FIX] Avatar touchable

* [IMPROVEMENT] onLongPress on all Message Touchables

* [IMPROVEMENT] User & baseUrl on MessageContext

* [FIX] Context Access

* [FIX] BaseURL

* Fix User

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Djorkaeff Alexandre 2020-04-30 17:05:59 -03:00 committed by GitHub
parent 2ea6d34fd1
commit e46ee13b38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 3831 additions and 4271 deletions

File diff suppressed because it is too large Load Diff

View File

@ -2,12 +2,13 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import { settings as RocketChatSettings } from '@rocket.chat/sdk'; import { settings as RocketChatSettings } from '@rocket.chat/sdk';
import Touch from '../utils/touch';
import { avatarURL } from '../utils/avatar'; import { avatarURL } from '../utils/avatar';
const Avatar = React.memo(({ const Avatar = React.memo(({
text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress, theme text, size, baseUrl, borderRadius, style, avatar, type, children, userId, token, onPress
}) => { }) => {
const avatarStyle = { const avatarStyle = {
width: size, width: size,
@ -36,9 +37,9 @@ const Avatar = React.memo(({
if (onPress) { if (onPress) {
image = ( image = (
<Touch onPress={onPress} theme={theme}> <Touchable onPress={onPress}>
{image} {image}
</Touch> </Touchable>
); );
} }
@ -61,7 +62,6 @@ Avatar.propTypes = {
children: PropTypes.object, children: PropTypes.object,
userId: PropTypes.string, userId: PropTypes.string,
token: PropTypes.string, token: PropTypes.string,
theme: PropTypes.string,
onPress: PropTypes.func onPress: PropTypes.func
}; };

View File

@ -8,7 +8,7 @@ import Video from './Video';
import Reply from './Reply'; import Reply from './Reply';
const Attachments = React.memo(({ const Attachments = React.memo(({
attachments, timeFormat, user, baseUrl, showAttachment, getCustomEmoji, theme attachments, timeFormat, showAttachment, getCustomEmoji, theme
}) => { }) => {
if (!attachments || attachments.length === 0) { if (!attachments || attachments.length === 0) {
return null; return null;
@ -16,25 +16,23 @@ const Attachments = React.memo(({
return attachments.map((file, index) => { return attachments.map((file, index) => {
if (file.image_url) { if (file.image_url) {
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Image key={file.image_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
if (file.audio_url) { if (file.audio_url) {
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Audio key={file.audio_url} file={file} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
if (file.video_url) { if (file.video_url) {
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Video key={file.video_url} file={file} showAttachment={showAttachment} getCustomEmoji={getCustomEmoji} theme={theme} />;
} }
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} theme={theme} />; return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} getCustomEmoji={getCustomEmoji} theme={theme} />;
}); });
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments) && prevProps.theme === nextProps.theme);
Attachments.propTypes = { Attachments.propTypes = {
attachments: PropTypes.array, attachments: PropTypes.array,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
user: PropTypes.object,
baseUrl: PropTypes.string,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -7,14 +7,15 @@ import { Audio } from 'expo-av';
import Slider from '@react-native-community/slider'; import Slider from '@react-native-community/slider';
import moment from 'moment'; import moment from 'moment';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { isAndroid, isIOS } from '../../utils/deviceInfo'; import { isAndroid, isIOS } from '../../utils/deviceInfo';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import MessageContext from './Context';
import ActivityIndicator from '../ActivityIndicator'; import ActivityIndicator from '../ActivityIndicator';
const mode = { const mode = {
@ -91,10 +92,10 @@ Button.propTypes = {
Button.displayName = 'MessageAudioButton'; Button.displayName = 'MessageAudioButton';
class MessageAudio extends React.Component { class MessageAudio extends React.Component {
static contextType = MessageContext;
static propTypes = { static propTypes = {
file: PropTypes.object.isRequired, file: PropTypes.object.isRequired,
baseUrl: PropTypes.string.isRequired,
user: PropTypes.object.isRequired,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool, split: PropTypes.bool,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
@ -102,13 +103,11 @@ class MessageAudio extends React.Component {
constructor(props) { constructor(props) {
super(props); super(props);
const { baseUrl, file, user } = props;
this.state = { this.state = {
loading: false, loading: false,
currentTime: 0, currentTime: 0,
duration: 0, duration: 0,
paused: true, paused: true
uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }`
}; };
this.sound = new Audio.Sound(); this.sound = new Audio.Sound();
@ -116,12 +115,13 @@ class MessageAudio extends React.Component {
} }
async componentDidMount() { async componentDidMount() {
const { uri } = this.state; const { file } = this.props;
const { baseUrl, user } = this.context;
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
await Audio.setAudioModeAsync(mode); await Audio.setAudioModeAsync(mode);
await this.sound.loadAsync({ uri }); await this.sound.loadAsync({ uri: `${ baseUrl }${ file.audio_url }?rc_uid=${ user.id }&rc_token=${ user.token }` });
} catch { } catch {
// Do nothing // Do nothing
} }
@ -130,7 +130,7 @@ class MessageAudio extends React.Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
currentTime, duration, paused, uri, loading currentTime, duration, paused, loading
} = this.state; } = this.state;
const { file, split, theme } = this.props; const { file, split, theme } = this.props;
if (nextProps.theme !== theme) { if (nextProps.theme !== theme) {
@ -145,9 +145,6 @@ class MessageAudio extends React.Component {
if (nextState.paused !== paused) { if (nextState.paused !== paused) {
return true; return true;
} }
if (nextState.uri !== uri) {
return true;
}
if (!equal(nextProps.file, file)) { if (!equal(nextProps.file, file)) {
return true; return true;
} }
@ -237,9 +234,10 @@ class MessageAudio extends React.Component {
loading, paused, currentTime, duration loading, paused, currentTime, duration
} = this.state; } = this.state;
const { const {
user, baseUrl, file, getCustomEmoji, split, theme file, getCustomEmoji, split, theme
} = this.props; } = this.props;
const { description } = file; const { description } = file;
const { baseUrl, user } = this.context;
if (!baseUrl) { if (!baseUrl) {
return null; return null;

View File

@ -1,17 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Broadcast = React.memo(({ const Broadcast = React.memo(({
author, user, broadcast, replyBroadcast, theme author, broadcast, theme
}) => { }) => {
const { user, replyBroadcast } = useContext(MessageContext);
const isOwn = author._id === user.id; const isOwn = author._id === user.id;
if (broadcast && !isOwn) { if (broadcast && !isOwn) {
return ( return (
@ -36,10 +38,8 @@ const Broadcast = React.memo(({
Broadcast.propTypes = { Broadcast.propTypes = {
author: PropTypes.object, author: PropTypes.object,
user: PropTypes.object,
broadcast: PropTypes.bool, broadcast: PropTypes.bool,
theme: PropTypes.string, theme: PropTypes.string
replyBroadcast: PropTypes.func
}; };
Broadcast.displayName = 'MessageBroadcast'; Broadcast.displayName = 'MessageBroadcast';

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, BUTTON_HIT_SLOP } from './utils'; import { formatLastMessage, BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useContext } from 'react';
import { Text, View } from 'react-native'; import { Text, View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import equal from 'deep-equal'; import equal from 'deep-equal';
@ -8,6 +8,7 @@ import styles from './styles';
import Markdown from '../markdown'; import Markdown from '../markdown';
import { getInfoMessage } from './utils'; import { getInfoMessage } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Content = React.memo((props) => { const Content = React.memo((props) => {
if (props.isInfo) { if (props.isInfo) {
@ -26,12 +27,13 @@ const Content = React.memo((props) => {
if (props.tmid && !props.msg) { if (props.tmid && !props.msg) {
content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>; content = <Text style={[styles.text, { color: themes[props.theme].bodyText }]}>{I18n.t('Sent_an_attachment')}</Text>;
} else { } else {
const { baseUrl, user } = useContext(MessageContext);
content = ( content = (
<Markdown <Markdown
msg={props.msg} msg={props.msg}
baseUrl={props.baseUrl} baseUrl={baseUrl}
getCustomEmoji={props.getCustomEmoji} getCustomEmoji={props.getCustomEmoji}
username={props.user.username} username={user.username}
isEdited={props.isEdited} isEdited={props.isEdited}
numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0} numberOfLines={(props.tmid && !props.isThreadRoom) ? 1 : 0}
preview={props.tmid && !props.isThreadRoom} preview={props.tmid && !props.isThreadRoom}
@ -77,8 +79,6 @@ Content.propTypes = {
msg: PropTypes.string, msg: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
isEdited: PropTypes.bool, isEdited: PropTypes.bool,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),

View File

@ -0,0 +1,4 @@
import React from 'react';
const MessageContext = React.createContext();
export default MessageContext;

View File

@ -1,20 +1,22 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils'; import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
import styles from './styles'; import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { DISCUSSION } from './constants'; import { DISCUSSION } from './constants';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const Discussion = React.memo(({ const Discussion = React.memo(({
msg, dcount, dlm, onDiscussionPress, theme msg, dcount, dlm, theme
}) => { }) => {
const time = formatLastMessage(dlm); const time = formatLastMessage(dlm);
const buttonText = formatMessageCount(dcount, DISCUSSION); const buttonText = formatMessageCount(dcount, DISCUSSION);
const { onDiscussionPress } = useContext(MessageContext);
return ( return (
<> <>
<Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text> <Text style={[styles.startedDiscussion, { color: themes[theme].auxiliaryText }]}>{I18n.t('Started_discussion')}</Text>
@ -55,8 +57,7 @@ Discussion.propTypes = {
msg: PropTypes.string, msg: PropTypes.string,
dcount: PropTypes.number, dcount: PropTypes.number,
dlm: PropTypes.string, dlm: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string
onDiscussionPress: PropTypes.func
}; };
Discussion.displayName = 'MessageDiscussion'; Discussion.displayName = 'MessageDiscussion';

View File

@ -1,13 +1,15 @@
import React from 'react'; import React, { useContext } from 'react';
import { Text } from 'react-native'; import { Text } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import shortnameToUnicode from '../../utils/shortnameToUnicode'; import shortnameToUnicode from '../../utils/shortnameToUnicode';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MessageContext from './Context';
const Emoji = React.memo(({ const Emoji = React.memo(({
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji content, standardEmojiStyle, customEmojiStyle, getCustomEmoji
}) => { }) => {
const { baseUrl } = useContext(MessageContext);
const parsedContent = content.replace(/^:|:$/g, ''); const parsedContent = content.replace(/^:|:$/g, '');
const emoji = getCustomEmoji(parsedContent); const emoji = getCustomEmoji(parsedContent);
if (emoji) { if (emoji) {
@ -20,7 +22,6 @@ Emoji.propTypes = {
content: PropTypes.string, content: PropTypes.string,
standardEmojiStyle: PropTypes.object, standardEmojiStyle: PropTypes.object,
customEmojiStyle: PropTypes.object, customEmojiStyle: PropTypes.object,
baseUrl: PropTypes.string,
getCustomEmoji: PropTypes.func getCustomEmoji: PropTypes.func
}; };
Emoji.displayName = 'MessageEmoji'; Emoji.displayName = 'MessageEmoji';

View File

@ -1,18 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View } from 'react-native'; import { View } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import { createImageProgress } from 'react-native-image-progress'; import { createImageProgress } from 'react-native-image-progress';
import * as Progress from 'react-native-progress'; import * as Progress from 'react-native-progress';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import styles from './styles'; import styles from './styles';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const ImageProgress = createImageProgress(FastImage); const ImageProgress = createImageProgress(FastImage);
@ -41,8 +42,9 @@ export const MessageImage = React.memo(({ img, theme }) => (
)); ));
const ImageContainer = React.memo(({ const ImageContainer = React.memo(({
file, imageUrl, baseUrl, user, showAttachment, getCustomEmoji, split, theme file, imageUrl, showAttachment, getCustomEmoji, split, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl); const img = imageUrl || formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
if (!img) { if (!img) {
return null; return null;
@ -71,8 +73,6 @@ const ImageContainer = React.memo(({
ImageContainer.propTypes = { ImageContainer.propTypes = {
file: PropTypes.object, file: PropTypes.object,
imageUrl: PropTypes.string, imageUrl: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,

View File

@ -1,8 +1,10 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { View } from 'react-native'; import { View } from 'react-native';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import MessageContext from './Context';
import User from './User'; import User from './User';
import styles from './styles'; import styles from './styles';
import RepliedThread from './RepliedThread'; import RepliedThread from './RepliedThread';
@ -111,10 +113,11 @@ const MessageTouchable = React.memo((props) => {
</View> </View>
); );
} }
const { onPress, onLongPress } = useContext(MessageContext);
return ( return (
<Touchable <Touchable
onLongPress={props.onLongPress} onLongPress={onLongPress}
onPress={props.onPress} onPress={onPress}
disabled={props.isInfo || props.archived || props.isTemp} disabled={props.isInfo || props.archived || props.isTemp}
> >
<View> <View>
@ -129,9 +132,7 @@ MessageTouchable.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
isInfo: PropTypes.bool, isInfo: PropTypes.bool,
isTemp: PropTypes.bool, isTemp: PropTypes.bool,
archived: PropTypes.bool, archived: PropTypes.bool
onLongPress: PropTypes.func,
onPress: PropTypes.func
}; };
Message.propTypes = { Message.propTypes = {
@ -143,7 +144,6 @@ Message.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
style: PropTypes.any, style: PropTypes.any,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onPress: PropTypes.func,
isReadReceiptEnabled: PropTypes.bool, isReadReceiptEnabled: PropTypes.bool,
unread: PropTypes.bool, unread: PropTypes.bool,
theme: PropTypes.string theme: PropTypes.string

View File

@ -1,34 +1,31 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { TouchableOpacity } from 'react-native';
import Avatar from '../Avatar'; import Avatar from '../Avatar';
import styles from './styles'; import styles from './styles';
import MessageContext from './Context';
const MessageAvatar = React.memo(({ const MessageAvatar = React.memo(({
isHeader, avatar, author, baseUrl, user, small, navToRoomInfo isHeader, avatar, author, small, navToRoomInfo
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
if (isHeader && author) { if (isHeader && author) {
const navParam = { const navParam = {
t: 'd', t: 'd',
rid: author._id rid: author._id
}; };
return ( return (
<TouchableOpacity
onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id}
>
<Avatar <Avatar
style={small ? styles.avatarSmall : styles.avatar} style={small ? styles.avatarSmall : styles.avatar}
text={avatar ? '' : author.username} text={avatar ? '' : author.username}
size={small ? 20 : 36} size={small ? 20 : 36}
borderRadius={small ? 2 : 4} borderRadius={small ? 2 : 4}
onPress={author._id === user.id ? undefined : () => navToRoomInfo(navParam)}
avatar={avatar} avatar={avatar}
baseUrl={baseUrl} baseUrl={baseUrl}
userId={user.id} userId={user.id}
token={user.token} token={user.token}
/> />
</TouchableOpacity>
); );
} }
return null; return null;
@ -38,8 +35,6 @@ MessageAvatar.propTypes = {
isHeader: PropTypes.bool, isHeader: PropTypes.bool,
avatar: PropTypes.string, avatar: PropTypes.string,
author: PropTypes.obj, author: PropTypes.obj,
baseUrl: PropTypes.string,
user: PropTypes.obj,
small: PropTypes.bool, small: PropTypes.bool,
navToRoomInfo: PropTypes.func navToRoomInfo: PropTypes.func
}; };

View File

@ -1,16 +1,18 @@
import React from 'react'; import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import MessageContext from './Context';
const MessageError = React.memo(({ hasError, onErrorPress, theme }) => { const MessageError = React.memo(({ hasError, theme }) => {
if (!hasError) { if (!hasError) {
return null; return null;
} }
const { onErrorPress } = useContext(MessageContext);
return ( return (
<Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}> <Touchable onPress={onErrorPress} style={styles.errorButton} hitSlop={BUTTON_HIT_SLOP}>
<CustomIcon name='warning' color={themes[theme].dangerColor} size={18} /> <CustomIcon name='warning' color={themes[theme].dangerColor} size={18} />
@ -20,7 +22,6 @@ const MessageError = React.memo(({ hasError, onErrorPress, theme }) => {
MessageError.propTypes = { MessageError.propTypes = {
hasError: PropTypes.bool, hasError: PropTypes.bool,
onErrorPress: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
MessageError.displayName = 'MessageError'; MessageError.displayName = 'MessageError';

View File

@ -1,16 +1,19 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text } from 'react-native'; import { View, Text } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Touchable from './Touchable';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import styles from './styles'; import styles from './styles';
import Emoji from './Emoji'; import Emoji from './Emoji';
import { BUTTON_HIT_SLOP } from './utils'; import { BUTTON_HIT_SLOP } from './utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withTheme } from '../../theme'; import { withTheme } from '../../theme';
import MessageContext from './Context';
const AddReaction = React.memo(({ reactionInit, theme }) => ( const AddReaction = React.memo(({ theme }) => {
const { reactionInit } = useContext(MessageContext);
return (
<Touchable <Touchable
onPress={reactionInit} onPress={reactionInit}
key='message-add-reaction' key='message-add-reaction'
@ -23,11 +26,15 @@ const AddReaction = React.memo(({ reactionInit, theme }) => (
<CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} /> <CustomIcon name='add-reaction' size={21} color={themes[theme].tintColor} />
</View> </View>
</Touchable> </Touchable>
)); );
});
const Reaction = React.memo(({ const Reaction = React.memo(({
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji, theme reaction, getCustomEmoji, theme
}) => { }) => {
const {
onReactionPress, onReactionLongPress, baseUrl, user
} = useContext(MessageContext);
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1; const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
return ( return (
<Touchable <Touchable
@ -54,7 +61,7 @@ const Reaction = React.memo(({
}); });
const Reactions = React.memo(({ const Reactions = React.memo(({
reactions, user, baseUrl, onReactionPress, reactionInit, onReactionLongPress, getCustomEmoji, theme reactions, getCustomEmoji, theme
}) => { }) => {
if (!Array.isArray(reactions) || reactions.length === 0) { if (!Array.isArray(reactions) || reactions.length === 0) {
return null; return null;
@ -65,25 +72,17 @@ const Reactions = React.memo(({
<Reaction <Reaction
key={reaction.emoji} key={reaction.emoji}
reaction={reaction} reaction={reaction}
user={user}
baseUrl={baseUrl}
onReactionLongPress={onReactionLongPress}
onReactionPress={onReactionPress}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
))} ))}
<AddReaction reactionInit={reactionInit} theme={theme} /> <AddReaction theme={theme} />
</View> </View>
); );
}); });
Reaction.propTypes = { Reaction.propTypes = {
reaction: PropTypes.object, reaction: PropTypes.object,
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
@ -91,18 +90,12 @@ Reaction.displayName = 'MessageReaction';
Reactions.propTypes = { Reactions.propTypes = {
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
user: PropTypes.object,
baseUrl: PropTypes.string,
onReactionPress: PropTypes.func,
reactionInit: PropTypes.func,
onReactionLongPress: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
Reactions.displayName = 'MessageReactions'; Reactions.displayName = 'MessageReactions';
AddReaction.propTypes = { AddReaction.propTypes = {
reactionInit: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };
AddReaction.displayName = 'MessageAddReaction'; AddReaction.displayName = 'MessageAddReaction';

View File

@ -1,15 +1,16 @@
import React from 'react'; import React, { useContext } from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import { withSplit } from '../../split'; import { withSplit } from '../../split';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -79,12 +80,13 @@ const Title = React.memo(({ attachment, timeFormat, theme }) => {
}); });
const Description = React.memo(({ const Description = React.memo(({
attachment, baseUrl, user, getCustomEmoji, theme attachment, getCustomEmoji, theme
}) => { }) => {
const text = attachment.text || attachment.title; const text = attachment.text || attachment.title;
if (!text) { if (!text) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
return ( return (
<Markdown <Markdown
msg={text} msg={text}
@ -124,11 +126,12 @@ const Fields = React.memo(({ attachment, theme }) => {
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme); }, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields) && prevProps.theme === nextProps.theme);
const Reply = React.memo(({ const Reply = React.memo(({
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, split, theme attachment, timeFormat, index, getCustomEmoji, split, theme
}) => { }) => {
if (!attachment) { if (!attachment) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
const onPress = () => { const onPress = () => {
let url = attachment.title_link || attachment.author_link; let url = attachment.title_link || attachment.author_link;
@ -160,8 +163,6 @@ const Reply = React.memo(({
<Description <Description
attachment={attachment} attachment={attachment}
timeFormat={timeFormat} timeFormat={timeFormat}
baseUrl={baseUrl}
user={user}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
theme={theme} theme={theme}
/> />
@ -174,8 +175,6 @@ const Reply = React.memo(({
Reply.propTypes = { Reply.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
baseUrl: PropTypes.string,
user: PropTypes.object,
index: PropTypes.number, index: PropTypes.number,
theme: PropTypes.string, theme: PropTypes.string,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
@ -192,8 +191,6 @@ Title.displayName = 'MessageReplyTitle';
Description.propTypes = { Description.propTypes = {
attachment: PropTypes.object, attachment: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string
}; };

View File

@ -0,0 +1,25 @@
import React, { useContext } from 'react';
import Touchable from 'react-native-platform-touchable';
import PropTypes from 'prop-types';
import MessageContext from './Context';
const RCTouchable = React.memo(({ children, ...props }) => {
const { onLongPress } = useContext(MessageContext);
return (
<Touchable
onLongPress={onLongPress}
{...props}
>
{children}
</Touchable>
);
});
RCTouchable.propTypes = {
children: PropTypes.node
};
RCTouchable.Ripple = (...args) => Touchable.Ripple(...args);
RCTouchable.SelectableBackgroundBorderless = () => Touchable.SelectableBackgroundBorderless();
export default RCTouchable;

View File

@ -1,12 +1,12 @@
import React from 'react'; import React, { useContext } from 'react';
import { import {
View, Text, StyleSheet, Clipboard View, Text, StyleSheet, Clipboard
} from 'react-native'; } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import Touchable from './Touchable';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
@ -15,6 +15,7 @@ import { withSplit } from '../../split';
import { LISTENER } from '../Toast'; import { LISTENER } from '../Toast';
import EventEmitter from '../../utils/events'; import EventEmitter from '../../utils/events';
import I18n from '../../i18n'; import I18n from '../../i18n';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -52,10 +53,11 @@ const styles = StyleSheet.create({
} }
}); });
const UrlImage = React.memo(({ image, user, baseUrl }) => { const UrlImage = React.memo(({ image }) => {
if (!image) { if (!image) {
return null; return null;
} }
const { baseUrl, user } = useContext(MessageContext);
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`; image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />; return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
}, (prevProps, nextProps) => prevProps.image === nextProps.image); }, (prevProps, nextProps) => prevProps.image === nextProps.image);
@ -79,7 +81,7 @@ const UrlContent = React.memo(({ title, description, theme }) => (
}); });
const Url = React.memo(({ const Url = React.memo(({
url, index, user, baseUrl, split, theme url, index, split, theme
}) => { }) => {
if (!url) { if (!url) {
return null; return null;
@ -109,7 +111,7 @@ const Url = React.memo(({
background={Touchable.Ripple(themes[theme].bannerBackground)} background={Touchable.Ripple(themes[theme].bannerBackground)}
> >
<> <>
<UrlImage image={url.image} user={user} baseUrl={baseUrl} /> <UrlImage image={url.image} />
<UrlContent title={url.title} description={url.description} theme={theme} /> <UrlContent title={url.title} description={url.description} theme={theme} />
</> </>
</Touchable> </Touchable>
@ -117,21 +119,19 @@ const Url = React.memo(({
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.url, newProps.url) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
const Urls = React.memo(({ const Urls = React.memo(({
urls, user, baseUrl, split, theme urls, split, theme
}) => { }) => {
if (!urls || urls.length === 0) { if (!urls || urls.length === 0) {
return null; return null;
} }
return urls.map((url, index) => ( return urls.map((url, index) => (
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} split={split} theme={theme} /> <Url url={url} key={url.url} index={index} split={split} theme={theme} />
)); ));
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme); }, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls) && oldProps.split === newProps.split && oldProps.theme === newProps.theme);
UrlImage.propTypes = { UrlImage.propTypes = {
image: PropTypes.string, image: PropTypes.string
user: PropTypes.object,
baseUrl: PropTypes.string
}; };
UrlImage.displayName = 'MessageUrlImage'; UrlImage.displayName = 'MessageUrlImage';
@ -145,8 +145,6 @@ UrlContent.displayName = 'MessageUrlContent';
Url.propTypes = { Url.propTypes = {
url: PropTypes.object.isRequired, url: PropTypes.object.isRequired,
index: PropTypes.number, index: PropTypes.number,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };
@ -154,8 +152,6 @@ Url.displayName = 'MessageUrl';
Urls.propTypes = { Urls.propTypes = {
urls: PropTypes.array, urls: PropTypes.array,
user: PropTypes.object,
baseUrl: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
split: PropTypes.bool split: PropTypes.bool
}; };

View File

@ -1,4 +1,4 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
View, Text, StyleSheet, TouchableOpacity View, Text, StyleSheet, TouchableOpacity
@ -11,6 +11,7 @@ import { withTheme } from '../../theme';
import MessageError from './MessageError'; import MessageError from './MessageError';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import messageStyles from './styles'; import messageStyles from './styles';
import MessageContext from './Context';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -35,13 +36,14 @@ const styles = StyleSheet.create({
}); });
const User = React.memo(({ const User = React.memo(({
isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, user, ...props isHeader, useRealName, author, alias, ts, timeFormat, hasError, theme, navToRoomInfo, ...props
}) => { }) => {
if (isHeader || hasError) { if (isHeader || hasError) {
const navParam = { const navParam = {
t: 'd', t: 'd',
rid: author._id rid: author._id
}; };
const { user } = useContext(MessageContext);
const username = (useRealName && author.name) || author.username; const username = (useRealName && author.name) || author.username;
const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null; const aliasUsername = alias ? (<Text style={[styles.alias, { color: themes[theme].auxiliaryText }]}> @{username}</Text>) : null;
const time = moment(ts).format(timeFormat); const time = moment(ts).format(timeFormat);
@ -49,15 +51,14 @@ const User = React.memo(({
return ( return (
<View style={styles.container}> <View style={styles.container}>
<TouchableOpacity <TouchableOpacity
style={styles.titleContainer}
onPress={() => navToRoomInfo(navParam)} onPress={() => navToRoomInfo(navParam)}
disabled={author._id === user.id} disabled={author._id === user.id}
> >
<View style={styles.titleContainer}>
<Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}> <Text style={[styles.username, { color: themes[theme].titleText }]} numberOfLines={1}>
{alias || username} {alias || username}
{aliasUsername} {aliasUsername}
</Text> </Text>
</View>
</TouchableOpacity> </TouchableOpacity>
<Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text> <Text style={[messageStyles.time, { color: themes[theme].auxiliaryText }]}>{time}</Text>
{ hasError && <MessageError hasError={hasError} theme={theme} {...props} /> } { hasError && <MessageError hasError={hasError} theme={theme} {...props} /> }
@ -76,7 +77,6 @@ User.propTypes = {
ts: PropTypes.instanceOf(Date), ts: PropTypes.instanceOf(Date),
timeFormat: PropTypes.string, timeFormat: PropTypes.string,
theme: PropTypes.string, theme: PropTypes.string,
user: PropTypes.obj,
navToRoomInfo: PropTypes.func navToRoomInfo: PropTypes.func
}; };
User.displayName = 'MessageUser'; User.displayName = 'MessageUser';

View File

@ -1,9 +1,9 @@
import React from 'react'; import React, { useContext } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { StyleSheet } from 'react-native'; import { StyleSheet } from 'react-native';
import Touchable from 'react-native-platform-touchable';
import isEqual from 'deep-equal'; import isEqual from 'deep-equal';
import Touchable from './Touchable';
import Markdown from '../markdown'; import Markdown from '../markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import { isIOS, isTablet } from '../../utils/deviceInfo'; import { isIOS, isTablet } from '../../utils/deviceInfo';
@ -11,6 +11,7 @@ import { CustomIcon } from '../../lib/Icons';
import { formatAttachmentUrl } from '../../lib/utils'; import { formatAttachmentUrl } from '../../lib/utils';
import { themes } from '../../constants/colors'; import { themes } from '../../constants/colors';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import MessageContext from './Context';
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])]; const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/3gp', 'video/mkv'])];
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1; const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
@ -27,12 +28,12 @@ const styles = StyleSheet.create({
}); });
const Video = React.memo(({ const Video = React.memo(({
file, baseUrl, user, showAttachment, getCustomEmoji, theme file, showAttachment, getCustomEmoji, theme
}) => { }) => {
const { baseUrl, user } = useContext(MessageContext);
if (!baseUrl) { if (!baseUrl) {
return null; return null;
} }
const onPress = () => { const onPress = () => {
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type)) {
return showAttachment(file); return showAttachment(file);
@ -61,8 +62,6 @@ const Video = React.memo(({
Video.propTypes = { Video.propTypes = {
file: PropTypes.object, file: PropTypes.object,
baseUrl: PropTypes.string,
user: PropTypes.object,
showAttachment: PropTypes.func, showAttachment: PropTypes.func,
getCustomEmoji: PropTypes.func, getCustomEmoji: PropTypes.func,
theme: PropTypes.string theme: PropTypes.string

View File

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import Message from './Message'; import Message from './Message';
import MessageContext from './Context';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
import { SYSTEM_MESSAGES, getMessageTranslation } from './utils'; import { SYSTEM_MESSAGES, getMessageTranslation } from './utils';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
@ -240,6 +241,20 @@ class MessageContainer extends React.Component {
} }
return ( return (
<MessageContext.Provider
value={{
user,
baseUrl,
onPress: this.onPress,
onLongPress: this.onLongPress,
reactionInit: this.reactionInit,
onErrorPress: this.onErrorPress,
replyBroadcast: this.replyBroadcast,
onReactionPress: this.onReactionPress,
onDiscussionPress: this.onDiscussionPress,
onReactionLongPress: this.onReactionLongPress
}}
>
<Message <Message
id={id} id={id}
msg={message} msg={message}
@ -253,13 +268,11 @@ class MessageContainer extends React.Component {
reactions={reactions} reactions={reactions}
alias={alias} alias={alias}
avatar={avatar} avatar={avatar}
user={user}
timeFormat={timeFormat} timeFormat={timeFormat}
customThreadTimeFormat={customThreadTimeFormat} customThreadTimeFormat={customThreadTimeFormat}
style={style} style={style}
archived={archived} archived={archived}
broadcast={broadcast} broadcast={broadcast}
baseUrl={baseUrl}
useRealName={useRealName} useRealName={useRealName}
isReadReceiptEnabled={isReadReceiptEnabled} isReadReceiptEnabled={isReadReceiptEnabled}
unread={unread} unread={unread}
@ -282,14 +295,6 @@ class MessageContainer extends React.Component {
isInfo={this.isInfo} isInfo={this.isInfo}
isTemp={this.isTemp} isTemp={this.isTemp}
hasError={this.hasError} hasError={this.hasError}
onErrorPress={this.onErrorPress}
onPress={this.onPress}
onLongPress={this.onLongPress}
onReactionLongPress={this.onReactionLongPress}
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
reactionInit={this.reactionInit}
onDiscussionPress={this.onDiscussionPress}
showAttachment={showAttachment} showAttachment={showAttachment}
getCustomEmoji={getCustomEmoji} getCustomEmoji={getCustomEmoji}
navToRoomInfo={navToRoomInfo} navToRoomInfo={navToRoomInfo}
@ -297,6 +302,7 @@ class MessageContainer extends React.Component {
blockAction={blockAction} blockAction={blockAction}
theme={theme} theme={theme}
/> />
</MessageContext.Provider>
); );
} }
} }

View File

@ -37,7 +37,6 @@ const RoomHeaderLeft = ({
style={styles.avatar} style={styles.avatar}
userId={userId} userId={userId}
token={token} token={token}
theme={theme}
onPress={goRoomActionsView} onPress={goRoomActionsView}
/> />
); );

View File

@ -8,6 +8,7 @@ import messagesStatus from '../../app/constants/messagesStatus';
import MessageSeparator from '../../app/views/RoomView/Separator'; import MessageSeparator from '../../app/views/RoomView/Separator';
import { themes } from '../../app/constants/colors'; import { themes } from '../../app/constants/colors';
import MessageContext from '../../app/containers/message/Context';
let _theme = 'light'; let _theme = 'light';
@ -41,6 +42,20 @@ const getCustomEmoji = (content) => {
}; };
const Message = props => ( const Message = props => (
<MessageContext.Provider
value={{
user,
baseUrl,
onPress: () => {},
onLongPress: () => {},
reactionInit: () => {},
onErrorPress: () => {},
replyBroadcast: () => {},
onReactionPress: () => {},
onDiscussionPress: () => {},
onReactionLongPress: () => {}
}}
>
<MessageComponent <MessageComponent
baseUrl={baseUrl} baseUrl={baseUrl}
user={user} user={user}
@ -52,6 +67,7 @@ const Message = props => (
theme={_theme} theme={_theme}
{...props} {...props}
/> />
</MessageContext.Provider>
); );
// eslint-disable-next-line react/prop-types // eslint-disable-next-line react/prop-types