Message render performance (#880)
- Refactored Message component to use React.memo and re-render only what's necessary - Added a test mode to toggle markdown parse by long press drawer (it'll be removed in the next release)
This commit is contained in:
parent
31cf0e5f2f
commit
60418b75a4
File diff suppressed because it is too large
Load Diff
|
@ -40,13 +40,6 @@ export function setAllSettings(settings) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setCustomEmojis(emojis) {
|
||||
return {
|
||||
type: types.SET_CUSTOM_EMOJIS,
|
||||
payload: emojis
|
||||
};
|
||||
}
|
||||
|
||||
export function login() {
|
||||
return {
|
||||
type: 'LOGIN'
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import VideoPlayer from 'react-native-video-controls';
|
||||
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
import { formatAttachmentUrl } from '../lib/utils';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
safeArea: {
|
||||
flex: 1
|
||||
},
|
||||
modal: {
|
||||
margin: 0
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicator: {
|
||||
flex: 1
|
||||
}
|
||||
});
|
||||
|
||||
const Indicator = React.memo(() => (
|
||||
<ActivityIndicator style={styles.indicator} />
|
||||
));
|
||||
|
||||
const ModalContent = React.memo(({
|
||||
attachment, onClose, user, baseUrl
|
||||
}) => {
|
||||
if (attachment && attachment.image_url) {
|
||||
const url = formatAttachmentUrl(attachment.image_url, user.id, user.token, baseUrl);
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{attachment.title}</Text>
|
||||
{attachment.description ? <Text style={styles.description}>{attachment.description}</Text> : null}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<ImageViewer
|
||||
imageUrls={[{ url }]}
|
||||
onClick={onClose}
|
||||
backgroundColor='transparent'
|
||||
enableSwipeDown
|
||||
onSwipeDown={onClose}
|
||||
renderIndicator={() => null}
|
||||
renderImage={props => <FastImage {...props} />}
|
||||
loadingRender={() => <Indicator />}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
if (attachment && attachment.video_url) {
|
||||
const uri = formatAttachmentUrl(attachment.video_url, user.id, user.token, baseUrl);
|
||||
return (
|
||||
<SafeAreaView style={styles.safeArea}>
|
||||
<VideoPlayer
|
||||
source={{ uri }}
|
||||
onBack={onClose}
|
||||
disableVolume
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const FileModal = React.memo(({
|
||||
isVisible, onClose, attachment, user, baseUrl
|
||||
}) => (
|
||||
<Modal
|
||||
style={styles.modal}
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
>
|
||||
<ModalContent attachment={attachment} onClose={onClose} user={user} baseUrl={baseUrl} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
|
||||
FileModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
FileModal.displayName = 'FileModal';
|
||||
|
||||
ModalContent.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ModalContent.displayName = 'FileModalContent';
|
||||
|
||||
export default FileModal;
|
|
@ -20,9 +20,9 @@ export const CustomHeaderButtons = React.memo(props => (
|
|||
/>
|
||||
));
|
||||
|
||||
export const DrawerButton = React.memo(({ navigation, testID }) => (
|
||||
export const DrawerButton = React.memo(({ navigation, testID, ...otherProps }) => (
|
||||
<CustomHeaderButtons left>
|
||||
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} />
|
||||
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
|
||||
</CustomHeaderButtons>
|
||||
));
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
|||
import { connect } from 'react-redux';
|
||||
|
||||
import Markdown from '../message/Markdown';
|
||||
import { getCustomEmoji } from '../message/utils';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
|
@ -49,7 +50,6 @@ const styles = StyleSheet.create({
|
|||
|
||||
@connect(state => ({
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
customEmojis: state.customEmojis,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||
}))
|
||||
export default class ReplyPreview extends Component {
|
||||
|
@ -57,7 +57,6 @@ export default class ReplyPreview extends Component {
|
|||
message: PropTypes.object.isRequired,
|
||||
Message_TimeFormat: PropTypes.string.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired
|
||||
}
|
||||
|
@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
message, Message_TimeFormat, customEmojis, baseUrl, username
|
||||
message, Message_TimeFormat, baseUrl, username
|
||||
} = this.props;
|
||||
const time = moment(message.ts).format(Message_TimeFormat);
|
||||
return (
|
||||
|
@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
|
|||
<Text style={styles.username}>{message.u.username}</Text>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
|
||||
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} />
|
||||
</View>
|
||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||
</View>
|
||||
|
|
|
@ -179,6 +179,7 @@ export default class UploadModal extends Component {
|
|||
animationOut='fadeOut'
|
||||
useNativeDriver
|
||||
hideModalContentWhileAnimating
|
||||
avoidKeyboard
|
||||
>
|
||||
<View style={[styles.container, { width: width - 32 }]}>
|
||||
<View style={styles.titleContainer}>
|
||||
|
|
|
@ -0,0 +1,151 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, FlatList, StyleSheet, SafeAreaView
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Emoji from './message/Emoji';
|
||||
import I18n from '../i18n';
|
||||
import { CustomIcon } from '../lib/Icons';
|
||||
import sharedStyles from '../views/Styles';
|
||||
import { COLOR_WHITE } from '../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemContainer: {
|
||||
height: 50,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: COLOR_WHITE
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
|
||||
const Item = React.memo(({ item, user, baseUrl }) => {
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
.map(username => (username === user.username ? I18n.t('you') : username)).join(', ');
|
||||
if (count > 3) {
|
||||
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
|
||||
} else {
|
||||
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
|
||||
}
|
||||
return (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.emojiContainer}>
|
||||
<Emoji
|
||||
content={item.emoji}
|
||||
standardEmojiStyle={standardEmojiStyle}
|
||||
customEmojiStyle={customEmojiStyle}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
||||
const ModalContent = React.memo(({ message, onClose, ...props }) => {
|
||||
if (message && message.reactions) {
|
||||
return (
|
||||
<SafeAreaView style={{ flex: 1 }}>
|
||||
<Touchable onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={styles.closeButton}
|
||||
name='cross'
|
||||
size={20}
|
||||
/>
|
||||
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
<FlatList
|
||||
style={styles.listContainer}
|
||||
data={message.reactions}
|
||||
renderItem={({ item }) => <Item item={item} {...props} />}
|
||||
keyExtractor={item => item.emoji}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
const ReactionsModal = React.memo(({ isVisible, onClose, ...props }) => (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
backdropOpacity={0.8}
|
||||
onSwipeComplete={onClose}
|
||||
swipeDirection={['up', 'left', 'right', 'down']}
|
||||
>
|
||||
<ModalContent onClose={onClose} {...props} />
|
||||
</Modal>
|
||||
), (prevProps, nextProps) => prevProps.isVisible === nextProps.isVisible);
|
||||
|
||||
ReactionsModal.propTypes = {
|
||||
isVisible: PropTypes.bool,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ReactionsModal.displayName = 'ReactionsModal';
|
||||
|
||||
ModalContent.propTypes = {
|
||||
message: PropTypes.object,
|
||||
onClose: PropTypes.func
|
||||
};
|
||||
ModalContent.displayName = 'ReactionsModalContent';
|
||||
|
||||
Item.propTypes = {
|
||||
item: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Item.displayName = 'ReactionsModalItem';
|
||||
|
||||
export default ReactionsModal;
|
|
@ -0,0 +1,44 @@
|
|||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Image from './Image';
|
||||
import Audio from './Audio';
|
||||
import Video from './Video';
|
||||
import Reply from './Reply';
|
||||
|
||||
const Attachments = React.memo(({
|
||||
attachments, timeFormat, user, baseUrl, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
if (!attachments || attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attachments.map((file, index) => {
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} onOpenFileModal={onOpenFileModal} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />;
|
||||
}
|
||||
|
||||
// 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} useMarkdown={useMarkdown} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachments, nextProps.attachments));
|
||||
|
||||
Attachments.propTypes = {
|
||||
attachments: PropTypes.array,
|
||||
timeFormat: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Attachments.displayName = 'MessageAttachments';
|
||||
|
||||
export default Attachments;
|
|
@ -56,20 +56,40 @@ const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
|
|||
const BUTTON_HIT_SLOP = {
|
||||
top: 12, right: 12, bottom: 12, left: 12
|
||||
};
|
||||
const sliderAnimationConfig = {
|
||||
duration: 250,
|
||||
easing: Easing.linear,
|
||||
delay: 0
|
||||
};
|
||||
|
||||
const Button = React.memo(({ paused, onPress }) => (
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={onPress}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
<CustomIcon name={paused ? 'play' : 'pause'} size={36} style={styles.playPauseImage} />
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
Button.propTypes = {
|
||||
paused: PropTypes.bool,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
Button.displayName = 'MessageAudioButton';
|
||||
|
||||
export default class Audio extends React.Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onLoad = this.onLoad.bind(this);
|
||||
this.onProgress = this.onProgress.bind(this);
|
||||
this.onEnd = this.onEnd.bind(this);
|
||||
const { baseUrl, file, user } = props;
|
||||
this.state = {
|
||||
currentTime: 0,
|
||||
|
@ -120,22 +140,26 @@ export default class Audio extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
getDuration = () => {
|
||||
get duration() {
|
||||
const { duration } = this.state;
|
||||
return formatTime(duration);
|
||||
}
|
||||
|
||||
setRef = ref => this.player = ref;
|
||||
|
||||
togglePlayPause = () => {
|
||||
const { paused } = this.state;
|
||||
this.setState({ paused: !paused });
|
||||
}
|
||||
|
||||
onValueChange = value => this.setState({ currentTime: value });
|
||||
|
||||
render() {
|
||||
const {
|
||||
uri, paused, currentTime, duration
|
||||
} = this.state;
|
||||
const {
|
||||
user, baseUrl, customEmojis, file
|
||||
user, baseUrl, file, getCustomEmoji, useMarkdown
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
|
||||
|
@ -144,12 +168,10 @@ export default class Audio extends React.Component {
|
|||
}
|
||||
|
||||
return (
|
||||
[
|
||||
<View key='audio' style={styles.audioContainer}>
|
||||
<React.Fragment>
|
||||
<View style={styles.audioContainer}>
|
||||
<Video
|
||||
ref={(ref) => {
|
||||
this.player = ref;
|
||||
}}
|
||||
ref={this.setRef}
|
||||
source={{ uri }}
|
||||
onLoad={this.onLoad}
|
||||
onProgress={this.onProgress}
|
||||
|
@ -157,39 +179,24 @@ export default class Audio extends React.Component {
|
|||
paused={paused}
|
||||
repeat={false}
|
||||
/>
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={this.togglePlayPause}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{
|
||||
paused
|
||||
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
|
||||
}
|
||||
</Touchable>
|
||||
<Button paused={paused} onPress={this.togglePlayPause} />
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
maximumValue={duration}
|
||||
minimumValue={0}
|
||||
animateTransitions
|
||||
animationConfig={{
|
||||
duration: 250,
|
||||
easing: Easing.linear,
|
||||
delay: 0
|
||||
}}
|
||||
animationConfig={sliderAnimationConfig}
|
||||
thumbTintColor={COLOR_PRIMARY}
|
||||
minimumTrackTintColor={COLOR_PRIMARY}
|
||||
onValueChange={value => this.setState({ currentTime: value })}
|
||||
onValueChange={this.onValueChange}
|
||||
thumbStyle={styles.thumbStyle}
|
||||
trackStyle={styles.trackStyle}
|
||||
/>
|
||||
<Text style={styles.duration}>{this.getDuration()}</Text>
|
||||
</View>,
|
||||
<Markdown key='description' msg={description} baseUrl={baseUrl} customEmojis={customEmojis} username={user.username} />
|
||||
]
|
||||
<Text style={styles.duration}>{this.duration}</Text>
|
||||
</View>
|
||||
<Markdown msg={description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
const Broadcast = React.memo(({
|
||||
author, user, broadcast, replyBroadcast
|
||||
}) => {
|
||||
const isOwn = author._id === user.id;
|
||||
if (broadcast && !isOwn) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, () => true);
|
||||
|
||||
Broadcast.propTypes = {
|
||||
author: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
broadcast: PropTypes.bool,
|
||||
replyBroadcast: PropTypes.func
|
||||
};
|
||||
Broadcast.displayName = 'MessageBroadcast';
|
||||
|
||||
export default Broadcast;
|
|
@ -0,0 +1,48 @@
|
|||
import React from 'react';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import styles from './styles';
|
||||
import Markdown from './Markdown';
|
||||
import { getInfoMessage } from './utils';
|
||||
|
||||
const Content = React.memo((props) => {
|
||||
if (props.isInfo) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...props })}</Text>;
|
||||
}
|
||||
|
||||
if (props.tmid && !props.msg) {
|
||||
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
msg={props.msg}
|
||||
baseUrl={props.baseUrl}
|
||||
username={props.user.username}
|
||||
isEdited={props.isEdited}
|
||||
mentions={props.mentions}
|
||||
channels={props.channels}
|
||||
numberOfLines={props.tmid ? 1 : 0}
|
||||
getCustomEmoji={props.getCustomEmoji}
|
||||
useMarkdown={props.useMarkdown}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.msg === nextProps.msg);
|
||||
|
||||
Content.propTypes = {
|
||||
isInfo: PropTypes.bool,
|
||||
isEdited: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
tmid: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Content.displayName = 'MessageContent';
|
||||
|
||||
export default Content;
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatLastMessage, formatMessageCount, BUTTON_HIT_SLOP } from './utils';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { DISCUSSION } from './constants';
|
||||
|
||||
const Discussion = React.memo(({
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
}) => {
|
||||
const time = formatLastMessage(dlm);
|
||||
const buttonText = formatMessageCount(dcount, DISCUSSION);
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.msg !== nextProps.msg) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.dcount !== nextProps.dcount) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.dlm !== nextProps.dlm) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Discussion.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.string,
|
||||
onDiscussionPress: PropTypes.func
|
||||
};
|
||||
Discussion.displayName = 'MessageDiscussion';
|
||||
|
||||
export default Discussion;
|
|
@ -1,31 +1,28 @@
|
|||
import React from 'react';
|
||||
import { Text, ViewPropTypes } from 'react-native';
|
||||
import { Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import { emojify } from 'react-emojione';
|
||||
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
|
||||
export default class Emoji extends React.PureComponent {
|
||||
static propTypes = {
|
||||
content: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
standardEmojiStyle: Text.propTypes.style,
|
||||
customEmojiStyle: ViewPropTypes.style,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
const Emoji = React.memo(({
|
||||
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
|
||||
}) => {
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emoji = getCustomEmoji(parsedContent);
|
||||
if (emoji) {
|
||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||
}
|
||||
return <Text style={standardEmojiStyle}>{ emojify(content, { output: 'unicode' }) }</Text>;
|
||||
}, () => true);
|
||||
|
||||
render() {
|
||||
const {
|
||||
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const parsedContent = content.replace(/^:|:$/g, '');
|
||||
const emojiExtension = customEmojis[parsedContent];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content: parsedContent };
|
||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||
}
|
||||
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
|
||||
}
|
||||
}
|
||||
Emoji.propTypes = {
|
||||
content: PropTypes.string,
|
||||
standardEmojiStyle: PropTypes.object,
|
||||
customEmojiStyle: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Emoji.displayName = 'MessageEmoji';
|
||||
|
||||
export default Emoji;
|
||||
|
|
|
@ -1,95 +1,79 @@
|
|||
import React, { Component } from 'react';
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import PhotoModal from './PhotoModal';
|
||||
import Markdown from './Markdown';
|
||||
import styles from './styles';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
|
||||
export default class extends Component {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
const Button = React.memo(({ children, onPress }) => (
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
{children}
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Image = React.memo(({ img }) => (
|
||||
<FastImage
|
||||
style={styles.image}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
));
|
||||
|
||||
const ImageContainer = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
const img = formatAttachmentUrl(file.image_url, user.id, user.token, baseUrl);
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
|
||||
state = { modalVisible: false, isPressed: false };
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { modalVisible, isPressed } = this.state;
|
||||
const { file } = this.props;
|
||||
if (nextState.modalVisible !== modalVisible) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.isPressed !== isPressed) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextProps.file, file)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
onPressButton = () => {
|
||||
this.setState({
|
||||
modalVisible: true
|
||||
});
|
||||
}
|
||||
|
||||
getDescription() {
|
||||
const {
|
||||
file, customEmojis, baseUrl, user
|
||||
} = this.props;
|
||||
if (file.description) {
|
||||
return <Markdown msg={file.description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />;
|
||||
}
|
||||
}
|
||||
|
||||
isPressed = (state) => {
|
||||
this.setState({ isPressed: state });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { modalVisible, isPressed } = this.state;
|
||||
const { baseUrl, file, user } = this.props;
|
||||
const img = file.image_url.includes('http') ? file.image_url : `${ baseUrl }${ file.image_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
|
||||
if (!img) {
|
||||
return null;
|
||||
}
|
||||
const onPress = () => onOpenFileModal(file);
|
||||
|
||||
if (file.description) {
|
||||
return (
|
||||
[
|
||||
<Touchable
|
||||
key='image'
|
||||
onPress={this.onPressButton}
|
||||
style={styles.imageContainer}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<React.Fragment>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</React.Fragment>
|
||||
</Touchable>,
|
||||
<PhotoModal
|
||||
key='modal'
|
||||
title={file.title}
|
||||
description={file.description}
|
||||
image={img}
|
||||
isVisible={modalVisible}
|
||||
onClose={() => this.setState({ modalVisible: false })}
|
||||
/>
|
||||
]
|
||||
<Button onPress={onPress}>
|
||||
<View>
|
||||
<Image img={img} />
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</View>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Button onPress={onPress}>
|
||||
<Image img={img} />
|
||||
</Button>
|
||||
);
|
||||
}, (prevProps, nextProps) => equal(prevProps.file, nextProps.file));
|
||||
|
||||
ImageContainer.propTypes = {
|
||||
file: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImageContainer';
|
||||
|
||||
Image.propTypes = {
|
||||
img: PropTypes.string
|
||||
};
|
||||
ImageContainer.displayName = 'MessageImage';
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.node,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
ImageContainer.displayName = 'MessageButton';
|
||||
|
||||
export default ImageContainer;
|
||||
|
|
|
@ -4,9 +4,15 @@ import PropTypes from 'prop-types';
|
|||
import { emojify } from 'react-emojione';
|
||||
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
|
||||
import MarkdownFlowdock from 'markdown-it-flowdock';
|
||||
|
||||
import styles from './styles';
|
||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
|
||||
import I18n from '../../i18n';
|
||||
|
||||
const EmojiPlugin = new PluginContainer(MarkdownEmojiPlugin);
|
||||
const MentionsPlugin = new PluginContainer(MarkdownFlowdock);
|
||||
const plugins = [EmojiPlugin, MentionsPlugin];
|
||||
|
||||
// Support <http://link|Text>
|
||||
const formatText = text => text.replace(
|
||||
|
@ -15,7 +21,7 @@ const formatText = text => text.replace(
|
|||
);
|
||||
|
||||
const Markdown = React.memo(({
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
|
||||
msg, style, rules, baseUrl, username, isEdited, numberOfLines, mentions, channels, getCustomEmoji, useMarkdown = true
|
||||
}) => {
|
||||
if (!msg) {
|
||||
return null;
|
||||
|
@ -28,14 +34,18 @@ const Markdown = React.memo(({
|
|||
if (numberOfLines > 0) {
|
||||
m = m.replace(/[\n]+/g, '\n').trim();
|
||||
}
|
||||
|
||||
if (!useMarkdown) {
|
||||
return <Text style={styles.text}>{m}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
rules={{
|
||||
paragraph: (node, children) => (
|
||||
// eslint-disable-next-line
|
||||
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
||||
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
|
||||
</Text>
|
||||
),
|
||||
mention: (node) => {
|
||||
|
@ -52,23 +62,31 @@ const Markdown = React.memo(({
|
|||
...styles.mentionLoggedUser
|
||||
};
|
||||
}
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
|
||||
return (
|
||||
<Text style={mentionStyle} key={key}>
|
||||
{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return `@${ content }`;
|
||||
},
|
||||
hashtag: (node) => {
|
||||
const { content, key } = node;
|
||||
if (channels && channels.length && channels.findIndex(channel => channel.name === content) !== -1) {
|
||||
return (
|
||||
<Text key={key} style={styles.mention}>
|
||||
#{content}
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
return `#${ content }`;
|
||||
},
|
||||
hashtag: node => (
|
||||
<Text key={node.key} style={styles.mention}>
|
||||
#{node.content}
|
||||
</Text>
|
||||
),
|
||||
emoji: (node) => {
|
||||
if (node.children && node.children.length && node.children[0].content) {
|
||||
const { content } = node.children[0];
|
||||
const emojiExtension = customEmojis[content];
|
||||
if (emojiExtension) {
|
||||
const emoji = { extension: emojiExtension, content };
|
||||
const emoji = getCustomEmoji && getCustomEmoji(content);
|
||||
if (emoji) {
|
||||
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
||||
}
|
||||
return <Text key={node.key}>:{content}:</Text>;
|
||||
|
@ -90,10 +108,7 @@ const Markdown = React.memo(({
|
|||
link: styles.link,
|
||||
...style
|
||||
}}
|
||||
plugins={[
|
||||
new PluginContainer(MarkdownFlowdock),
|
||||
new PluginContainer(MarkdownEmojiPlugin)
|
||||
]}
|
||||
plugins={plugins}
|
||||
>{m}
|
||||
</MarkdownRenderer>
|
||||
);
|
||||
|
@ -101,13 +116,17 @@ const Markdown = React.memo(({
|
|||
|
||||
Markdown.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
username: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
username: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
style: PropTypes.any,
|
||||
rules: PropTypes.object,
|
||||
edited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number
|
||||
isEdited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number,
|
||||
useMarkdown: PropTypes.bool,
|
||||
mentions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
channels: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Markdown.displayName = 'MessageMarkdown';
|
||||
|
||||
export default Markdown;
|
||||
|
|
|
@ -1,609 +1,133 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
View, Text, ViewPropTypes, TouchableWithoutFeedback
|
||||
} from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
import { View } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { emojify } from 'react-emojione';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
|
||||
import Image from './Image';
|
||||
import User from './User';
|
||||
import Avatar from '../Avatar';
|
||||
import Audio from './Audio';
|
||||
import Video from './Video';
|
||||
import Markdown from './Markdown';
|
||||
import Url from './Url';
|
||||
import Reply from './Reply';
|
||||
import ReactionsModal from './ReactionsModal';
|
||||
import Emoji from './Emoji';
|
||||
import MessageError from './MessageError';
|
||||
import styles from './styles';
|
||||
import I18n from '../../i18n';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import debounce from '../../utils/debounce';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import RepliedThread from './RepliedThread';
|
||||
import MessageAvatar from './MessageAvatar';
|
||||
import Attachments from './Attachments';
|
||||
import Urls from './Urls';
|
||||
import Thread from './Thread';
|
||||
import Reactions from './Reactions';
|
||||
import Broadcast from './Broadcast';
|
||||
import Discussion from './Discussion';
|
||||
import Content from './Content';
|
||||
|
||||
const SYSTEM_MESSAGES = [
|
||||
'r',
|
||||
'au',
|
||||
'ru',
|
||||
'ul',
|
||||
'uj',
|
||||
'ut',
|
||||
'rm',
|
||||
'user-muted',
|
||||
'user-unmuted',
|
||||
'message_pinned',
|
||||
'subscription-role-added',
|
||||
'subscription-role-removed',
|
||||
'room_changed_description',
|
||||
'room_changed_announcement',
|
||||
'room_changed_topic',
|
||||
'room_changed_privacy',
|
||||
'message_snippeted',
|
||||
'thread-created'
|
||||
];
|
||||
|
||||
const getInfoMessage = ({
|
||||
type, role, msg, author
|
||||
}) => {
|
||||
const { username } = author;
|
||||
if (type === 'rm') {
|
||||
return I18n.t('Message_removed');
|
||||
} else if (type === 'uj') {
|
||||
return I18n.t('Has_joined_the_channel');
|
||||
} else if (type === 'ut') {
|
||||
return I18n.t('Has_joined_the_conversation');
|
||||
} else if (type === 'r') {
|
||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||
} else if (type === 'message_pinned') {
|
||||
return I18n.t('Message_pinned');
|
||||
} else if (type === 'ul') {
|
||||
return I18n.t('Has_left_the_channel');
|
||||
} else if (type === 'ru') {
|
||||
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
|
||||
} else if (type === 'au') {
|
||||
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
|
||||
} else if (type === 'user-muted') {
|
||||
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
|
||||
} else if (type === 'user-unmuted') {
|
||||
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
|
||||
} else if (type === 'subscription-role-added') {
|
||||
return `${ msg } was set ${ role } by ${ username }`;
|
||||
} else if (type === 'subscription-role-removed') {
|
||||
return `${ msg } is no longer ${ role } by ${ username }`;
|
||||
} else if (type === 'room_changed_description') {
|
||||
return I18n.t('Room_changed_description', { description: msg, userBy: username });
|
||||
} else if (type === 'room_changed_announcement') {
|
||||
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
|
||||
} else if (type === 'room_changed_topic') {
|
||||
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
|
||||
} else if (type === 'room_changed_privacy') {
|
||||
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
|
||||
} else if (type === 'message_snippeted') {
|
||||
return I18n.t('Created_snippet');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
const BUTTON_HIT_SLOP = {
|
||||
top: 4, right: 4, bottom: 4, left: 4
|
||||
};
|
||||
|
||||
export default class Message extends PureComponent {
|
||||
static propTypes = {
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
msg: PropTypes.string,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
author: PropTypes.shape({
|
||||
_id: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
name: PropTypes.string
|
||||
}),
|
||||
status: PropTypes.any,
|
||||
reactions: PropTypes.any,
|
||||
editing: PropTypes.bool,
|
||||
style: ViewPropTypes.style,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
reactionsModal: PropTypes.bool,
|
||||
type: PropTypes.string,
|
||||
header: PropTypes.bool,
|
||||
isThreadReply: PropTypes.bool,
|
||||
isThreadSequential: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string
|
||||
]),
|
||||
edited: PropTypes.bool,
|
||||
attachments: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]),
|
||||
urls: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
]),
|
||||
useRealName: PropTypes.bool,
|
||||
dcount: PropTypes.number,
|
||||
dlm: PropTypes.instanceOf(Date),
|
||||
tmid: PropTypes.string,
|
||||
tcount: PropTypes.number,
|
||||
tlm: PropTypes.instanceOf(Date),
|
||||
tmsg: PropTypes.string,
|
||||
// methods
|
||||
closeReactions: PropTypes.func,
|
||||
onErrorPress: PropTypes.func,
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
onThreadPress: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
archived: false,
|
||||
broadcast: false,
|
||||
attachments: [],
|
||||
urls: [],
|
||||
reactions: [],
|
||||
onLongPress: () => {}
|
||||
}
|
||||
|
||||
onPress = debounce(() => {
|
||||
KeyboardUtils.dismiss();
|
||||
|
||||
const { onThreadPress, tlm, tmid } = this.props;
|
||||
if ((tlm || tmid) && onThreadPress) {
|
||||
onThreadPress();
|
||||
}
|
||||
}, 300, true)
|
||||
|
||||
onLongPress = () => {
|
||||
const { archived, onLongPress } = this.props;
|
||||
if (this.isInfoMessage() || this.hasError() || archived) {
|
||||
return;
|
||||
}
|
||||
onLongPress();
|
||||
}
|
||||
|
||||
formatLastMessage = (lm) => {
|
||||
const { customThreadTimeFormat } = this.props;
|
||||
if (customThreadTimeFormat) {
|
||||
return moment(lm).format(customThreadTimeFormat);
|
||||
}
|
||||
return lm ? moment(lm).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
}) : null;
|
||||
}
|
||||
|
||||
formatMessageCount = (count, type) => {
|
||||
const discussion = type === 'discussion';
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (count === 1) {
|
||||
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
} else if (count > 999) {
|
||||
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
isInfoMessage = () => {
|
||||
const { type } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(type);
|
||||
}
|
||||
|
||||
isOwn = () => {
|
||||
const { author, user } = this.props;
|
||||
return author._id === user.id;
|
||||
}
|
||||
|
||||
isDeleted() {
|
||||
const { type } = this.props;
|
||||
return type === 'rm';
|
||||
}
|
||||
|
||||
isTemp() {
|
||||
const { status } = this.props;
|
||||
return status === messagesStatus.TEMP || status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
hasError() {
|
||||
const { status } = this.props;
|
||||
return status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
renderAvatar = (small = false) => {
|
||||
const {
|
||||
header, avatar, author, baseUrl, user
|
||||
} = this.props;
|
||||
if (header) {
|
||||
return (
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderUsername = () => {
|
||||
const {
|
||||
header, timeFormat, author, alias, ts, useRealName
|
||||
} = this.props;
|
||||
if (header) {
|
||||
return (
|
||||
<User
|
||||
onPress={this.onPress}
|
||||
timeFormat={timeFormat}
|
||||
username={(useRealName && author.name) || author.username}
|
||||
alias={alias}
|
||||
ts={ts}
|
||||
temp={this.isTemp()}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
if (this.isInfoMessage()) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
|
||||
}
|
||||
|
||||
const {
|
||||
customEmojis, msg, baseUrl, user, edited, tmid
|
||||
} = this.props;
|
||||
|
||||
if (tmid && !msg) {
|
||||
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Markdown
|
||||
msg={msg}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
edited={edited}
|
||||
numberOfLines={tmid ? 1 : 0}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
renderAttachment() {
|
||||
const { attachments, timeFormat } = this.props;
|
||||
|
||||
if (attachments.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return attachments.map((file, index) => {
|
||||
const { user, baseUrl, customEmojis } = this.props;
|
||||
if (file.image_url) {
|
||||
return <Image key={file.image_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
if (file.audio_url) {
|
||||
return <Audio key={file.audio_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
if (file.video_url) {
|
||||
return <Video key={file.video_url} file={file} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
return <Reply key={index} index={index} attachment={file} timeFormat={timeFormat} user={user} baseUrl={baseUrl} customEmojis={customEmojis} />;
|
||||
});
|
||||
}
|
||||
|
||||
renderUrl = () => {
|
||||
const { urls, user, baseUrl } = this.props;
|
||||
if (urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
));
|
||||
}
|
||||
|
||||
renderError = () => {
|
||||
if (!this.hasError()) {
|
||||
return null;
|
||||
}
|
||||
const { onErrorPress } = this.props;
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
renderReaction = (reaction) => {
|
||||
const {
|
||||
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
renderReactions() {
|
||||
const { reactions, toggleReactionPicker } = this.props;
|
||||
if (reactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{reactions.map(this.renderReaction)}
|
||||
<Touchable
|
||||
onPress={toggleReactionPicker}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
</View>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderBroadcastReply() {
|
||||
const { broadcast, replyBroadcast } = this.props;
|
||||
if (broadcast && !this.isOwn()) {
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderDiscussion = () => {
|
||||
const {
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
} = this.props;
|
||||
const time = this.formatLastMessage(dlm);
|
||||
const buttonText = this.formatMessageCount(dcount, 'discussion');
|
||||
const MessageInner = React.memo((props) => {
|
||||
if (props.type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
||||
<Text style={styles.text}>{msg}</Text>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onDiscussionPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='chat' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
<User {...props} />
|
||||
<Discussion {...props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
<User {...props} />
|
||||
<Content {...props} />
|
||||
<Attachments {...props} />
|
||||
<Urls {...props} />
|
||||
<Thread {...props} />
|
||||
<Reactions {...props} />
|
||||
<Broadcast {...props} />
|
||||
</React.Fragment>
|
||||
);
|
||||
});
|
||||
MessageInner.displayName = 'MessageInner';
|
||||
|
||||
renderThread = () => {
|
||||
const {
|
||||
tcount, tlm, onThreadPress, msg
|
||||
} = this.props;
|
||||
|
||||
if (!tlm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = this.formatLastMessage(tlm);
|
||||
const buttonText = this.formatMessageCount(tcount, 'thread');
|
||||
const Message = React.memo((props) => {
|
||||
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
|
||||
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={onThreadPress}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={[styles.button, styles.smallButton]}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderRepliedThread = () => {
|
||||
const {
|
||||
tmid, tmsg, header, fetchThreadName
|
||||
} = this.props;
|
||||
if (!tmid || !header || this.isTemp()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tmsg) {
|
||||
fetchThreadName(tmid);
|
||||
return null;
|
||||
}
|
||||
|
||||
let msg = emojify(tmsg, { output: 'unicode' });
|
||||
msg = removeMarkdown(msg);
|
||||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderInner = () => {
|
||||
const { type } = this.props;
|
||||
if (type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderDiscussion()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderContent()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderUrl()}
|
||||
{this.renderThread()}
|
||||
{this.renderReactions()}
|
||||
{this.renderBroadcastReply()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
renderMessage = () => {
|
||||
const { header, isThreadReply, isThreadSequential } = this.props;
|
||||
|
||||
if (isThreadReply || isThreadSequential || this.isInfoMessage()) {
|
||||
const thread = isThreadReply ? this.renderRepliedThread() : null;
|
||||
return (
|
||||
<React.Fragment>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
{this.renderAvatar(true)}
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
]}
|
||||
>
|
||||
{this.renderContent()}
|
||||
</View>
|
||||
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||
{thread}
|
||||
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||
<MessageAvatar small {...props} />
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
props.isHeader && styles.messageContentWithHeader,
|
||||
props.hasError && props.isHeader && styles.messageContentWithHeader,
|
||||
props.hasError && !props.isHeader && styles.messageContentWithError
|
||||
]}
|
||||
>
|
||||
<Content {...props} />
|
||||
</View>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||
<View style={styles.flex}>
|
||||
{this.renderAvatar()}
|
||||
<MessageAvatar {...props} />
|
||||
<View
|
||||
style={[
|
||||
styles.messageContent,
|
||||
header && styles.messageContentWithHeader,
|
||||
this.hasError() && header && styles.messageContentWithHeader,
|
||||
this.hasError() && !header && styles.messageContentWithError,
|
||||
this.isTemp() && styles.temp
|
||||
props.isHeader && styles.messageContentWithHeader,
|
||||
props.hasError && props.isHeader && styles.messageContentWithHeader,
|
||||
props.hasError && !props.isHeader && styles.messageContentWithError
|
||||
]}
|
||||
>
|
||||
{this.renderInner()}
|
||||
<MessageInner {...props} />
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
editing, style, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
|
||||
} = this.props;
|
||||
const accessibilityLabel = I18n.t('Message_accessibility', { user: author.username, time: moment(ts).format(timeFormat), message: msg });
|
||||
</View>
|
||||
);
|
||||
});
|
||||
Message.displayName = 'Message';
|
||||
|
||||
const MessageTouchable = React.memo((props) => {
|
||||
if (props.hasError) {
|
||||
return (
|
||||
<View style={styles.root}>
|
||||
{this.renderError()}
|
||||
<TouchableWithoutFeedback
|
||||
onLongPress={this.onLongPress}
|
||||
onPress={this.onPress}
|
||||
>
|
||||
<View
|
||||
style={[styles.container, editing && styles.editing, style]}
|
||||
accessibilityLabel={accessibilityLabel}
|
||||
>
|
||||
{this.renderMessage()}
|
||||
{reactionsModal
|
||||
? (
|
||||
<ReactionsModal
|
||||
isVisible={reactionsModal}
|
||||
reactions={reactions}
|
||||
user={user}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
close={closeReactions}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
}
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<MessageError {...props} />
|
||||
<Message {...props} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Touchable
|
||||
onLongPress={props.onLongPress}
|
||||
onPress={props.onPress}
|
||||
disabled={props.isInfo || props.archived || props.isTemp}
|
||||
>
|
||||
<View>
|
||||
<Message {...props} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
});
|
||||
MessageTouchable.displayName = 'MessageTouchable';
|
||||
|
||||
MessageTouchable.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
archived: PropTypes.bool,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
Message.propTypes = {
|
||||
isThreadReply: PropTypes.bool,
|
||||
isThreadSequential: PropTypes.bool,
|
||||
isInfo: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
isHeader: PropTypes.bool,
|
||||
hasError: PropTypes.bool,
|
||||
style: PropTypes.any,
|
||||
onLongPress: PropTypes.func,
|
||||
onPress: PropTypes.func
|
||||
};
|
||||
|
||||
MessageInner.propTypes = {
|
||||
type: PropTypes.string
|
||||
};
|
||||
|
||||
export default MessageTouchable;
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import Avatar from '../Avatar';
|
||||
import styles from './styles';
|
||||
|
||||
const MessageAvatar = React.memo(({
|
||||
isHeader, avatar, author, baseUrl, user, small
|
||||
}) => {
|
||||
if (isHeader) {
|
||||
return (
|
||||
<Avatar
|
||||
style={small ? styles.avatarSmall : styles.avatar}
|
||||
text={avatar ? '' : author.username}
|
||||
size={small ? 20 : 36}
|
||||
borderRadius={small ? 2 : 4}
|
||||
avatar={avatar}
|
||||
baseUrl={baseUrl}
|
||||
userId={user.id}
|
||||
token={user.token}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}, () => true);
|
||||
|
||||
MessageAvatar.propTypes = {
|
||||
isHeader: PropTypes.bool,
|
||||
avatar: PropTypes.string,
|
||||
author: PropTypes.obj,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.obj,
|
||||
small: PropTypes.bool
|
||||
};
|
||||
MessageAvatar.displayName = 'MessageAvatar';
|
||||
|
||||
export default MessageAvatar;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { COLOR_DANGER } from '../../constants/colors';
|
||||
import styles from './styles';
|
||||
|
||||
const MessageError = React.memo(({ hasError, onErrorPress }) => {
|
||||
if (!hasError) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</Touchable>
|
||||
);
|
||||
}, (prevProps, nextProps) => prevProps.hasError === nextProps.hasError);
|
||||
|
||||
MessageError.propTypes = {
|
||||
hasError: PropTypes.bool,
|
||||
onErrorPress: PropTypes.func
|
||||
};
|
||||
MessageError.displayName = 'MessageError';
|
||||
|
||||
export default MessageError;
|
|
@ -1,94 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, ActivityIndicator, StyleSheet
|
||||
} from 'react-native';
|
||||
import FastImage from 'react-native-fast-image';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
import ImageViewer from 'react-native-image-zoom-viewer';
|
||||
import { responsive } from 'react-native-responsive-ui';
|
||||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
imageWrapper: {
|
||||
flex: 1
|
||||
},
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
marginVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
description: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
indicatorContainer: {
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
const margin = 40;
|
||||
|
||||
@responsive
|
||||
export default class PhotoModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
image: PropTypes.string.isRequired,
|
||||
isVisible: PropTypes.bool,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
window: PropTypes.object
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
image, isVisible, onClose, title, description, window: { width, height }
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
style={{ alignItems: 'center' }}
|
||||
onBackdropPress={onClose}
|
||||
onBackButtonPress={onClose}
|
||||
animationIn='fadeIn'
|
||||
animationOut='fadeOut'
|
||||
>
|
||||
<View style={{ width: width - margin, height: height - margin }}>
|
||||
<TouchableWithoutFeedback onPress={onClose}>
|
||||
<View style={styles.titleContainer}>
|
||||
<Text style={styles.title}>{title}</Text>
|
||||
<Text style={styles.description}>{description}</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={styles.imageWrapper}>
|
||||
<ImageViewer
|
||||
imageUrls={[{ url: encodeURI(image) }]}
|
||||
onClick={onClose}
|
||||
backgroundColor='transparent'
|
||||
enableSwipeDown
|
||||
onSwipeDown={onClose}
|
||||
renderIndicator={() => {}}
|
||||
renderImage={props => <FastImage {...props} />}
|
||||
loadingRender={() => (
|
||||
<View style={[styles.indicatorContainer, { width, height }]}>
|
||||
<ActivityIndicator />
|
||||
</View>
|
||||
)}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import styles from './styles';
|
||||
import Emoji from './Emoji';
|
||||
import { BUTTON_HIT_SLOP } from './utils';
|
||||
|
||||
const AddReaction = React.memo(({ toggleReactionPicker }) => (
|
||||
<Touchable
|
||||
onPress={toggleReactionPicker}
|
||||
key='message-add-reaction'
|
||||
testID='message-add-reaction'
|
||||
style={styles.reactionButton}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={styles.reactionContainer}>
|
||||
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
|
||||
</View>
|
||||
</Touchable>
|
||||
));
|
||||
|
||||
const Reaction = React.memo(({
|
||||
reaction, user, onReactionLongPress, onReactionPress, baseUrl, getCustomEmoji
|
||||
}) => {
|
||||
const reacted = reaction.usernames.findIndex(item => item === user.username) !== -1;
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}, () => true);
|
||||
|
||||
const Reactions = React.memo(({
|
||||
reactions, user, baseUrl, onReactionPress, toggleReactionPicker, onReactionLongPress, getCustomEmoji
|
||||
}) => {
|
||||
if (!reactions || reactions.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.reactionsContainer}>
|
||||
{reactions.map(reaction => (
|
||||
<Reaction
|
||||
key={reaction.emoji}
|
||||
reaction={reaction}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
onReactionLongPress={onReactionLongPress}
|
||||
onReactionPress={onReactionPress}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
))}
|
||||
<AddReaction toggleReactionPicker={toggleReactionPicker} />
|
||||
</View>
|
||||
);
|
||||
});
|
||||
// FIXME: can't compare because it's a Realm object (it may be fixed by JSON.parse(JSON.stringify(reactions)))
|
||||
|
||||
Reaction.propTypes = {
|
||||
reaction: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reaction.displayName = 'MessageReaction';
|
||||
|
||||
Reactions.propTypes = {
|
||||
reactions: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
onReactionPress: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reactions.displayName = 'MessageReactions';
|
||||
|
||||
AddReaction.propTypes = {
|
||||
toggleReactionPicker: PropTypes.func
|
||||
};
|
||||
AddReaction.displayName = 'MessageAddReaction';
|
||||
|
||||
export default Reactions;
|
|
@ -1,140 +0,0 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
View, Text, TouchableWithoutFeedback, FlatList, StyleSheet
|
||||
} from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Modal from 'react-native-modal';
|
||||
|
||||
import Emoji from './Emoji';
|
||||
import I18n from '../../i18n';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
titleContainer: {
|
||||
width: '100%',
|
||||
alignItems: 'center',
|
||||
paddingVertical: 10
|
||||
},
|
||||
title: {
|
||||
color: COLOR_WHITE,
|
||||
textAlign: 'center',
|
||||
fontSize: 16,
|
||||
...sharedStyles.textSemibold
|
||||
},
|
||||
reactCount: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
peopleReacted: {
|
||||
color: COLOR_WHITE,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textMedium
|
||||
},
|
||||
peopleItemContainer: {
|
||||
flex: 1,
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
emojiContainer: {
|
||||
width: 50,
|
||||
height: 50,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
itemContainer: {
|
||||
height: 50,
|
||||
flexDirection: 'row'
|
||||
},
|
||||
listContainer: {
|
||||
flex: 1
|
||||
},
|
||||
closeButton: {
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 10,
|
||||
color: COLOR_WHITE
|
||||
}
|
||||
});
|
||||
const standardEmojiStyle = { fontSize: 20 };
|
||||
const customEmojiStyle = { width: 20, height: 20 };
|
||||
|
||||
export default class ReactionsModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
isVisible: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
reactions: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.oneOfType([
|
||||
PropTypes.array,
|
||||
PropTypes.object
|
||||
])
|
||||
}
|
||||
|
||||
renderItem = (item) => {
|
||||
const { user, customEmojis, baseUrl } = this.props;
|
||||
const count = item.usernames.length;
|
||||
let usernames = item.usernames.slice(0, 3)
|
||||
.map(username => (username.value === user.username ? I18n.t('you') : username.value)).join(', ');
|
||||
if (count > 3) {
|
||||
usernames = `${ usernames } ${ I18n.t('and_more') } ${ count - 3 }`;
|
||||
} else {
|
||||
usernames = usernames.replace(/,(?=[^,]*$)/, ` ${ I18n.t('and') }`);
|
||||
}
|
||||
return (
|
||||
<View style={styles.itemContainer}>
|
||||
<View style={styles.emojiContainer}>
|
||||
<Emoji
|
||||
content={item.emoji}
|
||||
standardEmojiStyle={standardEmojiStyle}
|
||||
customEmojiStyle={customEmojiStyle}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</View>
|
||||
<View style={styles.peopleItemContainer}>
|
||||
<Text style={styles.reactCount}>
|
||||
{count === 1 ? I18n.t('1_person_reacted') : I18n.t('N_people_reacted', { n: count })}
|
||||
</Text>
|
||||
<Text style={styles.peopleReacted}>{ usernames }</Text>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
isVisible, close, reactions
|
||||
} = this.props;
|
||||
return (
|
||||
<Modal
|
||||
isVisible={isVisible}
|
||||
onBackdropPress={close}
|
||||
onBackButtonPress={close}
|
||||
backdropOpacity={0.9}
|
||||
>
|
||||
<TouchableWithoutFeedback onPress={close}>
|
||||
<View style={styles.titleContainer}>
|
||||
<CustomIcon
|
||||
style={styles.closeButton}
|
||||
name='cross'
|
||||
size={20}
|
||||
onPress={close}
|
||||
/>
|
||||
<Text style={styles.title}>{I18n.t('Reactions')}</Text>
|
||||
</View>
|
||||
</TouchableWithoutFeedback>
|
||||
<View style={styles.listContainer}>
|
||||
<FlatList
|
||||
data={reactions}
|
||||
renderItem={({ item }) => this.renderItem(item)}
|
||||
keyExtractor={item => item.emoji}
|
||||
/>
|
||||
</View>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import removeMarkdown from 'remove-markdown';
|
||||
import { emojify } from 'react-emojione';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import DisclosureIndicator from '../DisclosureIndicator';
|
||||
import styles from './styles';
|
||||
|
||||
const RepliedThread = React.memo(({
|
||||
tmid, tmsg, isHeader, isTemp, fetchThreadName
|
||||
}) => {
|
||||
if (!tmid || !isHeader || isTemp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!tmsg) {
|
||||
fetchThreadName(tmid);
|
||||
return null;
|
||||
}
|
||||
|
||||
let msg = emojify(tmsg, { output: 'unicode' });
|
||||
msg = removeMarkdown(msg);
|
||||
|
||||
return (
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={styles.repliedThreadIcon} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
<DisclosureIndicator />
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.tmid !== nextProps.tmid) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.tmsg !== nextProps.tmsg) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isHeader !== nextProps.isHeader) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.isTemp !== nextProps.isTemp) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
RepliedThread.propTypes = {
|
||||
tmid: PropTypes.string,
|
||||
tmsg: PropTypes.string,
|
||||
isHeader: PropTypes.bool,
|
||||
isTemp: PropTypes.bool,
|
||||
fetchThreadName: PropTypes.func
|
||||
};
|
||||
RepliedThread.displayName = 'MessageRepliedThread';
|
||||
|
||||
export default RepliedThread;
|
|
@ -3,6 +3,7 @@ import { View, Text, StyleSheet } from 'react-native';
|
|||
import PropTypes from 'prop-types';
|
||||
import moment from 'moment';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
|
@ -69,98 +70,130 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
const onPress = (attachment, baseUrl, user) => {
|
||||
let url = attachment.title_link || attachment.author_link;
|
||||
if (!url) {
|
||||
return;
|
||||
const Title = React.memo(({ attachment, timeFormat }) => {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
}
|
||||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
openLink(url);
|
||||
};
|
||||
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null}
|
||||
{time ? <Text style={styles.time}>{ time }</Text> : null}
|
||||
</View>
|
||||
);
|
||||
}, () => true);
|
||||
|
||||
const Reply = ({
|
||||
attachment, timeFormat, baseUrl, customEmojis, user, index
|
||||
const Description = React.memo(({
|
||||
attachment, baseUrl, user, getCustomEmoji, useMarkdown
|
||||
}) => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (!text) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
/>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.attachment.text !== nextProps.attachment.text) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.attachment.title !== nextProps.attachment.title) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Fields = React.memo(({ attachment }) => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<View style={styles.fieldsContainer}>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment.fields, nextProps.attachment.fields));
|
||||
|
||||
const Reply = React.memo(({
|
||||
attachment, timeFormat, baseUrl, user, index, getCustomEmoji, useMarkdown
|
||||
}) => {
|
||||
if (!attachment) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const renderAuthor = () => (
|
||||
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
|
||||
);
|
||||
|
||||
const renderTime = () => {
|
||||
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||
return time ? <Text style={styles.time}>{ time }</Text> : null;
|
||||
};
|
||||
|
||||
const renderTitle = () => {
|
||||
if (!attachment.author_name) {
|
||||
return null;
|
||||
const onPress = () => {
|
||||
let url = attachment.title_link || attachment.author_link;
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<View style={styles.authorContainer}>
|
||||
{renderAuthor()}
|
||||
{renderTime()}
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
const renderText = () => {
|
||||
const text = attachment.text || attachment.title;
|
||||
if (text) {
|
||||
return (
|
||||
<Markdown
|
||||
msg={text}
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
username={user.username}
|
||||
/>
|
||||
);
|
||||
if (attachment.type === 'file') {
|
||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderFields = () => {
|
||||
if (!attachment.fields) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<View style={styles.fieldsContainer}>
|
||||
{attachment.fields.map(field => (
|
||||
<View key={field.title} style={[styles.fieldContainer, { width: field.short ? '50%' : '100%' }]}>
|
||||
<Text style={styles.fieldTitle}>{field.title}</Text>
|
||||
<Text style={styles.fieldValue}>{field.value}</Text>
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
);
|
||||
openLink(url);
|
||||
};
|
||||
|
||||
return (
|
||||
<Touchable
|
||||
onPress={() => onPress(attachment, baseUrl, user)}
|
||||
onPress={onPress}
|
||||
style={[styles.button, index > 0 && styles.marginTop]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<View style={styles.attachmentContainer}>
|
||||
{renderTitle()}
|
||||
{renderText()}
|
||||
{renderFields()}
|
||||
<Title attachment={attachment} timeFormat={timeFormat} />
|
||||
<Description
|
||||
attachment={attachment}
|
||||
timeFormat={timeFormat}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
useMarkdown={useMarkdown}
|
||||
/>
|
||||
<Fields attachment={attachment} />
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
|
||||
|
||||
Reply.propTypes = {
|
||||
attachment: PropTypes.object.isRequired,
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
index: PropTypes.number
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
index: PropTypes.number,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Reply.displayName = 'MessageReply';
|
||||
|
||||
Title.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
timeFormat: PropTypes.string
|
||||
};
|
||||
Title.displayName = 'MessageReplyTitle';
|
||||
|
||||
Description.propTypes = {
|
||||
attachment: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
Description.displayName = 'MessageReplyDescription';
|
||||
|
||||
Fields.propTypes = {
|
||||
attachment: PropTypes.object
|
||||
};
|
||||
Fields.displayName = 'MessageReplyFields';
|
||||
|
||||
export default Reply;
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { View, Text } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { formatLastMessage, formatMessageCount } from './utils';
|
||||
import styles from './styles';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { THREAD } from './constants';
|
||||
|
||||
const Thread = React.memo(({
|
||||
msg, tcount, tlm, customThreadTimeFormat
|
||||
}) => {
|
||||
if (!tlm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const time = formatLastMessage(tlm, customThreadTimeFormat);
|
||||
const buttonText = formatMessageCount(tcount, THREAD);
|
||||
return (
|
||||
<View style={styles.buttonContainer}>
|
||||
<View
|
||||
style={[styles.button, styles.smallButton]}
|
||||
testID={`message-thread-button-${ msg }`}
|
||||
>
|
||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
||||
</View>
|
||||
<Text style={styles.time}>{time}</Text>
|
||||
</View>
|
||||
);
|
||||
}, (prevProps, nextProps) => {
|
||||
if (prevProps.tcount !== nextProps.tcount) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
Thread.propTypes = {
|
||||
msg: PropTypes.string,
|
||||
tcount: PropTypes.string,
|
||||
tlm: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string
|
||||
};
|
||||
Thread.displayName = 'MessageThread';
|
||||
|
||||
export default Thread;
|
|
@ -57,14 +57,22 @@ const UrlImage = React.memo(({ image, user, baseUrl }) => {
|
|||
}
|
||||
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} />;
|
||||
});
|
||||
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
|
||||
|
||||
const UrlContent = React.memo(({ title, description }) => (
|
||||
<View style={styles.textContainer}>
|
||||
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
||||
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
||||
</View>
|
||||
));
|
||||
), (prevProps, nextProps) => {
|
||||
if (prevProps.title !== nextProps.title) {
|
||||
return false;
|
||||
}
|
||||
if (prevProps.description !== nextProps.description) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const Url = React.memo(({
|
||||
url, index, user, baseUrl
|
||||
|
@ -89,16 +97,28 @@ const Url = React.memo(({
|
|||
);
|
||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
||||
|
||||
const Urls = React.memo(({ urls, user, baseUrl }) => {
|
||||
if (!urls || urls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return urls.map((url, index) => (
|
||||
<Url url={url} key={url.url} index={index} user={user} baseUrl={baseUrl} />
|
||||
));
|
||||
}, (oldProps, newProps) => isEqual(oldProps.urls, newProps.urls));
|
||||
|
||||
UrlImage.propTypes = {
|
||||
image: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
UrlImage.displayName = 'MessageUrlImage';
|
||||
|
||||
UrlContent.propTypes = {
|
||||
title: PropTypes.string,
|
||||
description: PropTypes.string
|
||||
};
|
||||
UrlContent.displayName = 'MessageUrlContent';
|
||||
|
||||
Url.propTypes = {
|
||||
url: PropTypes.object.isRequired,
|
||||
|
@ -106,5 +126,13 @@ Url.propTypes = {
|
|||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Url.displayName = 'MessageUrl';
|
||||
|
||||
export default Url;
|
||||
Urls.propTypes = {
|
||||
urls: PropTypes.array,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string
|
||||
};
|
||||
Urls.displayName = 'MessageUrls';
|
||||
|
||||
export default Urls;
|
|
@ -30,28 +30,11 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export default class User extends React.PureComponent {
|
||||
static propTypes = {
|
||||
timeFormat: PropTypes.string.isRequired,
|
||||
username: PropTypes.string,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.oneOfType([
|
||||
PropTypes.instanceOf(Date),
|
||||
PropTypes.string
|
||||
]),
|
||||
temp: PropTypes.bool
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
username, alias, ts, temp, timeFormat
|
||||
} = this.props;
|
||||
|
||||
const extraStyle = {};
|
||||
if (temp) {
|
||||
extraStyle.opacity = 0.3;
|
||||
}
|
||||
|
||||
const User = React.memo(({
|
||||
isHeader, useRealName, author, alias, ts, timeFormat
|
||||
}) => {
|
||||
if (isHeader) {
|
||||
const username = (useRealName && author.name) || author.username;
|
||||
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
||||
const time = moment(ts).format(timeFormat);
|
||||
|
||||
|
@ -67,4 +50,17 @@ export default class User extends React.PureComponent {
|
|||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
});
|
||||
|
||||
User.propTypes = {
|
||||
isHeader: PropTypes.bool,
|
||||
useRealName: PropTypes.bool,
|
||||
author: PropTypes.object,
|
||||
alias: PropTypes.string,
|
||||
ts: PropTypes.instanceOf(Date),
|
||||
timeFormat: PropTypes.string
|
||||
};
|
||||
User.displayName = 'MessageUser';
|
||||
|
||||
export default User;
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { StyleSheet, View } from 'react-native';
|
||||
import Modal from 'react-native-modal';
|
||||
import VideoPlayer from 'react-native-video-controls';
|
||||
import { StyleSheet } from 'react-native';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import isEqual from 'deep-equal';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import openLink from '../../utils/openLink';
|
||||
import { isIOS } from '../../utils/deviceInfo';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
import { formatAttachmentUrl } from '../../lib/utils';
|
||||
|
||||
const SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
|
||||
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||
|
@ -32,77 +32,46 @@ const styles = StyleSheet.create({
|
|||
}
|
||||
});
|
||||
|
||||
export default class Video extends React.PureComponent {
|
||||
static propTypes = {
|
||||
file: PropTypes.object.isRequired,
|
||||
baseUrl: PropTypes.string.isRequired,
|
||||
user: PropTypes.object.isRequired,
|
||||
customEmojis: PropTypes.object.isRequired
|
||||
const Video = React.memo(({
|
||||
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||
}) => {
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
state = { isVisible: false };
|
||||
|
||||
get uri() {
|
||||
const { baseUrl, user, file } = this.props;
|
||||
const { video_url } = file;
|
||||
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||
}
|
||||
|
||||
toggleModal = () => {
|
||||
this.setState(prevState => ({
|
||||
isVisible: !prevState.isVisible
|
||||
}));
|
||||
}
|
||||
|
||||
open = () => {
|
||||
const { file } = this.props;
|
||||
const onPress = () => {
|
||||
if (isTypeSupported(file.video_type)) {
|
||||
return this.toggleModal();
|
||||
return onOpenFileModal(file);
|
||||
}
|
||||
openLink(this.uri);
|
||||
}
|
||||
const uri = formatAttachmentUrl(file.video_url, user.id, user.token, baseUrl);
|
||||
openLink(uri);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isVisible } = this.state;
|
||||
const {
|
||||
baseUrl, user, customEmojis, file
|
||||
} = this.props;
|
||||
const { description } = file;
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Touchable
|
||||
onPress={onPress}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
/>
|
||||
</Touchable>
|
||||
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||
</React.Fragment>
|
||||
);
|
||||
}, (prevProps, nextProps) => isEqual(prevProps.file, nextProps.file));
|
||||
|
||||
if (!baseUrl) {
|
||||
return null;
|
||||
}
|
||||
Video.propTypes = {
|
||||
file: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
user: PropTypes.object,
|
||||
useMarkdown: PropTypes.bool,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
getCustomEmoji: PropTypes.func
|
||||
};
|
||||
|
||||
return (
|
||||
[
|
||||
<View key='button'>
|
||||
<Touchable
|
||||
onPress={this.open}
|
||||
style={styles.button}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
>
|
||||
<CustomIcon
|
||||
name='play'
|
||||
size={54}
|
||||
style={styles.image}
|
||||
/>
|
||||
</Touchable>
|
||||
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
|
||||
</View>,
|
||||
<Modal
|
||||
key='modal'
|
||||
isVisible={isVisible}
|
||||
style={styles.modal}
|
||||
supportedOrientations={['portrait', 'landscape']}
|
||||
onBackButtonPress={() => this.toggleModal()}
|
||||
>
|
||||
<VideoPlayer
|
||||
source={{ uri: this.uri }}
|
||||
onBack={this.toggleModal}
|
||||
disableVolume
|
||||
/>
|
||||
</Modal>
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
export default Video;
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
export const DISCUSSION = 'discussion';
|
||||
export const THREAD = 'thread';
|
|
@ -1,30 +1,13 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ViewPropTypes } from 'react-native';
|
||||
import { connect } from 'react-redux';
|
||||
import equal from 'deep-equal';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
|
||||
import Message from './Message';
|
||||
import {
|
||||
errorActionsShow as errorActionsShowAction,
|
||||
toggleReactionPicker as toggleReactionPickerAction,
|
||||
replyBroadcast as replyBroadcastAction
|
||||
} from '../../actions/messages';
|
||||
import { vibrate } from '../../utils/vibration';
|
||||
import debounce from '../../utils/debounce';
|
||||
import { SYSTEM_MESSAGES, getCustomEmoji } from './utils';
|
||||
import messagesStatus from '../../constants/messagesStatus';
|
||||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
editingMessage: state.messages.message,
|
||||
useRealName: state.settings.UI_Use_Real_Name
|
||||
}), dispatch => ({
|
||||
errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
|
||||
replyBroadcast: message => dispatch(replyBroadcastAction(message)),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message))
|
||||
}))
|
||||
export default class MessageContainer extends React.Component {
|
||||
static propTypes = {
|
||||
item: PropTypes.object.isRequired,
|
||||
|
@ -33,31 +16,28 @@ export default class MessageContainer extends React.Component {
|
|||
username: PropTypes.string.isRequired,
|
||||
token: PropTypes.string.isRequired
|
||||
}),
|
||||
customTimeFormat: PropTypes.string,
|
||||
timeFormat: PropTypes.string,
|
||||
customThreadTimeFormat: PropTypes.string,
|
||||
style: ViewPropTypes.style,
|
||||
archived: PropTypes.bool,
|
||||
broadcast: PropTypes.bool,
|
||||
previousItem: PropTypes.object,
|
||||
_updatedAt: PropTypes.instanceOf(Date),
|
||||
// redux
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.object,
|
||||
Message_GroupingPeriod: PropTypes.number,
|
||||
Message_TimeFormat: PropTypes.string,
|
||||
editingMessage: PropTypes.object,
|
||||
useRealName: PropTypes.bool,
|
||||
useMarkdown: PropTypes.bool,
|
||||
status: PropTypes.number,
|
||||
navigation: PropTypes.object,
|
||||
// methods - props
|
||||
onLongPress: PropTypes.func,
|
||||
onReactionPress: PropTypes.func,
|
||||
onDiscussionPress: PropTypes.func,
|
||||
// methods - redux
|
||||
onThreadPress: PropTypes.func,
|
||||
errorActionsShow: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
fetchThreadName: PropTypes.func
|
||||
fetchThreadName: PropTypes.func,
|
||||
onOpenFileModal: PropTypes.func,
|
||||
onReactionLongPress: PropTypes.func
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
|
@ -67,21 +47,11 @@ export default class MessageContainer extends React.Component {
|
|||
broadcast: false
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { reactionsModal: false };
|
||||
this.closeReactions = this.closeReactions.bind(this);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { reactionsModal } = this.state;
|
||||
shouldComponentUpdate(nextProps) {
|
||||
const {
|
||||
status, editingMessage, item, _updatedAt, navigation
|
||||
status, item, _updatedAt, previousItem
|
||||
} = this.props;
|
||||
|
||||
if (reactionsModal !== nextState.reactionsModal) {
|
||||
return true;
|
||||
}
|
||||
if (status !== nextProps.status) {
|
||||
return true;
|
||||
}
|
||||
|
@ -89,65 +59,68 @@ export default class MessageContainer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
|
||||
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
|
||||
return true;
|
||||
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
|
||||
return true;
|
||||
}
|
||||
if (!previousItem && !!nextProps.previousItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
|
||||
}
|
||||
|
||||
onPress = debounce(() => {
|
||||
const { item } = this.props;
|
||||
KeyboardUtils.dismiss();
|
||||
|
||||
if ((item.tlm || item.tmid)) {
|
||||
this.onThreadPress();
|
||||
}
|
||||
}, 300, true);
|
||||
|
||||
onLongPress = () => {
|
||||
const { onLongPress } = this.props;
|
||||
onLongPress(this.parseMessage());
|
||||
const { archived, onLongPress } = this.props;
|
||||
if (this.isInfo || this.hasError || archived) {
|
||||
return;
|
||||
}
|
||||
if (onLongPress) {
|
||||
onLongPress(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
onErrorPress = () => {
|
||||
const { errorActionsShow } = this.props;
|
||||
errorActionsShow(this.parseMessage());
|
||||
if (errorActionsShow) {
|
||||
errorActionsShow(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
onReactionPress = (emoji) => {
|
||||
const { onReactionPress, item } = this.props;
|
||||
onReactionPress(emoji, item._id);
|
||||
if (onReactionPress) {
|
||||
onReactionPress(emoji, item._id);
|
||||
}
|
||||
}
|
||||
|
||||
onReactionLongPress = () => {
|
||||
this.setState({ reactionsModal: true });
|
||||
vibrate();
|
||||
const { onReactionLongPress, item } = this.props;
|
||||
if (onReactionLongPress) {
|
||||
onReactionLongPress(item);
|
||||
}
|
||||
}
|
||||
|
||||
onDiscussionPress = () => {
|
||||
const { onDiscussionPress, item } = this.props;
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
|
||||
onThreadPress = debounce(() => {
|
||||
const { navigation, item } = this.props;
|
||||
if (item.tmid) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
||||
});
|
||||
} else if (item.tlm) {
|
||||
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item._id, name: title, t: 'thread'
|
||||
});
|
||||
if (onDiscussionPress) {
|
||||
onDiscussionPress(item);
|
||||
}
|
||||
}, 1000, true)
|
||||
|
||||
get timeFormat() {
|
||||
const { customTimeFormat, Message_TimeFormat } = this.props;
|
||||
return customTimeFormat || Message_TimeFormat;
|
||||
}
|
||||
|
||||
closeReactions = () => {
|
||||
this.setState({ reactionsModal: false });
|
||||
onThreadPress = () => {
|
||||
const { onThreadPress, item } = this.props;
|
||||
if (onThreadPress) {
|
||||
onThreadPress(item);
|
||||
}
|
||||
}
|
||||
|
||||
isHeader = () => {
|
||||
get isHeader() {
|
||||
const {
|
||||
item, previousItem, broadcast, Message_GroupingPeriod
|
||||
} = this.props;
|
||||
|
@ -163,7 +136,7 @@ export default class MessageContainer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
isThreadReply = () => {
|
||||
get isThreadReply() {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
|
@ -173,7 +146,7 @@ export default class MessageContainer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
isThreadSequential = () => {
|
||||
get isThreadSequential() {
|
||||
const {
|
||||
item, previousItem
|
||||
} = this.props;
|
||||
|
@ -183,6 +156,21 @@ export default class MessageContainer extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
get isInfo() {
|
||||
const { item } = this.props;
|
||||
return SYSTEM_MESSAGES.includes(item.t);
|
||||
}
|
||||
|
||||
get isTemp() {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.TEMP || item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
get hasError() {
|
||||
const { item } = this.props;
|
||||
return item.status === messagesStatus.ERROR;
|
||||
}
|
||||
|
||||
parseMessage = () => {
|
||||
const { item } = this.props;
|
||||
return JSON.parse(JSON.stringify(item));
|
||||
|
@ -190,23 +178,26 @@ export default class MessageContainer extends React.Component {
|
|||
|
||||
toggleReactionPicker = () => {
|
||||
const { toggleReactionPicker } = this.props;
|
||||
toggleReactionPicker(this.parseMessage());
|
||||
if (toggleReactionPicker) {
|
||||
toggleReactionPicker(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
replyBroadcast = () => {
|
||||
const { replyBroadcast } = this.props;
|
||||
replyBroadcast(this.parseMessage());
|
||||
if (replyBroadcast) {
|
||||
replyBroadcast(this.parseMessage());
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { reactionsModal } = this.state;
|
||||
const {
|
||||
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
|
||||
item, user, style, archived, baseUrl, useRealName, broadcast, fetchThreadName, customThreadTimeFormat, onOpenFileModal, timeFormat, useMarkdown
|
||||
} = this.props;
|
||||
const {
|
||||
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
|
||||
_id, msg, ts, attachments, urls, reactions, t, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg, mentions, channels
|
||||
} = item;
|
||||
const isEditing = editingMessage._id === item._id;
|
||||
|
||||
return (
|
||||
<Message
|
||||
id={_id}
|
||||
|
@ -214,26 +205,18 @@ export default class MessageContainer extends React.Component {
|
|||
author={u}
|
||||
ts={ts}
|
||||
type={t}
|
||||
status={status}
|
||||
attachments={attachments}
|
||||
urls={urls}
|
||||
reactions={reactions}
|
||||
alias={alias}
|
||||
editing={isEditing}
|
||||
header={this.isHeader()}
|
||||
isThreadReply={this.isThreadReply()}
|
||||
isThreadSequential={this.isThreadSequential()}
|
||||
avatar={avatar}
|
||||
user={user}
|
||||
edited={editedBy && !!editedBy.username}
|
||||
timeFormat={this.timeFormat}
|
||||
timeFormat={timeFormat}
|
||||
customThreadTimeFormat={customThreadTimeFormat}
|
||||
style={style}
|
||||
archived={archived}
|
||||
broadcast={broadcast}
|
||||
baseUrl={baseUrl}
|
||||
customEmojis={customEmojis}
|
||||
reactionsModal={reactionsModal}
|
||||
useRealName={useRealName}
|
||||
role={role}
|
||||
drid={drid}
|
||||
|
@ -243,16 +226,27 @@ export default class MessageContainer extends React.Component {
|
|||
tcount={tcount}
|
||||
tlm={tlm}
|
||||
tmsg={tmsg}
|
||||
useMarkdown={useMarkdown}
|
||||
fetchThreadName={fetchThreadName}
|
||||
closeReactions={this.closeReactions}
|
||||
mentions={mentions}
|
||||
channels={channels}
|
||||
isEdited={editedBy && !!editedBy.username}
|
||||
isHeader={this.isHeader}
|
||||
isThreadReply={this.isThreadReply}
|
||||
isThreadSequential={this.isThreadSequential}
|
||||
isInfo={this.isInfo}
|
||||
isTemp={this.isTemp}
|
||||
hasError={this.hasError}
|
||||
onErrorPress={this.onErrorPress}
|
||||
onPress={this.onPress}
|
||||
onLongPress={this.onLongPress}
|
||||
onReactionLongPress={this.onReactionLongPress}
|
||||
onReactionPress={this.onReactionPress}
|
||||
replyBroadcast={this.replyBroadcast}
|
||||
toggleReactionPicker={this.toggleReactionPicker}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
onThreadPress={this.onThreadPress}
|
||||
onOpenFileModal={onOpenFileModal}
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -18,8 +18,7 @@ export default StyleSheet.create({
|
|||
paddingVertical: 4,
|
||||
width: '100%',
|
||||
paddingHorizontal: 14,
|
||||
flexDirection: 'column',
|
||||
flex: 1
|
||||
flexDirection: 'column'
|
||||
},
|
||||
messageContent: {
|
||||
flex: 1,
|
||||
|
@ -32,8 +31,8 @@ export default StyleSheet.create({
|
|||
marginLeft: 0
|
||||
},
|
||||
flex: {
|
||||
flexDirection: 'row',
|
||||
flex: 1
|
||||
flexDirection: 'row'
|
||||
// flex: 1
|
||||
},
|
||||
text: {
|
||||
fontSize: 16,
|
||||
|
@ -46,9 +45,6 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
},
|
||||
editing: {
|
||||
backgroundColor: '#fff5df'
|
||||
},
|
||||
customEmoji: {
|
||||
width: 20,
|
||||
height: 20
|
||||
|
@ -161,7 +157,7 @@ export default StyleSheet.create({
|
|||
justifyContent: 'flex-start'
|
||||
},
|
||||
imageContainer: {
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
flexDirection: 'column',
|
||||
borderRadius: 4
|
||||
},
|
||||
|
@ -173,6 +169,9 @@ export default StyleSheet.create({
|
|||
borderColor: COLOR_BORDER,
|
||||
borderWidth: 1
|
||||
},
|
||||
imagePressed: {
|
||||
opacity: 0.5
|
||||
},
|
||||
inlineImage: {
|
||||
width: 300,
|
||||
height: 300,
|
||||
|
@ -220,7 +219,7 @@ export default StyleSheet.create({
|
|||
},
|
||||
repliedThread: {
|
||||
flexDirection: 'row',
|
||||
flex: 1,
|
||||
// flex: 1,
|
||||
alignItems: 'center',
|
||||
marginTop: 6,
|
||||
marginBottom: 12
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import I18n from '../../i18n';
|
||||
import database from '../../lib/realm';
|
||||
import { DISCUSSION } from './constants';
|
||||
|
||||
export const formatLastMessage = (lm, customFormat) => {
|
||||
if (customFormat) {
|
||||
return moment(lm).format(customFormat);
|
||||
}
|
||||
return lm ? moment(lm).calendar(null, {
|
||||
lastDay: `[${ I18n.t('Yesterday') }]`,
|
||||
sameDay: 'h:mm A',
|
||||
lastWeek: 'dddd',
|
||||
sameElse: 'MMM D'
|
||||
}) : null;
|
||||
};
|
||||
|
||||
export const formatMessageCount = (count, type) => {
|
||||
const discussion = type === DISCUSSION;
|
||||
let text = discussion ? I18n.t('No_messages_yet') : null;
|
||||
if (count === 1) {
|
||||
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
|
||||
} else if (count > 1 && count < 1000) {
|
||||
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
} else if (count > 999) {
|
||||
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
export const BUTTON_HIT_SLOP = {
|
||||
top: 4, right: 4, bottom: 4, left: 4
|
||||
};
|
||||
|
||||
export const SYSTEM_MESSAGES = [
|
||||
'r',
|
||||
'au',
|
||||
'ru',
|
||||
'ul',
|
||||
'uj',
|
||||
'ut',
|
||||
'rm',
|
||||
'user-muted',
|
||||
'user-unmuted',
|
||||
'message_pinned',
|
||||
'subscription-role-added',
|
||||
'subscription-role-removed',
|
||||
'room_changed_description',
|
||||
'room_changed_announcement',
|
||||
'room_changed_topic',
|
||||
'room_changed_privacy',
|
||||
'message_snippeted',
|
||||
'thread-created'
|
||||
];
|
||||
|
||||
export const getInfoMessage = ({
|
||||
type, role, msg, author
|
||||
}) => {
|
||||
const { username } = author;
|
||||
if (type === 'rm') {
|
||||
return I18n.t('Message_removed');
|
||||
} else if (type === 'uj') {
|
||||
return I18n.t('Has_joined_the_channel');
|
||||
} else if (type === 'ut') {
|
||||
return I18n.t('Has_joined_the_conversation');
|
||||
} else if (type === 'r') {
|
||||
return I18n.t('Room_name_changed', { name: msg, userBy: username });
|
||||
} else if (type === 'message_pinned') {
|
||||
return I18n.t('Message_pinned');
|
||||
} else if (type === 'ul') {
|
||||
return I18n.t('Has_left_the_channel');
|
||||
} else if (type === 'ru') {
|
||||
return I18n.t('User_removed_by', { userRemoved: msg, userBy: username });
|
||||
} else if (type === 'au') {
|
||||
return I18n.t('User_added_by', { userAdded: msg, userBy: username });
|
||||
} else if (type === 'user-muted') {
|
||||
return I18n.t('User_muted_by', { userMuted: msg, userBy: username });
|
||||
} else if (type === 'user-unmuted') {
|
||||
return I18n.t('User_unmuted_by', { userUnmuted: msg, userBy: username });
|
||||
} else if (type === 'subscription-role-added') {
|
||||
return `${ msg } was set ${ role } by ${ username }`;
|
||||
} else if (type === 'subscription-role-removed') {
|
||||
return `${ msg } is no longer ${ role } by ${ username }`;
|
||||
} else if (type === 'room_changed_description') {
|
||||
return I18n.t('Room_changed_description', { description: msg, userBy: username });
|
||||
} else if (type === 'room_changed_announcement') {
|
||||
return I18n.t('Room_changed_announcement', { announcement: msg, userBy: username });
|
||||
} else if (type === 'room_changed_topic') {
|
||||
return I18n.t('Room_changed_topic', { topic: msg, userBy: username });
|
||||
} else if (type === 'room_changed_privacy') {
|
||||
return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
|
||||
} else if (type === 'message_snippeted') {
|
||||
return I18n.t('Created_snippet');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
export const getCustomEmoji = (content) => {
|
||||
// search by name
|
||||
const data = database.objects('customEmojis').filtered('name == $0', content);
|
||||
if (data.length) {
|
||||
return data[0];
|
||||
}
|
||||
|
||||
// searches by alias
|
||||
// RealmJS doesn't support IN operator: https://github.com/realm/realm-js/issues/450
|
||||
const emojis = database.objects('customEmojis');
|
||||
const findByAlias = emojis.find((emoji) => {
|
||||
if (emoji.aliases.length && emoji.aliases.findIndex(alias => alias === content) !== -1) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
return findByAlias;
|
||||
};
|
|
@ -148,13 +148,14 @@ export default {
|
|||
Dont_Have_An_Account: 'Don\'t have an account?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||
edit: 'edit',
|
||||
erasing_room: 'erasing room',
|
||||
edited: 'edited',
|
||||
Edit: 'Edit',
|
||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||
Email: 'Email',
|
||||
email: 'e-mail',
|
||||
Enable_notifications: 'Enable notifications',
|
||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||
erasing_room: 'erasing room',
|
||||
Error_uploading: 'Error uploading',
|
||||
Favorites: 'Favorites',
|
||||
Files: 'Files',
|
||||
|
|
|
@ -154,6 +154,7 @@ export default {
|
|||
Dont_Have_An_Account: 'Não tem uma conta?',
|
||||
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||
edit: 'editar',
|
||||
edited: 'editado',
|
||||
erasing_room: 'apagando sala',
|
||||
Edit: 'Editar',
|
||||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
||||
|
|
|
@ -3,7 +3,6 @@ import semver from 'semver';
|
|||
|
||||
import reduxStore from '../createStore';
|
||||
import database from '../realm';
|
||||
import * as actions from '../../actions';
|
||||
import log from '../../utils/log';
|
||||
|
||||
const getUpdatedSince = () => {
|
||||
|
@ -17,7 +16,7 @@ const create = (customEmojis) => {
|
|||
try {
|
||||
database.create('customEmojis', emoji, true);
|
||||
} catch (e) {
|
||||
log('getEmojis create', e);
|
||||
// log('getEmojis create', e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -40,7 +39,6 @@ export default async function() {
|
|||
database.write(() => {
|
||||
create(emojis);
|
||||
});
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
|
||||
});
|
||||
} else {
|
||||
const params = {};
|
||||
|
@ -72,9 +70,6 @@ export default async function() {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
const allEmojis = database.objects('customEmojis');
|
||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(allEmojis)));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -27,9 +27,8 @@ export const merge = (subscription, room) => {
|
|||
if (!subscription.roles || !subscription.roles.length) {
|
||||
subscription.roles = [];
|
||||
}
|
||||
|
||||
if (room.muted && room.muted.length) {
|
||||
subscription.muted = room.muted.filter(user => user).map(user => ({ value: user }));
|
||||
subscription.muted = room.muted.filter(muted => !!muted);
|
||||
} else {
|
||||
subscription.muted = [];
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export default (msg) => {
|
|||
// msg.reactions = Object.keys(msg.reactions).map(key => ({ emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
|
||||
// }
|
||||
if (!Array.isArray(msg.reactions)) {
|
||||
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
|
||||
msg.reactions = Object.keys(msg.reactions).map(key => ({ _id: `${ msg._id }${ key }`, emoji: key, usernames: msg.reactions[key].usernames }));
|
||||
}
|
||||
msg.urls = msg.urls ? parseUrls(msg.urls) : [];
|
||||
msg._updatedAt = new Date();
|
||||
|
|
|
@ -125,7 +125,7 @@ export default function subscribeRoom({ rid }) {
|
|||
|
||||
const read = debounce(() => {
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (room._id) {
|
||||
if (room && room._id) {
|
||||
this.readMessages(rid);
|
||||
}
|
||||
}, 300);
|
||||
|
|
|
@ -43,18 +43,11 @@ const roomsSchema = {
|
|||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
name: 'string?',
|
||||
broadcast: { type: 'bool', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
const userMutedInRoomSchema = {
|
||||
name: 'usersMuted',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const subscriptionSchema = {
|
||||
name: 'subscriptions',
|
||||
primaryKey: '_id',
|
||||
|
@ -85,7 +78,7 @@ const subscriptionSchema = {
|
|||
archived: { type: 'bool', optional: true },
|
||||
joinCodeRequired: { type: 'bool', optional: true },
|
||||
notifications: { type: 'bool', optional: true },
|
||||
muted: { type: 'list', objectType: 'usersMuted' },
|
||||
muted: 'string[]',
|
||||
broadcast: { type: 'bool', optional: true },
|
||||
prid: { type: 'string', optional: true },
|
||||
draftMessage: { type: 'string', optional: true },
|
||||
|
@ -99,8 +92,7 @@ const usersSchema = {
|
|||
properties: {
|
||||
_id: 'string',
|
||||
username: 'string',
|
||||
name: { type: 'string', optional: true },
|
||||
avatarVersion: { type: 'int', optional: true }
|
||||
name: { type: 'string', optional: true }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -155,21 +147,13 @@ const url = {
|
|||
}
|
||||
};
|
||||
|
||||
const messagesReactionsUsernamesSchema = {
|
||||
name: 'messagesReactionsUsernames',
|
||||
primaryKey: 'value',
|
||||
properties: {
|
||||
value: 'string'
|
||||
}
|
||||
};
|
||||
|
||||
const messagesReactionsSchema = {
|
||||
name: 'messagesReactions',
|
||||
primaryKey: '_id',
|
||||
properties: {
|
||||
_id: 'string',
|
||||
emoji: 'string',
|
||||
usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
|
||||
usernames: 'string[]'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -211,7 +195,9 @@ const messagesSchema = {
|
|||
tmid: { type: 'string', optional: true },
|
||||
tcount: { type: 'int', optional: true },
|
||||
tlm: { type: 'date', optional: true },
|
||||
replies: 'string[]'
|
||||
replies: 'string[]',
|
||||
mentions: { type: 'list', objectType: 'users' },
|
||||
channels: { type: 'list', objectType: 'rooms' }
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -359,9 +345,7 @@ const schema = [
|
|||
frequentlyUsedEmojiSchema,
|
||||
customEmojisSchema,
|
||||
messagesReactionsSchema,
|
||||
messagesReactionsUsernamesSchema,
|
||||
rolesSchema,
|
||||
userMutedInRoomSchema,
|
||||
uploadsSchema
|
||||
];
|
||||
|
||||
|
@ -374,9 +358,9 @@ class DB {
|
|||
schema: [
|
||||
serversSchema
|
||||
],
|
||||
schemaVersion: 6,
|
||||
schemaVersion: 8,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
|
||||
const newServers = newRealm.objects('servers');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
@ -431,16 +415,11 @@ class DB {
|
|||
return this.databases.activeDB = new Realm({
|
||||
path: `${ path }.realm`,
|
||||
schema,
|
||||
schemaVersion: 9,
|
||||
schemaVersion: 11,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
for (let i = 0; i < newSubs.length; i++) {
|
||||
newSubs[i].lastOpen = null;
|
||||
newSubs[i].ls = null;
|
||||
}
|
||||
newRealm.delete(newSubs);
|
||||
const newMessages = newRealm.objects('messages');
|
||||
newRealm.delete(newMessages);
|
||||
const newThreads = newRealm.objects('threads');
|
||||
|
@ -449,8 +428,6 @@ class DB {
|
|||
newRealm.delete(newThreadMessages);
|
||||
}
|
||||
if (newRealm.schemaVersion === 9) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
newRealm.delete(newSubs);
|
||||
const newEmojis = newRealm.objects('customEmojis');
|
||||
newRealm.delete(newEmojis);
|
||||
const newSettings = newRealm.objects('settings');
|
||||
|
|
|
@ -472,19 +472,6 @@ const RocketChat = {
|
|||
return setting;
|
||||
});
|
||||
},
|
||||
parseEmojis: emojis => emojis.reduce((ret, item) => {
|
||||
ret[item.name] = item.extension;
|
||||
item.aliases.forEach((alias) => {
|
||||
ret[alias.value] = item.extension;
|
||||
});
|
||||
return ret;
|
||||
}, {}),
|
||||
_prepareEmojis(emojis) {
|
||||
emojis.forEach((emoji) => {
|
||||
emoji.aliases = emoji.aliases.map(alias => ({ value: alias }));
|
||||
});
|
||||
return emojis;
|
||||
},
|
||||
deleteMessage(message) {
|
||||
const { _id, rid } = message;
|
||||
// RC 0.48.0
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export const formatAttachmentUrl = (attachmentUrl, userId, token, server) => (
|
||||
encodeURI(attachmentUrl.includes('http') ? attachmentUrl : `${ server }${ attachmentUrl }?rc_uid=${ userId }&rc_token=${ token }`)
|
||||
);
|
|
@ -1,17 +0,0 @@
|
|||
import * as types from '../constants/types';
|
||||
|
||||
const initialState = {
|
||||
customEmojis: {}
|
||||
};
|
||||
|
||||
|
||||
export default function customEmojis(state = initialState.customEmojis, action) {
|
||||
if (action.type === types.SET_CUSTOM_EMOJIS) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
}
|
|
@ -8,7 +8,6 @@ import server from './server';
|
|||
import selectedUsers from './selectedUsers';
|
||||
import createChannel from './createChannel';
|
||||
import app from './app';
|
||||
import customEmojis from './customEmojis';
|
||||
import sortPreferences from './sortPreferences';
|
||||
|
||||
export default combineReducers({
|
||||
|
@ -21,6 +20,5 @@ export default combineReducers({
|
|||
createChannel,
|
||||
app,
|
||||
rooms,
|
||||
customEmojis,
|
||||
sortPreferences
|
||||
});
|
||||
|
|
|
@ -19,7 +19,13 @@ const handleRoomsRequest = function* handleRoomsRequest() {
|
|||
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
|
||||
|
||||
database.write(() => {
|
||||
subscriptions.forEach(subscription => database.create('subscriptions', subscription, true));
|
||||
subscriptions.forEach((subscription) => {
|
||||
try {
|
||||
database.create('subscriptions', subscription, true);
|
||||
} catch (error) {
|
||||
log('handleRoomsRequest create sub', error);
|
||||
}
|
||||
});
|
||||
});
|
||||
database.databases.serversDB.write(() => {
|
||||
try {
|
||||
|
|
|
@ -51,8 +51,6 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
|||
|
||||
const settings = database.objects('settings');
|
||||
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
|
||||
const emojis = database.objects('customEmojis');
|
||||
yield put(actions.setCustomEmojis(RocketChat.parseEmojis(emojis.slice(0, emojis.length))));
|
||||
|
||||
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
|
||||
} catch (e) {
|
||||
|
|
|
@ -14,13 +14,13 @@ import I18n from '../../i18n';
|
|||
import RocketChat from '../../lib/rocketchat';
|
||||
import StatusBar from '../../containers/StatusBar';
|
||||
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
|
||||
import FileModal from '../../containers/FileModal';
|
||||
|
||||
const ACTION_INDEX = 0;
|
||||
const CANCEL_INDEX = 1;
|
||||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
|
@ -36,7 +36,6 @@ export default class MessagesView extends LoggedView {
|
|||
static propTypes = {
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.object,
|
||||
navigation: PropTypes.object
|
||||
}
|
||||
|
||||
|
@ -44,7 +43,9 @@ export default class MessagesView extends LoggedView {
|
|||
super('MessagesView', props);
|
||||
this.state = {
|
||||
loading: false,
|
||||
messages: []
|
||||
messages: [],
|
||||
selectedAttachment: {},
|
||||
photoModalVisible: false
|
||||
};
|
||||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
|
@ -56,10 +57,13 @@ export default class MessagesView extends LoggedView {
|
|||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { loading, messages } = this.state;
|
||||
const { loading, messages, photoModalVisible } = this.state;
|
||||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
}
|
||||
if (nextState.photoModalVisible !== photoModalVisible) {
|
||||
return true;
|
||||
}
|
||||
if (!equal(nextState.messages, messages)) {
|
||||
return true;
|
||||
}
|
||||
|
@ -68,18 +72,18 @@ export default class MessagesView extends LoggedView {
|
|||
|
||||
defineMessagesViewContent = (name) => {
|
||||
const { messages } = this.state;
|
||||
const { user, baseUrl, customEmojis } = this.props;
|
||||
const { user, baseUrl } = this.props;
|
||||
|
||||
const renderItemCommonProps = item => ({
|
||||
customEmojis,
|
||||
baseUrl,
|
||||
user,
|
||||
author: item.u || item.user,
|
||||
ts: item.ts || item.uploadedAt,
|
||||
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
||||
edited: !!item.editedAt,
|
||||
header: true,
|
||||
attachments: item.attachments || []
|
||||
isEdited: !!item.editedAt,
|
||||
isHeader: true,
|
||||
attachments: item.attachments || [],
|
||||
onOpenFileModal: this.onOpenFileModal
|
||||
});
|
||||
|
||||
return ({
|
||||
|
@ -190,6 +194,14 @@ export default class MessagesView extends LoggedView {
|
|||
}
|
||||
}
|
||||
|
||||
onOpenFileModal = (attachment) => {
|
||||
this.setState({ selectedAttachment: attachment, photoModalVisible: true });
|
||||
}
|
||||
|
||||
onCloseFileModal = () => {
|
||||
this.setState({ selectedAttachment: {}, photoModalVisible: false });
|
||||
}
|
||||
|
||||
onLongPress = (message) => {
|
||||
this.setState({ message });
|
||||
this.showActionSheet();
|
||||
|
@ -232,7 +244,10 @@ export default class MessagesView extends LoggedView {
|
|||
renderItem = ({ item }) => this.content.renderItem(item)
|
||||
|
||||
render() {
|
||||
const { messages, loading } = this.state;
|
||||
const {
|
||||
messages, loading, selectedAttachment, photoModalVisible
|
||||
} = this.state;
|
||||
const { user, baseUrl } = this.props;
|
||||
|
||||
if (!loading && messages.length === 0) {
|
||||
return this.renderEmpty();
|
||||
|
@ -249,6 +264,13 @@ export default class MessagesView extends LoggedView {
|
|||
onEndReached={this.load}
|
||||
ListFooterComponent={loading ? <RCActivityIndicator /> : null}
|
||||
/>
|
||||
<FileModal
|
||||
attachment={selectedAttachment}
|
||||
isVisible={photoModalVisible}
|
||||
onClose={this.onCloseFileModal}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -8,8 +8,9 @@ import { isIOS } from '../utils/deviceInfo';
|
|||
import { CloseModalButton } from '../containers/HeaderButton';
|
||||
import StatusBar from '../containers/StatusBar';
|
||||
|
||||
const userAgentAndroid = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1';
|
||||
const userAgent = isIOS ? 'UserAgent' : userAgentAndroid;
|
||||
const userAgent = isIOS
|
||||
? 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1'
|
||||
: 'Mozilla/5.0 (Linux; Android 6.0.1; SM-G920V Build/MMB29K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.98 Mobile Safari/537.36';
|
||||
|
||||
@connect(state => ({
|
||||
server: state.server.server
|
||||
|
@ -62,6 +63,7 @@ export default class OAuthView extends React.PureComponent {
|
|||
<React.Fragment>
|
||||
<StatusBar />
|
||||
<WebView
|
||||
useWebKit
|
||||
source={{ uri: oAuthUrl }}
|
||||
userAgent={userAgent}
|
||||
onNavigationStateChange={(webViewState) => {
|
||||
|
|
|
@ -158,7 +158,7 @@ export default class RoomMembersView extends LoggedView {
|
|||
const { muted } = room;
|
||||
|
||||
this.actionSheetOptions = [I18n.t('Cancel')];
|
||||
const userIsMuted = !!muted.find(m => m.value === user.username);
|
||||
const userIsMuted = !!muted.find(m => m === user.username);
|
||||
user.muted = userIsMuted;
|
||||
if (userIsMuted) {
|
||||
this.actionSheetOptions.push(I18n.t('Unmute'));
|
||||
|
|
|
@ -46,7 +46,9 @@ class RightButtonsContainer extends React.PureComponent {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
safeAddListener(this.thread, this.updateThread);
|
||||
if (this.thread) {
|
||||
safeAddListener(this.thread, this.updateThread);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
|
@ -147,11 +147,11 @@ export class List extends React.PureComponent {
|
|||
style={styles.list}
|
||||
inverted
|
||||
removeClippedSubviews
|
||||
initialNumToRender={5}
|
||||
initialNumToRender={7}
|
||||
onEndReached={this.onEndReached}
|
||||
onEndReachedThreshold={0.5}
|
||||
onEndReachedThreshold={5}
|
||||
maxToRenderPerBatch={5}
|
||||
windowSize={21}
|
||||
windowSize={10}
|
||||
ListFooterComponent={this.renderFooter}
|
||||
{...scrollPersistTaps}
|
||||
/>
|
||||
|
|
|
@ -13,8 +13,10 @@ import EJSON from 'ejson';
|
|||
import {
|
||||
toggleReactionPicker as toggleReactionPickerAction,
|
||||
actionsShow as actionsShowAction,
|
||||
errorActionsShow as errorActionsShowAction,
|
||||
editCancel as editCancelAction,
|
||||
replyCancel as replyCancelAction
|
||||
replyCancel as replyCancelAction,
|
||||
replyBroadcast as replyBroadcastAction
|
||||
} from '../../actions/messages';
|
||||
import LoggedView from '../View';
|
||||
import { List } from './List';
|
||||
|
@ -37,6 +39,9 @@ import Separator from './Separator';
|
|||
import { COLOR_WHITE } from '../../constants/colors';
|
||||
import debounce from '../../utils/debounce';
|
||||
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
||||
import FileModal from '../../containers/FileModal';
|
||||
import { vibrate } from '../../utils/vibration';
|
||||
import ReactionsModal from '../../containers/ReactionsModal';
|
||||
import { Toast } from '../../utils/info';
|
||||
|
||||
@connect(state => ({
|
||||
|
@ -52,12 +57,17 @@ import { Toast } from '../../utils/info';
|
|||
showErrorActions: state.messages.showErrorActions,
|
||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
||||
useRealName: state.settings.UI_Use_Real_Name,
|
||||
isAuthenticated: state.login.isAuthenticated
|
||||
isAuthenticated: state.login.isAuthenticated,
|
||||
Message_GroupingPeriod: state.settings.Message_GroupingPeriod,
|
||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||
baseUrl: state.settings.baseUrl || state.server ? state.server.server : ''
|
||||
}), dispatch => ({
|
||||
editCancel: () => dispatch(editCancelAction()),
|
||||
replyCancel: () => dispatch(replyCancelAction()),
|
||||
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
|
||||
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage))
|
||||
errorActionsShow: actionMessage => dispatch(errorActionsShowAction(actionMessage)),
|
||||
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
|
||||
replyBroadcast: message => dispatch(replyBroadcastAction(message))
|
||||
}))
|
||||
/** @extends React.Component */
|
||||
export default class RoomView extends LoggedView {
|
||||
|
@ -105,12 +115,17 @@ export default class RoomView extends LoggedView {
|
|||
appState: PropTypes.string,
|
||||
useRealName: PropTypes.bool,
|
||||
isAuthenticated: PropTypes.bool,
|
||||
Message_GroupingPeriod: PropTypes.number,
|
||||
Message_TimeFormat: PropTypes.string,
|
||||
editing: PropTypes.bool,
|
||||
replying: PropTypes.bool,
|
||||
toggleReactionPicker: PropTypes.func.isRequired,
|
||||
baseUrl: PropTypes.string,
|
||||
toggleReactionPicker: PropTypes.func,
|
||||
actionsShow: PropTypes.func,
|
||||
editCancel: PropTypes.func,
|
||||
replyCancel: PropTypes.func
|
||||
replyCancel: PropTypes.func,
|
||||
replyBroadcast: PropTypes.func,
|
||||
errorActionsShow: PropTypes.func
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
|
@ -120,16 +135,20 @@ export default class RoomView extends LoggedView {
|
|||
this.rid = props.navigation.getParam('rid');
|
||||
this.t = props.navigation.getParam('t');
|
||||
this.tmid = props.navigation.getParam('tmid');
|
||||
this.useMarkdown = props.navigation.getParam('useMarkdown', true);
|
||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||
this.state = {
|
||||
joined: this.rooms.length > 0,
|
||||
room: this.rooms[0] || { rid: this.rid, t: this.t },
|
||||
lastOpen: null
|
||||
lastOpen: null,
|
||||
photoModalVisible: false,
|
||||
reactionsModalVisible: false,
|
||||
selectedAttachment: {},
|
||||
selectedMessage: {}
|
||||
};
|
||||
this.beginAnimating = false;
|
||||
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
|
||||
this.messagebox = React.createRef();
|
||||
safeAddListener(this.rooms, this.updateRoom);
|
||||
this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false);
|
||||
this.mounted = false;
|
||||
console.timeEnd(`${ this.constructor.name } init`);
|
||||
|
@ -152,6 +171,7 @@ export default class RoomView extends LoggedView {
|
|||
} else {
|
||||
EventEmitter.addEventListener('connected', this.handleConnected);
|
||||
}
|
||||
safeAddListener(this.rooms, this.updateRoom);
|
||||
this.mounted = true;
|
||||
});
|
||||
console.timeEnd(`${ this.constructor.name } mount`);
|
||||
|
@ -159,12 +179,16 @@ export default class RoomView extends LoggedView {
|
|||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const {
|
||||
room, joined, lastOpen
|
||||
room, joined, lastOpen, photoModalVisible, reactionsModalVisible
|
||||
} = this.state;
|
||||
const { showActions, showErrorActions, appState } = this.props;
|
||||
|
||||
if (lastOpen !== nextState.lastOpen) {
|
||||
return true;
|
||||
} else if (photoModalVisible !== nextState.photoModalVisible) {
|
||||
return true;
|
||||
} else if (reactionsModalVisible !== nextState.reactionsModalVisible) {
|
||||
return true;
|
||||
} else if (room.ro !== nextState.room.ro) {
|
||||
return true;
|
||||
} else if (room.f !== nextState.room.f) {
|
||||
|
@ -285,6 +309,14 @@ export default class RoomView extends LoggedView {
|
|||
actionsShow({ ...message, rid: this.rid });
|
||||
}
|
||||
|
||||
onOpenFileModal = (attachment) => {
|
||||
this.setState({ selectedAttachment: attachment, photoModalVisible: true });
|
||||
}
|
||||
|
||||
onCloseFileModal = () => {
|
||||
this.setState({ selectedAttachment: {}, photoModalVisible: false });
|
||||
}
|
||||
|
||||
onReactionPress = (shortname, messageId) => {
|
||||
const { actionMessage, toggleReactionPicker } = this.props;
|
||||
try {
|
||||
|
@ -298,6 +330,15 @@ export default class RoomView extends LoggedView {
|
|||
}
|
||||
};
|
||||
|
||||
onReactionLongPress = (message) => {
|
||||
this.setState({ selectedMessage: message, reactionsModalVisible: true });
|
||||
vibrate();
|
||||
}
|
||||
|
||||
onCloseReactionsModal = () => {
|
||||
this.setState({ selectedMessage: {}, reactionsModalVisible: false });
|
||||
}
|
||||
|
||||
onDiscussionPress = debounce((item) => {
|
||||
const { navigation } = this.props;
|
||||
navigation.push('RoomView', {
|
||||
|
@ -305,6 +346,35 @@ export default class RoomView extends LoggedView {
|
|||
});
|
||||
}, 1000, true)
|
||||
|
||||
onThreadPress = debounce((item) => {
|
||||
const { navigation } = this.props;
|
||||
if (item.tmid) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
||||
});
|
||||
} else if (item.tlm) {
|
||||
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item._id, name: title, t: 'thread'
|
||||
});
|
||||
}
|
||||
}, 1000, true)
|
||||
|
||||
toggleReactionPicker = (message) => {
|
||||
const { toggleReactionPicker } = this.props;
|
||||
toggleReactionPicker(message);
|
||||
}
|
||||
|
||||
replyBroadcast = (message) => {
|
||||
const { replyBroadcast } = this.props;
|
||||
replyBroadcast(message);
|
||||
}
|
||||
|
||||
errorActionsShow = (message) => {
|
||||
const { errorActionsShow } = this.props;
|
||||
errorActionsShow(message);
|
||||
}
|
||||
|
||||
handleConnected = () => {
|
||||
this.init();
|
||||
EventEmitter.removeListener('connected', this.handleConnected);
|
||||
|
@ -365,7 +435,7 @@ export default class RoomView extends LoggedView {
|
|||
}
|
||||
}
|
||||
|
||||
setLastOpen = lastOpen => this.internalSetState({ lastOpen });
|
||||
setLastOpen = lastOpen => this.setState({ lastOpen });
|
||||
|
||||
joinRoom = async() => {
|
||||
try {
|
||||
|
@ -388,7 +458,7 @@ export default class RoomView extends LoggedView {
|
|||
isMuted = () => {
|
||||
const { room } = this.state;
|
||||
const { user } = this.props;
|
||||
return room && room.muted && !!Array.from(Object.keys(room.muted), i => room.muted[i].value).includes(user.username);
|
||||
return room && room.muted && room.muted.find && !!room.muted.find(m => m === user.username);
|
||||
}
|
||||
|
||||
isReadOnly = () => {
|
||||
|
@ -433,7 +503,9 @@ export default class RoomView extends LoggedView {
|
|||
|
||||
renderItem = (item, previousItem) => {
|
||||
const { room, lastOpen } = this.state;
|
||||
const { user, navigation } = this.props;
|
||||
const {
|
||||
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl
|
||||
} = this.props;
|
||||
let dateSeparator = null;
|
||||
let showUnreadSeparator = false;
|
||||
|
||||
|
@ -459,11 +531,21 @@ export default class RoomView extends LoggedView {
|
|||
status={item.status}
|
||||
_updatedAt={item._updatedAt}
|
||||
previousItem={previousItem}
|
||||
navigation={navigation}
|
||||
fetchThreadName={this.fetchThreadName}
|
||||
onReactionPress={this.onReactionPress}
|
||||
onReactionLongPress={this.onReactionLongPress}
|
||||
onLongPress={this.onMessageLongPress}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
onThreadPress={this.onThreadPress}
|
||||
onOpenFileModal={this.onOpenFileModal}
|
||||
toggleReactionPicker={this.toggleReactionPicker}
|
||||
replyBroadcast={this.replyBroadcast}
|
||||
errorActionsShow={this.errorActionsShow}
|
||||
baseUrl={baseUrl}
|
||||
Message_GroupingPeriod={Message_GroupingPeriod}
|
||||
timeFormat={Message_TimeFormat}
|
||||
useRealName={useRealName}
|
||||
useMarkdown={this.useMarkdown}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -548,7 +630,10 @@ export default class RoomView extends LoggedView {
|
|||
|
||||
render() {
|
||||
console.count(`${ this.constructor.name }.render calls`);
|
||||
const { room } = this.state;
|
||||
const {
|
||||
room, photoModalVisible, reactionsModalVisible, selectedAttachment, selectedMessage
|
||||
} = this.state;
|
||||
const { user, baseUrl } = this.props;
|
||||
const { rid, t } = room;
|
||||
|
||||
return (
|
||||
|
@ -559,6 +644,20 @@ export default class RoomView extends LoggedView {
|
|||
{this.renderActions()}
|
||||
<ReactionPicker onEmojiSelected={this.onReactionPress} />
|
||||
<UploadProgress rid={this.rid} />
|
||||
<FileModal
|
||||
attachment={selectedAttachment}
|
||||
isVisible={photoModalVisible}
|
||||
onClose={this.onCloseFileModal}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<ReactionsModal
|
||||
message={selectedMessage}
|
||||
isVisible={reactionsModalVisible}
|
||||
onClose={this.onCloseReactionsModal}
|
||||
user={user}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Toast ref={toast => this.toast = toast} />
|
||||
</SafeAreaView>
|
||||
);
|
||||
|
|
|
@ -66,6 +66,7 @@ export default class RoomsListView extends LoggedView {
|
|||
const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid');
|
||||
const onPressItem = navigation.getParam('onPressItem', () => {});
|
||||
const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {});
|
||||
const toggleUseMarkdown = navigation.getParam('toggleUseMarkdown', () => {});
|
||||
|
||||
return {
|
||||
headerLeft: (
|
||||
|
@ -75,7 +76,7 @@ export default class RoomsListView extends LoggedView {
|
|||
<Item title='cancel' iconName='cross' onPress={cancelSearchingAndroid} />
|
||||
</CustomHeaderButtons>
|
||||
)
|
||||
: <DrawerButton navigation={navigation} testID='rooms-list-view-sidebar' />
|
||||
: <DrawerButton navigation={navigation} testID='rooms-list-view-sidebar' onLongPress={toggleUseMarkdown} />
|
||||
),
|
||||
headerTitle: <RoomsListHeaderView />,
|
||||
headerRight: (
|
||||
|
@ -124,6 +125,7 @@ export default class RoomsListView extends LoggedView {
|
|||
searching: false,
|
||||
search: [],
|
||||
loading: true,
|
||||
useMarkdown: true,
|
||||
chats: [],
|
||||
unread: [],
|
||||
favorites: [],
|
||||
|
@ -142,7 +144,10 @@ export default class RoomsListView extends LoggedView {
|
|||
this.getSubscriptions();
|
||||
const { navigation } = this.props;
|
||||
navigation.setParams({
|
||||
onPressItem: this._onPressItem, initSearchingAndroid: this.initSearchingAndroid, cancelSearchingAndroid: this.cancelSearchingAndroid
|
||||
onPressItem: this._onPressItem,
|
||||
initSearchingAndroid: this.initSearchingAndroid,
|
||||
cancelSearchingAndroid: this.cancelSearchingAndroid,
|
||||
toggleUseMarkdown: this.toggleUseMarkdown
|
||||
});
|
||||
console.timeEnd(`${ this.constructor.name } mount`);
|
||||
}
|
||||
|
@ -311,6 +316,15 @@ export default class RoomsListView extends LoggedView {
|
|||
}
|
||||
}
|
||||
|
||||
// Just for tests purposes
|
||||
toggleUseMarkdown = () => {
|
||||
this.setState(({ useMarkdown }) => ({ useMarkdown: !useMarkdown }),
|
||||
() => {
|
||||
const { useMarkdown } = this.state;
|
||||
alert(`Markdown ${ useMarkdown ? 'enabled' : 'disabled' }`);
|
||||
});
|
||||
}
|
||||
|
||||
// this is necessary during development (enables Cmd + r)
|
||||
hasActiveDB = () => database && database.databases && database.databases.activeDB;
|
||||
|
||||
|
@ -341,9 +355,10 @@ export default class RoomsListView extends LoggedView {
|
|||
|
||||
goRoom = (item) => {
|
||||
this.cancelSearchingAndroid();
|
||||
const { useMarkdown } = this.state;
|
||||
const { navigation } = this.props;
|
||||
navigation.navigate('RoomView', {
|
||||
rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid
|
||||
rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid, useMarkdown
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,6 @@ import StatusBar from '../../containers/StatusBar';
|
|||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
|
@ -35,8 +34,7 @@ export default class SearchMessagesView extends LoggedView {
|
|||
static propTypes = {
|
||||
navigation: PropTypes.object,
|
||||
user: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
customEmojis: PropTypes.object
|
||||
baseUrl: PropTypes.string
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -96,10 +94,9 @@ export default class SearchMessagesView extends LoggedView {
|
|||
)
|
||||
|
||||
renderItem = ({ item }) => {
|
||||
const { user, customEmojis, baseUrl } = this.props;
|
||||
const { user, baseUrl } = this.props;
|
||||
return (
|
||||
<Message
|
||||
customEmojis={customEmojis}
|
||||
baseUrl={baseUrl}
|
||||
user={user}
|
||||
author={item.u}
|
||||
|
@ -107,8 +104,9 @@ export default class SearchMessagesView extends LoggedView {
|
|||
msg={item.msg}
|
||||
attachments={item.attachments || []}
|
||||
timeFormat='MMM Do YYYY, h:mm:ss a'
|
||||
edited={!!item.editedAt}
|
||||
header
|
||||
isEdited={!!item.editedAt}
|
||||
isHeader
|
||||
onOpenFileModal={() => {}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -145,7 +143,7 @@ export default class SearchMessagesView extends LoggedView {
|
|||
placeholder={I18n.t('Search_Messages')}
|
||||
testID='search-message-view-input'
|
||||
/>
|
||||
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' customEmojis={{}} />
|
||||
<Markdown msg={I18n.t('You_can_search_using_RegExp_eg')} username='' baseUrl='' />
|
||||
<View style={styles.divider} />
|
||||
</View>
|
||||
{this.renderList()}
|
||||
|
|
|
@ -24,12 +24,12 @@ const API_FETCH_COUNT = 50;
|
|||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
customEmojis: state.customEmojis,
|
||||
user: {
|
||||
id: state.login.user && state.login.user.id,
|
||||
username: state.login.user && state.login.user.username,
|
||||
token: state.login.user && state.login.user.token
|
||||
}
|
||||
},
|
||||
useRealName: state.settings.UI_Use_Real_Name
|
||||
}))
|
||||
/** @extends React.Component */
|
||||
export default class ThreadMessagesView extends LoggedView {
|
||||
|
@ -39,7 +39,9 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
|
||||
static propTypes = {
|
||||
user: PropTypes.object,
|
||||
navigation: PropTypes.object
|
||||
navigation: PropTypes.object,
|
||||
baseUrl: PropTypes.string,
|
||||
useRealName: PropTypes.bool
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
@ -82,6 +84,7 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
this.setState({ messages: this.messages });
|
||||
}, 300)
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
init = () => {
|
||||
const [room] = this.rooms;
|
||||
const lastThreadSync = new Date();
|
||||
|
@ -186,6 +189,20 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
}) : null
|
||||
)
|
||||
|
||||
onThreadPress = debounce((item) => {
|
||||
const { navigation } = this.props;
|
||||
if (item.tmid) {
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
|
||||
});
|
||||
} else if (item.tlm) {
|
||||
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
|
||||
navigation.push('RoomView', {
|
||||
rid: item.rid, tmid: item._id, name: title, t: 'thread'
|
||||
});
|
||||
}
|
||||
}, 1000, true)
|
||||
|
||||
renderSeparator = () => <Separator />
|
||||
|
||||
renderEmpty = () => (
|
||||
|
@ -195,7 +212,9 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
)
|
||||
|
||||
renderItem = ({ item }) => {
|
||||
const { user, navigation } = this.props;
|
||||
const {
|
||||
user, navigation, baseUrl, useRealName
|
||||
} = this.props;
|
||||
if (item.isValid && item.isValid()) {
|
||||
return (
|
||||
<Message
|
||||
|
@ -207,10 +226,11 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
status={item.status}
|
||||
_updatedAt={item._updatedAt}
|
||||
navigation={navigation}
|
||||
customTimeFormat='MMM D'
|
||||
timeFormat='MMM D'
|
||||
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
|
||||
fetchThreadName={this.fetchThreadName}
|
||||
onDiscussionPress={this.onDiscussionPress}
|
||||
onThreadPress={this.onThreadPress}
|
||||
baseUrl={baseUrl}
|
||||
useRealName={useRealName}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -39,10 +39,6 @@
|
|||
[self.window makeKeyAndVisible];
|
||||
[Fabric with:@[[Crashlytics class]]];
|
||||
|
||||
NSString *newAgent = @"Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1";
|
||||
NSDictionary *dictionary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
|
||||
|
||||
[RNSplashScreen show];
|
||||
|
||||
return YES;
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"react-native-screens": "^1.0.0-alpha.22",
|
||||
"react-native-scrollable-tab-view": "0.10.0",
|
||||
"react-native-slider": "^0.11.0",
|
||||
"react-native-slowlog": "^1.0.2",
|
||||
"react-native-splash-screen": "^3.2.0",
|
||||
"react-native-vector-icons": "^6.4.2",
|
||||
"react-native-video": "^4.4.1",
|
||||
|
|
|
@ -24,19 +24,27 @@ const author = {
|
|||
username: 'diego.mello'
|
||||
};
|
||||
const baseUrl = 'https://open.rocket.chat';
|
||||
const customEmojis = { react_rocket: 'png', nyan_rocket: 'png', marioparty: 'gif' };
|
||||
const date = new Date(2017, 10, 10, 10);
|
||||
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
|
||||
|
||||
const getCustomEmoji = (content) => {
|
||||
const customEmoji = {
|
||||
marioparty: { name: content, extension: 'gif' },
|
||||
react_rocket: { name: content, extension: 'png' },
|
||||
nyan_rocket: { name: content, extension: 'png' }
|
||||
}[content];
|
||||
return customEmoji;
|
||||
};
|
||||
|
||||
const Message = props => (
|
||||
<MessageComponent
|
||||
baseUrl={baseUrl}
|
||||
customEmojis={customEmojis}
|
||||
user={user}
|
||||
author={author}
|
||||
ts={date}
|
||||
timeFormat='LT'
|
||||
header
|
||||
isHeader
|
||||
getCustomEmoji={getCustomEmoji}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
@ -62,12 +70,12 @@ export default (
|
|||
username: longText
|
||||
}}
|
||||
/>
|
||||
<Message msg='This is the third message' header={false} />
|
||||
<Message msg='This is the second message' header={false} />
|
||||
<Message msg='This is the third message' isHeader={false} />
|
||||
<Message msg='This is the second message' isHeader={false} />
|
||||
<Message msg='This is the first message' />
|
||||
|
||||
<Separator title='Without header' />
|
||||
<Message msg='Message' header={false} />
|
||||
<Message msg='Message' isHeader={false} />
|
||||
|
||||
<Separator title='With alias' />
|
||||
<Message msg='Message' alias='Diego Mello' />
|
||||
|
@ -101,7 +109,21 @@ export default (
|
|||
/>
|
||||
|
||||
<Separator title='Mentions' />
|
||||
<Message msg='@rocket.cat @diego.mello @all @here #general' />
|
||||
<Message
|
||||
msg='@rocket.cat @diego.mello @all @here #general'
|
||||
mentions={[{
|
||||
username: 'rocket.cat'
|
||||
}, {
|
||||
username: 'diego.mello'
|
||||
}, {
|
||||
username: 'all'
|
||||
}, {
|
||||
username: 'here'
|
||||
}]}
|
||||
channels={[{
|
||||
name: 'general'
|
||||
}]}
|
||||
/>
|
||||
|
||||
<Separator title='Emojis' />
|
||||
<Message msg='👊🤙👏' />
|
||||
|
@ -194,7 +216,7 @@ export default (
|
|||
...author,
|
||||
username: 'rocket.cat'
|
||||
}}
|
||||
header={false}
|
||||
isHeader={false}
|
||||
/>
|
||||
<Message
|
||||
msg='Second message'
|
||||
|
@ -217,7 +239,7 @@ export default (
|
|||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
description: 'This is a description :nyan_rocket:',
|
||||
image_url: '/file-upload/sxLXBzjwuqxMnebyP/Clipboard%20-%2029%20de%20Agosto%20de%202018%20%C3%A0s%2018:10'
|
||||
}]}
|
||||
/>
|
||||
|
@ -226,7 +248,13 @@ export default (
|
|||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
description: 'This is a description :nyan_rocket:',
|
||||
video_url: '/file-upload/cqnKqb6kdajky5Rxj/WhatsApp%20Video%202018-08-22%20at%2019.09.55.mp4'
|
||||
}]}
|
||||
/>
|
||||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
video_url: '/file-upload/cqnKqb6kdajky5Rxj/WhatsApp%20Video%202018-08-22%20at%2019.09.55.mp4'
|
||||
}]}
|
||||
/>
|
||||
|
@ -235,32 +263,32 @@ export default (
|
|||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
description: 'This is a description :nyan_rocket:',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
/>
|
||||
<Message msg='First message' header={false} />
|
||||
<Message msg='First message' isHeader={false} />
|
||||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
header={false}
|
||||
isHeader={false}
|
||||
/>
|
||||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
header={false}
|
||||
isHeader={false}
|
||||
/>
|
||||
<Message
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
header={false}
|
||||
isHeader={false}
|
||||
/>
|
||||
|
||||
<Separator title='Message with reply' />
|
||||
|
@ -279,7 +307,7 @@ export default (
|
|||
author_name: 'rocket.cat',
|
||||
ts: date,
|
||||
timeFormat: 'LT',
|
||||
text: 'How are you?'
|
||||
text: 'How are you? :nyan_rocket:'
|
||||
}]}
|
||||
/>
|
||||
|
||||
|
@ -335,7 +363,7 @@ export default (
|
|||
tmsg='Thread with attachment'
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
description: 'This is a description :nyan_rocket:',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
isThreadReply
|
||||
|
@ -487,6 +515,22 @@ export default (
|
|||
description: 'Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for.'
|
||||
}]}
|
||||
/>
|
||||
<Message
|
||||
urls={[{
|
||||
url: 'https://google.com',
|
||||
title: 'Google',
|
||||
description: 'Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for.'
|
||||
}]}
|
||||
msg='Message :nyan_rocket:'
|
||||
/>
|
||||
<Message
|
||||
urls={[{
|
||||
url: 'https://google.com',
|
||||
title: 'Google',
|
||||
description: 'Search the world\'s information, including webpages, images, videos and more. Google has many special features to help you find exactly what you\'re looking for.'
|
||||
}]}
|
||||
isHeader={false}
|
||||
/>
|
||||
|
||||
<Separator title='Custom fields' />
|
||||
<Message
|
||||
|
@ -556,28 +600,29 @@ export default (
|
|||
<Message msg='This message is inside an archived room' archived />
|
||||
|
||||
<Separator title='Error' />
|
||||
<Message msg='This message has error too' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} header={false} />
|
||||
<Message msg='This message has error' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} />
|
||||
<Message hasError msg='This message has error' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} />
|
||||
<Message hasError msg='This message has error too' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} isHeader={false} />
|
||||
|
||||
<Separator title='Temp' />
|
||||
<Message msg='Temp message' status={messagesStatus.TEMP} />
|
||||
<Message msg='Temp message' status={messagesStatus.TEMP} isTemp />
|
||||
|
||||
<Separator title='Editing' />
|
||||
<Message msg='Message being edited' editing />
|
||||
|
||||
<Separator title='Removed' />
|
||||
<Message type='rm' />
|
||||
<Message type='rm' isInfo />
|
||||
|
||||
<Separator title='Joined' />
|
||||
<Message type='uj' />
|
||||
<Message type='uj' isInfo />
|
||||
|
||||
<Separator title='Room name changed' />
|
||||
<Message msg='New name' type='r' />
|
||||
<Message msg='New name' type='r' isInfo />
|
||||
|
||||
<Separator title='Message pinned' />
|
||||
<Message
|
||||
msg='New name'
|
||||
type='message_pinned'
|
||||
isInfo
|
||||
attachments={[{
|
||||
author_name: 'rocket.cat',
|
||||
ts: date,
|
||||
|
@ -587,25 +632,26 @@ export default (
|
|||
/>
|
||||
|
||||
<Separator title='Has left the channel' />
|
||||
<Message type='ul' />
|
||||
<Message type='ul' isInfo />
|
||||
|
||||
<Separator title='User removed' />
|
||||
<Message msg='rocket.cat' type='ru' />
|
||||
<Message msg='rocket.cat' type='ru' isInfo />
|
||||
|
||||
<Separator title='User added' />
|
||||
<Message msg='rocket.cat' type='au' />
|
||||
<Message msg='rocket.cat' type='au' isInfo />
|
||||
|
||||
<Separator title='User muted' />
|
||||
<Message msg='rocket.cat' type='user-muted' />
|
||||
<Message msg='rocket.cat' type='user-muted' isInfo />
|
||||
|
||||
<Separator title='User unmuted' />
|
||||
<Message msg='rocket.cat' type='user-unmuted' />
|
||||
<Message msg='rocket.cat' type='user-unmuted' isInfo />
|
||||
|
||||
<Separator title='Role added' />
|
||||
<Message
|
||||
msg='rocket.cat'
|
||||
role='admin' // eslint-disable-line
|
||||
type='subscription-role-added'
|
||||
isInfo
|
||||
/>
|
||||
|
||||
<Separator title='Role removed' />
|
||||
|
@ -613,19 +659,20 @@ export default (
|
|||
msg='rocket.cat'
|
||||
role='admin' // eslint-disable-line
|
||||
type='subscription-role-removed'
|
||||
isInfo
|
||||
/>
|
||||
|
||||
<Separator title='Changed description' />
|
||||
<Message msg='new description' type='room_changed_description' />
|
||||
<Message msg='new description' type='room_changed_description' isInfo />
|
||||
|
||||
<Separator title='Changed announcement' />
|
||||
<Message msg='new announcement' type='room_changed_announcement' />
|
||||
<Message msg='new announcement' type='room_changed_announcement' isInfo />
|
||||
|
||||
<Separator title='Changed topic' />
|
||||
<Message msg='new topic' type='room_changed_topic' />
|
||||
<Message msg='new topic' type='room_changed_topic' isInfo />
|
||||
|
||||
<Separator title='Changed type' />
|
||||
<Message msg='public' type='room_changed_privacy' />
|
||||
<Message msg='public' type='room_changed_privacy' isInfo />
|
||||
|
||||
<Separator title='Custom style' />
|
||||
<Message msg='Message' style={[styles.normalize, { backgroundColor: '#ddd' }]} />
|
||||
|
|
|
@ -10526,6 +10526,11 @@ react-native-slider@^0.11.0:
|
|||
dependencies:
|
||||
prop-types "^15.5.6"
|
||||
|
||||
react-native-slowlog@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-native-slowlog/-/react-native-slowlog-1.0.2.tgz#5520979e3ef9d5273495d431ff3be34f02e35c89"
|
||||
integrity sha1-VSCXnj751Sc0ldQx/zvjTwLjXIk=
|
||||
|
||||
react-native-splash-screen@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#d47ec8557b1ba988ee3ea98d01463081b60fff45"
|
||||
|
|
Loading…
Reference in New Issue