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() {
|
export function login() {
|
||||||
return {
|
return {
|
||||||
type: 'LOGIN'
|
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>
|
<CustomHeaderButtons left>
|
||||||
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} />
|
<Item title='drawer' iconName='customize' onPress={navigation.toggleDrawer} testID={testID} {...otherProps} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
));
|
));
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import moment from 'moment';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
import Markdown from '../message/Markdown';
|
import Markdown from '../message/Markdown';
|
||||||
|
import { getCustomEmoji } from '../message/utils';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
import { CustomIcon } from '../../lib/Icons';
|
||||||
import sharedStyles from '../../views/Styles';
|
import sharedStyles from '../../views/Styles';
|
||||||
import {
|
import {
|
||||||
|
@ -49,7 +50,6 @@ const styles = StyleSheet.create({
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
Message_TimeFormat: state.settings.Message_TimeFormat,
|
Message_TimeFormat: state.settings.Message_TimeFormat,
|
||||||
customEmojis: state.customEmojis,
|
|
||||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
|
||||||
}))
|
}))
|
||||||
export default class ReplyPreview extends Component {
|
export default class ReplyPreview extends Component {
|
||||||
|
@ -57,7 +57,6 @@ export default class ReplyPreview extends Component {
|
||||||
message: PropTypes.object.isRequired,
|
message: PropTypes.object.isRequired,
|
||||||
Message_TimeFormat: PropTypes.string.isRequired,
|
Message_TimeFormat: PropTypes.string.isRequired,
|
||||||
close: PropTypes.func.isRequired,
|
close: PropTypes.func.isRequired,
|
||||||
customEmojis: PropTypes.object.isRequired,
|
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string.isRequired,
|
||||||
username: PropTypes.string.isRequired
|
username: PropTypes.string.isRequired
|
||||||
}
|
}
|
||||||
|
@ -73,7 +72,7 @@ export default class ReplyPreview extends Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
message, Message_TimeFormat, customEmojis, baseUrl, username
|
message, Message_TimeFormat, baseUrl, username
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const time = moment(message.ts).format(Message_TimeFormat);
|
const time = moment(message.ts).format(Message_TimeFormat);
|
||||||
return (
|
return (
|
||||||
|
@ -83,7 +82,7 @@ export default class ReplyPreview extends Component {
|
||||||
<Text style={styles.username}>{message.u.username}</Text>
|
<Text style={styles.username}>{message.u.username}</Text>
|
||||||
<Text style={styles.time}>{time}</Text>
|
<Text style={styles.time}>{time}</Text>
|
||||||
</View>
|
</View>
|
||||||
<Markdown msg={message.msg} customEmojis={customEmojis} baseUrl={baseUrl} username={username} />
|
<Markdown msg={message.msg} baseUrl={baseUrl} username={username} getCustomEmoji={getCustomEmoji} />
|
||||||
</View>
|
</View>
|
||||||
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
<CustomIcon name='cross' color={COLOR_TEXT_DESCRIPTION} size={20} style={styles.close} onPress={this.close} />
|
||||||
</View>
|
</View>
|
||||||
|
|
|
@ -179,6 +179,7 @@ export default class UploadModal extends Component {
|
||||||
animationOut='fadeOut'
|
animationOut='fadeOut'
|
||||||
useNativeDriver
|
useNativeDriver
|
||||||
hideModalContentWhileAnimating
|
hideModalContentWhileAnimating
|
||||||
|
avoidKeyboard
|
||||||
>
|
>
|
||||||
<View style={[styles.container, { width: width - 32 }]}>
|
<View style={[styles.container, { width: width - 32 }]}>
|
||||||
<View style={styles.titleContainer}>
|
<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 = {
|
const BUTTON_HIT_SLOP = {
|
||||||
top: 12, right: 12, bottom: 12, left: 12
|
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 {
|
export default class Audio extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
file: PropTypes.object.isRequired,
|
file: PropTypes.object.isRequired,
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string.isRequired,
|
||||||
user: PropTypes.object.isRequired,
|
user: PropTypes.object.isRequired,
|
||||||
customEmojis: PropTypes.object.isRequired
|
useMarkdown: PropTypes.bool,
|
||||||
|
getCustomEmoji: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(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;
|
const { baseUrl, file, user } = props;
|
||||||
this.state = {
|
this.state = {
|
||||||
currentTime: 0,
|
currentTime: 0,
|
||||||
|
@ -120,22 +140,26 @@ export default class Audio extends React.Component {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getDuration = () => {
|
get duration() {
|
||||||
const { duration } = this.state;
|
const { duration } = this.state;
|
||||||
return formatTime(duration);
|
return formatTime(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setRef = ref => this.player = ref;
|
||||||
|
|
||||||
togglePlayPause = () => {
|
togglePlayPause = () => {
|
||||||
const { paused } = this.state;
|
const { paused } = this.state;
|
||||||
this.setState({ paused: !paused });
|
this.setState({ paused: !paused });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onValueChange = value => this.setState({ currentTime: value });
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
uri, paused, currentTime, duration
|
uri, paused, currentTime, duration
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const {
|
const {
|
||||||
user, baseUrl, customEmojis, file
|
user, baseUrl, file, getCustomEmoji, useMarkdown
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { description } = file;
|
const { description } = file;
|
||||||
|
|
||||||
|
@ -144,12 +168,10 @@ export default class Audio extends React.Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
[
|
<React.Fragment>
|
||||||
<View key='audio' style={styles.audioContainer}>
|
<View style={styles.audioContainer}>
|
||||||
<Video
|
<Video
|
||||||
ref={(ref) => {
|
ref={this.setRef}
|
||||||
this.player = ref;
|
|
||||||
}}
|
|
||||||
source={{ uri }}
|
source={{ uri }}
|
||||||
onLoad={this.onLoad}
|
onLoad={this.onLoad}
|
||||||
onProgress={this.onProgress}
|
onProgress={this.onProgress}
|
||||||
|
@ -157,39 +179,24 @@ export default class Audio extends React.Component {
|
||||||
paused={paused}
|
paused={paused}
|
||||||
repeat={false}
|
repeat={false}
|
||||||
/>
|
/>
|
||||||
<Touchable
|
<Button paused={paused} onPress={this.togglePlayPause} />
|
||||||
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>
|
|
||||||
<Slider
|
<Slider
|
||||||
style={styles.slider}
|
style={styles.slider}
|
||||||
value={currentTime}
|
value={currentTime}
|
||||||
maximumValue={duration}
|
maximumValue={duration}
|
||||||
minimumValue={0}
|
minimumValue={0}
|
||||||
animateTransitions
|
animateTransitions
|
||||||
animationConfig={{
|
animationConfig={sliderAnimationConfig}
|
||||||
duration: 250,
|
|
||||||
easing: Easing.linear,
|
|
||||||
delay: 0
|
|
||||||
}}
|
|
||||||
thumbTintColor={COLOR_PRIMARY}
|
thumbTintColor={COLOR_PRIMARY}
|
||||||
minimumTrackTintColor={COLOR_PRIMARY}
|
minimumTrackTintColor={COLOR_PRIMARY}
|
||||||
onValueChange={value => this.setState({ currentTime: value })}
|
onValueChange={this.onValueChange}
|
||||||
thumbStyle={styles.thumbStyle}
|
thumbStyle={styles.thumbStyle}
|
||||||
trackStyle={styles.trackStyle}
|
trackStyle={styles.trackStyle}
|
||||||
/>
|
/>
|
||||||
<Text style={styles.duration}>{this.getDuration()}</Text>
|
<Text style={styles.duration}>{this.duration}</Text>
|
||||||
</View>,
|
</View>
|
||||||
<Markdown key='description' msg={description} baseUrl={baseUrl} customEmojis={customEmojis} username={user.username} />
|
<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 React from 'react';
|
||||||
import { Text, ViewPropTypes } from 'react-native';
|
import { Text } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { emojify } from 'react-emojione';
|
import { emojify } from 'react-emojione';
|
||||||
|
|
||||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||||
|
|
||||||
export default class Emoji extends React.PureComponent {
|
const Emoji = React.memo(({
|
||||||
static propTypes = {
|
content, standardEmojiStyle, customEmojiStyle, baseUrl, getCustomEmoji
|
||||||
content: PropTypes.string.isRequired,
|
}) => {
|
||||||
baseUrl: PropTypes.string.isRequired,
|
const parsedContent = content.replace(/^:|:$/g, '');
|
||||||
standardEmojiStyle: Text.propTypes.style,
|
const emoji = getCustomEmoji(parsedContent);
|
||||||
customEmojiStyle: ViewPropTypes.style,
|
if (emoji) {
|
||||||
customEmojis: PropTypes.oneOfType([
|
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
||||||
PropTypes.array,
|
|
||||||
PropTypes.object
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
return <Text style={standardEmojiStyle}>{ emojify(content, { output: 'unicode' }) }</Text>;
|
||||||
|
}, () => true);
|
||||||
|
|
||||||
render() {
|
Emoji.propTypes = {
|
||||||
const {
|
content: PropTypes.string,
|
||||||
content, standardEmojiStyle, customEmojiStyle, customEmojis, baseUrl
|
standardEmojiStyle: PropTypes.object,
|
||||||
} = this.props;
|
customEmojiStyle: PropTypes.object,
|
||||||
const parsedContent = content.replace(/^:|:$/g, '');
|
baseUrl: PropTypes.string,
|
||||||
const emojiExtension = customEmojis[parsedContent];
|
getCustomEmoji: PropTypes.func
|
||||||
if (emojiExtension) {
|
};
|
||||||
const emoji = { extension: emojiExtension, content: parsedContent };
|
Emoji.displayName = 'MessageEmoji';
|
||||||
return <CustomEmoji key={content} baseUrl={baseUrl} style={customEmojiStyle} emoji={emoji} />;
|
|
||||||
}
|
export default Emoji;
|
||||||
return <Text style={standardEmojiStyle}>{ emojify(`${ content }`, { output: 'unicode' }) }</Text>;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,95 +1,79 @@
|
||||||
import React, { Component } from 'react';
|
import React from 'react';
|
||||||
|
import { View } from 'react-native';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import FastImage from 'react-native-fast-image';
|
import FastImage from 'react-native-fast-image';
|
||||||
import equal from 'deep-equal';
|
import equal from 'deep-equal';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
|
||||||
import PhotoModal from './PhotoModal';
|
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
|
import { formatAttachmentUrl } from '../../lib/utils';
|
||||||
|
|
||||||
export default class extends Component {
|
const Button = React.memo(({ children, onPress }) => (
|
||||||
static propTypes = {
|
<Touchable
|
||||||
file: PropTypes.object.isRequired,
|
onPress={onPress}
|
||||||
baseUrl: PropTypes.string.isRequired,
|
style={styles.imageContainer}
|
||||||
user: PropTypes.object.isRequired,
|
background={Touchable.Ripple('#fff')}
|
||||||
customEmojis: PropTypes.oneOfType([
|
>
|
||||||
PropTypes.array,
|
{children}
|
||||||
PropTypes.object
|
</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 };
|
const onPress = () => onOpenFileModal(file);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (file.description) {
|
||||||
return (
|
return (
|
||||||
[
|
<Button onPress={onPress}>
|
||||||
<Touchable
|
<View>
|
||||||
key='image'
|
<Image img={img} />
|
||||||
onPress={this.onPressButton}
|
<Markdown msg={file.description} baseUrl={baseUrl} username={user.username} getCustomEmoji={getCustomEmoji} useMarkdown={useMarkdown} />
|
||||||
style={styles.imageContainer}
|
</View>
|
||||||
background={Touchable.Ripple('#fff')}
|
</Button>
|
||||||
>
|
|
||||||
<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 })}
|
|
||||||
/>
|
|
||||||
]
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
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 { emojify } from 'react-emojione';
|
||||||
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
|
import MarkdownRenderer, { PluginContainer } from 'react-native-markdown-renderer';
|
||||||
import MarkdownFlowdock from 'markdown-it-flowdock';
|
import MarkdownFlowdock from 'markdown-it-flowdock';
|
||||||
|
|
||||||
import styles from './styles';
|
import styles from './styles';
|
||||||
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
import CustomEmoji from '../EmojiPicker/CustomEmoji';
|
||||||
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
|
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>
|
// Support <http://link|Text>
|
||||||
const formatText = text => text.replace(
|
const formatText = text => text.replace(
|
||||||
|
@ -15,7 +21,7 @@ const formatText = text => text.replace(
|
||||||
);
|
);
|
||||||
|
|
||||||
const Markdown = React.memo(({
|
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) {
|
if (!msg) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -28,14 +34,18 @@ const Markdown = React.memo(({
|
||||||
if (numberOfLines > 0) {
|
if (numberOfLines > 0) {
|
||||||
m = m.replace(/[\n]+/g, '\n').trim();
|
m = m.replace(/[\n]+/g, '\n').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useMarkdown) {
|
||||||
|
return <Text style={styles.text}>{m}</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MarkdownRenderer
|
<MarkdownRenderer
|
||||||
rules={{
|
rules={{
|
||||||
paragraph: (node, children) => (
|
paragraph: (node, children) => (
|
||||||
// eslint-disable-next-line
|
|
||||||
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
||||||
{children}
|
{children}
|
||||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
{isEdited ? <Text style={styles.edited}> ({I18n.t('edited')})</Text> : null}
|
||||||
</Text>
|
</Text>
|
||||||
),
|
),
|
||||||
mention: (node) => {
|
mention: (node) => {
|
||||||
|
@ -52,23 +62,31 @@ const Markdown = React.memo(({
|
||||||
...styles.mentionLoggedUser
|
...styles.mentionLoggedUser
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
if (mentions && mentions.length && mentions.findIndex(mention => mention.username === content) !== -1) {
|
||||||
<Text style={mentionStyle} key={key}>
|
return (
|
||||||
{content}
|
<Text style={mentionStyle} key={key}>
|
||||||
</Text>
|
{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) => {
|
emoji: (node) => {
|
||||||
if (node.children && node.children.length && node.children[0].content) {
|
if (node.children && node.children.length && node.children[0].content) {
|
||||||
const { content } = node.children[0];
|
const { content } = node.children[0];
|
||||||
const emojiExtension = customEmojis[content];
|
const emoji = getCustomEmoji && getCustomEmoji(content);
|
||||||
if (emojiExtension) {
|
if (emoji) {
|
||||||
const emoji = { extension: emojiExtension, content };
|
|
||||||
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
return <CustomEmoji key={node.key} baseUrl={baseUrl} style={styles.customEmoji} emoji={emoji} />;
|
||||||
}
|
}
|
||||||
return <Text key={node.key}>:{content}:</Text>;
|
return <Text key={node.key}>:{content}:</Text>;
|
||||||
|
@ -90,10 +108,7 @@ const Markdown = React.memo(({
|
||||||
link: styles.link,
|
link: styles.link,
|
||||||
...style
|
...style
|
||||||
}}
|
}}
|
||||||
plugins={[
|
plugins={plugins}
|
||||||
new PluginContainer(MarkdownFlowdock),
|
|
||||||
new PluginContainer(MarkdownEmojiPlugin)
|
|
||||||
]}
|
|
||||||
>{m}
|
>{m}
|
||||||
</MarkdownRenderer>
|
</MarkdownRenderer>
|
||||||
);
|
);
|
||||||
|
@ -101,13 +116,17 @@ const Markdown = React.memo(({
|
||||||
|
|
||||||
Markdown.propTypes = {
|
Markdown.propTypes = {
|
||||||
msg: PropTypes.string,
|
msg: PropTypes.string,
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string,
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string,
|
||||||
customEmojis: PropTypes.object.isRequired,
|
|
||||||
style: PropTypes.any,
|
style: PropTypes.any,
|
||||||
rules: PropTypes.object,
|
rules: PropTypes.object,
|
||||||
edited: PropTypes.bool,
|
isEdited: PropTypes.bool,
|
||||||
numberOfLines: PropTypes.number
|
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;
|
export default Markdown;
|
||||||
|
|
|
@ -1,609 +1,133 @@
|
||||||
import React, { PureComponent } from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import {
|
import { View } from 'react-native';
|
||||||
View, Text, ViewPropTypes, TouchableWithoutFeedback
|
|
||||||
} from 'react-native';
|
|
||||||
import moment from 'moment';
|
|
||||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
|
||||||
import Touchable from 'react-native-platform-touchable';
|
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 User from './User';
|
||||||
import Avatar from '../Avatar';
|
import MessageError from './MessageError';
|
||||||
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 styles from './styles';
|
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 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 = [
|
const MessageInner = React.memo((props) => {
|
||||||
'r',
|
if (props.type === 'discussion-created') {
|
||||||
'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');
|
|
||||||
return (
|
return (
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
|
<User {...props} />
|
||||||
<Text style={styles.text}>{msg}</Text>
|
<Discussion {...props} />
|
||||||
<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>
|
</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 Message = React.memo((props) => {
|
||||||
const {
|
if (props.isThreadReply || props.isThreadSequential || props.isInfo) {
|
||||||
tcount, tlm, onThreadPress, msg
|
const thread = props.isThreadReply ? <RepliedThread isTemp={props.isTemp} {...props} /> : null;
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (!tlm) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const time = this.formatLastMessage(tlm);
|
|
||||||
const buttonText = this.formatMessageCount(tcount, 'thread');
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.buttonContainer}>
|
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||||
<Touchable
|
{thread}
|
||||||
onPress={onThreadPress}
|
<View style={[styles.flex, sharedStyles.alignItemsCenter]}>
|
||||||
background={Touchable.Ripple('#fff')}
|
<MessageAvatar small {...props} />
|
||||||
style={[styles.button, styles.smallButton]}
|
<View
|
||||||
hitSlop={BUTTON_HIT_SLOP}
|
style={[
|
||||||
testID={`message-thread-button-${ msg }`}
|
styles.messageContent,
|
||||||
>
|
props.isHeader && styles.messageContentWithHeader,
|
||||||
<React.Fragment>
|
props.hasError && props.isHeader && styles.messageContentWithHeader,
|
||||||
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
|
props.hasError && !props.isHeader && styles.messageContentWithError
|
||||||
<Text style={styles.buttonText}>{buttonText}</Text>
|
]}
|
||||||
</React.Fragment>
|
>
|
||||||
</Touchable>
|
<Content {...props} />
|
||||||
<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>
|
</View>
|
||||||
</React.Fragment>
|
</View>
|
||||||
);
|
</View>
|
||||||
}
|
);
|
||||||
return (
|
}
|
||||||
|
return (
|
||||||
|
<View style={[styles.container, props.style, props.isTemp && styles.temp]}>
|
||||||
<View style={styles.flex}>
|
<View style={styles.flex}>
|
||||||
{this.renderAvatar()}
|
<MessageAvatar {...props} />
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
styles.messageContent,
|
styles.messageContent,
|
||||||
header && styles.messageContentWithHeader,
|
props.isHeader && styles.messageContentWithHeader,
|
||||||
this.hasError() && header && styles.messageContentWithHeader,
|
props.hasError && props.isHeader && styles.messageContentWithHeader,
|
||||||
this.hasError() && !header && styles.messageContentWithError,
|
props.hasError && !props.isHeader && styles.messageContentWithError
|
||||||
this.isTemp() && styles.temp
|
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
{this.renderInner()}
|
<MessageInner {...props} />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
);
|
</View>
|
||||||
}
|
);
|
||||||
|
});
|
||||||
render() {
|
Message.displayName = 'Message';
|
||||||
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 });
|
|
||||||
|
|
||||||
|
const MessageTouchable = React.memo((props) => {
|
||||||
|
if (props.hasError) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.root}>
|
<View style={styles.root}>
|
||||||
{this.renderError()}
|
<MessageError {...props} />
|
||||||
<TouchableWithoutFeedback
|
<Message {...props} />
|
||||||
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>
|
|
||||||
</View>
|
</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 PropTypes from 'prop-types';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
import isEqual from 'deep-equal';
|
||||||
|
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import openLink from '../../utils/openLink';
|
import openLink from '../../utils/openLink';
|
||||||
|
@ -69,98 +70,130 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onPress = (attachment, baseUrl, user) => {
|
const Title = React.memo(({ attachment, timeFormat }) => {
|
||||||
let url = attachment.title_link || attachment.author_link;
|
if (!attachment.author_name) {
|
||||||
if (!url) {
|
return null;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
if (attachment.type === 'file') {
|
const time = attachment.ts ? moment(attachment.ts).format(timeFormat) : null;
|
||||||
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
return (
|
||||||
}
|
<View style={styles.authorContainer}>
|
||||||
openLink(url);
|
{attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null}
|
||||||
};
|
{time ? <Text style={styles.time}>{ time }</Text> : null}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}, () => true);
|
||||||
|
|
||||||
const Reply = ({
|
const Description = React.memo(({
|
||||||
attachment, timeFormat, baseUrl, customEmojis, user, index
|
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) {
|
if (!attachment) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderAuthor = () => (
|
const onPress = () => {
|
||||||
attachment.author_name ? <Text style={styles.author}>{attachment.author_name}</Text> : null
|
let url = attachment.title_link || attachment.author_link;
|
||||||
);
|
if (!url) {
|
||||||
|
return;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
return (
|
if (attachment.type === 'file') {
|
||||||
<View style={styles.authorContainer}>
|
url = `${ baseUrl }${ url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||||
{renderAuthor()}
|
|
||||||
{renderTime()}
|
|
||||||
</View>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderText = () => {
|
|
||||||
const text = attachment.text || attachment.title;
|
|
||||||
if (text) {
|
|
||||||
return (
|
|
||||||
<Markdown
|
|
||||||
msg={text}
|
|
||||||
customEmojis={customEmojis}
|
|
||||||
baseUrl={baseUrl}
|
|
||||||
username={user.username}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
};
|
openLink(url);
|
||||||
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Touchable
|
<Touchable
|
||||||
onPress={() => onPress(attachment, baseUrl, user)}
|
onPress={onPress}
|
||||||
style={[styles.button, index > 0 && styles.marginTop]}
|
style={[styles.button, index > 0 && styles.marginTop]}
|
||||||
background={Touchable.Ripple('#fff')}
|
background={Touchable.Ripple('#fff')}
|
||||||
>
|
>
|
||||||
<View style={styles.attachmentContainer}>
|
<View style={styles.attachmentContainer}>
|
||||||
{renderTitle()}
|
<Title attachment={attachment} timeFormat={timeFormat} />
|
||||||
{renderText()}
|
<Description
|
||||||
{renderFields()}
|
attachment={attachment}
|
||||||
|
timeFormat={timeFormat}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
user={user}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
|
useMarkdown={useMarkdown}
|
||||||
|
/>
|
||||||
|
<Fields attachment={attachment} />
|
||||||
</View>
|
</View>
|
||||||
</Touchable>
|
</Touchable>
|
||||||
);
|
);
|
||||||
};
|
}, (prevProps, nextProps) => isEqual(prevProps.attachment, nextProps.attachment));
|
||||||
|
|
||||||
Reply.propTypes = {
|
Reply.propTypes = {
|
||||||
attachment: PropTypes.object.isRequired,
|
attachment: PropTypes.object,
|
||||||
timeFormat: PropTypes.string.isRequired,
|
timeFormat: PropTypes.string,
|
||||||
baseUrl: PropTypes.string.isRequired,
|
baseUrl: PropTypes.string,
|
||||||
customEmojis: PropTypes.object.isRequired,
|
user: PropTypes.object,
|
||||||
user: PropTypes.object.isRequired,
|
index: PropTypes.number,
|
||||||
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;
|
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 }`;
|
image = image.includes('http') ? image : `${ baseUrl }/${ image }?rc_uid=${ user.id }&rc_token=${ user.token }`;
|
||||||
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
return <FastImage source={{ uri: image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} />;
|
||||||
});
|
}, (prevProps, nextProps) => prevProps.image === nextProps.image);
|
||||||
|
|
||||||
const UrlContent = React.memo(({ title, description }) => (
|
const UrlContent = React.memo(({ title, description }) => (
|
||||||
<View style={styles.textContainer}>
|
<View style={styles.textContainer}>
|
||||||
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
{title ? <Text style={styles.title} numberOfLines={2}>{title}</Text> : null}
|
||||||
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
{description ? <Text style={styles.description} numberOfLines={2}>{description}</Text> : null}
|
||||||
</View>
|
</View>
|
||||||
));
|
), (prevProps, nextProps) => {
|
||||||
|
if (prevProps.title !== nextProps.title) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (prevProps.description !== nextProps.description) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
const Url = React.memo(({
|
const Url = React.memo(({
|
||||||
url, index, user, baseUrl
|
url, index, user, baseUrl
|
||||||
|
@ -89,16 +97,28 @@ const Url = React.memo(({
|
||||||
);
|
);
|
||||||
}, (oldProps, newProps) => isEqual(oldProps.url, newProps.url));
|
}, (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 = {
|
UrlImage.propTypes = {
|
||||||
image: PropTypes.string,
|
image: PropTypes.string,
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
baseUrl: PropTypes.string
|
baseUrl: PropTypes.string
|
||||||
};
|
};
|
||||||
|
UrlImage.displayName = 'MessageUrlImage';
|
||||||
|
|
||||||
UrlContent.propTypes = {
|
UrlContent.propTypes = {
|
||||||
title: PropTypes.string,
|
title: PropTypes.string,
|
||||||
description: PropTypes.string
|
description: PropTypes.string
|
||||||
};
|
};
|
||||||
|
UrlContent.displayName = 'MessageUrlContent';
|
||||||
|
|
||||||
Url.propTypes = {
|
Url.propTypes = {
|
||||||
url: PropTypes.object.isRequired,
|
url: PropTypes.object.isRequired,
|
||||||
|
@ -106,5 +126,13 @@ Url.propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
baseUrl: PropTypes.string
|
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 {
|
const User = React.memo(({
|
||||||
static propTypes = {
|
isHeader, useRealName, author, alias, ts, timeFormat
|
||||||
timeFormat: PropTypes.string.isRequired,
|
}) => {
|
||||||
username: PropTypes.string,
|
if (isHeader) {
|
||||||
alias: PropTypes.string,
|
const username = (useRealName && author.name) || author.username;
|
||||||
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 aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
const aliasUsername = alias ? (<Text style={styles.alias}> @{username}</Text>) : null;
|
||||||
const time = moment(ts).format(timeFormat);
|
const time = moment(ts).format(timeFormat);
|
||||||
|
|
||||||
|
@ -67,4 +50,17 @@ export default class User extends React.PureComponent {
|
||||||
</View>
|
</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 React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { StyleSheet, View } from 'react-native';
|
import { StyleSheet } from 'react-native';
|
||||||
import Modal from 'react-native-modal';
|
|
||||||
import VideoPlayer from 'react-native-video-controls';
|
|
||||||
import Touchable from 'react-native-platform-touchable';
|
import Touchable from 'react-native-platform-touchable';
|
||||||
|
import isEqual from 'deep-equal';
|
||||||
|
|
||||||
import Markdown from './Markdown';
|
import Markdown from './Markdown';
|
||||||
import openLink from '../../utils/openLink';
|
import openLink from '../../utils/openLink';
|
||||||
import { isIOS } from '../../utils/deviceInfo';
|
import { isIOS } from '../../utils/deviceInfo';
|
||||||
import { CustomIcon } from '../../lib/Icons';
|
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 SUPPORTED_TYPES = ['video/quicktime', 'video/mp4', ...(isIOS ? [] : ['video/webm', 'video/3gp', 'video/mkv'])];
|
||||||
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
const isTypeSupported = type => SUPPORTED_TYPES.indexOf(type) !== -1;
|
||||||
|
@ -32,77 +32,46 @@ const styles = StyleSheet.create({
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default class Video extends React.PureComponent {
|
const Video = React.memo(({
|
||||||
static propTypes = {
|
file, baseUrl, user, useMarkdown, onOpenFileModal, getCustomEmoji
|
||||||
file: PropTypes.object.isRequired,
|
}) => {
|
||||||
baseUrl: PropTypes.string.isRequired,
|
if (!baseUrl) {
|
||||||
user: PropTypes.object.isRequired,
|
return null;
|
||||||
customEmojis: PropTypes.object.isRequired
|
|
||||||
}
|
}
|
||||||
|
|
||||||
state = { isVisible: false };
|
const onPress = () => {
|
||||||
|
|
||||||
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;
|
|
||||||
if (isTypeSupported(file.video_type)) {
|
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() {
|
return (
|
||||||
const { isVisible } = this.state;
|
<React.Fragment>
|
||||||
const {
|
<Touchable
|
||||||
baseUrl, user, customEmojis, file
|
onPress={onPress}
|
||||||
} = this.props;
|
style={styles.button}
|
||||||
const { description } = file;
|
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) {
|
Video.propTypes = {
|
||||||
return null;
|
file: PropTypes.object,
|
||||||
}
|
baseUrl: PropTypes.string,
|
||||||
|
user: PropTypes.object,
|
||||||
|
useMarkdown: PropTypes.bool,
|
||||||
|
onOpenFileModal: PropTypes.func,
|
||||||
|
getCustomEmoji: PropTypes.func
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
export default Video;
|
||||||
[
|
|
||||||
<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>
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
export const DISCUSSION = 'discussion';
|
||||||
|
export const THREAD = 'thread';
|
|
@ -1,30 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import { ViewPropTypes } from 'react-native';
|
import { ViewPropTypes } from 'react-native';
|
||||||
import { connect } from 'react-redux';
|
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||||
import equal from 'deep-equal';
|
|
||||||
|
|
||||||
import Message from './Message';
|
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 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 {
|
export default class MessageContainer extends React.Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
item: PropTypes.object.isRequired,
|
item: PropTypes.object.isRequired,
|
||||||
|
@ -33,31 +16,28 @@ export default class MessageContainer extends React.Component {
|
||||||
username: PropTypes.string.isRequired,
|
username: PropTypes.string.isRequired,
|
||||||
token: PropTypes.string.isRequired
|
token: PropTypes.string.isRequired
|
||||||
}),
|
}),
|
||||||
customTimeFormat: PropTypes.string,
|
timeFormat: PropTypes.string,
|
||||||
customThreadTimeFormat: PropTypes.string,
|
customThreadTimeFormat: PropTypes.string,
|
||||||
style: ViewPropTypes.style,
|
style: ViewPropTypes.style,
|
||||||
archived: PropTypes.bool,
|
archived: PropTypes.bool,
|
||||||
broadcast: PropTypes.bool,
|
broadcast: PropTypes.bool,
|
||||||
previousItem: PropTypes.object,
|
previousItem: PropTypes.object,
|
||||||
_updatedAt: PropTypes.instanceOf(Date),
|
_updatedAt: PropTypes.instanceOf(Date),
|
||||||
// redux
|
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
customEmojis: PropTypes.object,
|
|
||||||
Message_GroupingPeriod: PropTypes.number,
|
Message_GroupingPeriod: PropTypes.number,
|
||||||
Message_TimeFormat: PropTypes.string,
|
|
||||||
editingMessage: PropTypes.object,
|
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
|
useMarkdown: PropTypes.bool,
|
||||||
status: PropTypes.number,
|
status: PropTypes.number,
|
||||||
navigation: PropTypes.object,
|
|
||||||
// methods - props
|
|
||||||
onLongPress: PropTypes.func,
|
onLongPress: PropTypes.func,
|
||||||
onReactionPress: PropTypes.func,
|
onReactionPress: PropTypes.func,
|
||||||
onDiscussionPress: PropTypes.func,
|
onDiscussionPress: PropTypes.func,
|
||||||
// methods - redux
|
onThreadPress: PropTypes.func,
|
||||||
errorActionsShow: PropTypes.func,
|
errorActionsShow: PropTypes.func,
|
||||||
replyBroadcast: PropTypes.func,
|
replyBroadcast: PropTypes.func,
|
||||||
toggleReactionPicker: PropTypes.func,
|
toggleReactionPicker: PropTypes.func,
|
||||||
fetchThreadName: PropTypes.func
|
fetchThreadName: PropTypes.func,
|
||||||
|
onOpenFileModal: PropTypes.func,
|
||||||
|
onReactionLongPress: PropTypes.func
|
||||||
}
|
}
|
||||||
|
|
||||||
static defaultProps = {
|
static defaultProps = {
|
||||||
|
@ -67,21 +47,11 @@ export default class MessageContainer extends React.Component {
|
||||||
broadcast: false
|
broadcast: false
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
shouldComponentUpdate(nextProps) {
|
||||||
super(props);
|
|
||||||
this.state = { reactionsModal: false };
|
|
||||||
this.closeReactions = this.closeReactions.bind(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
|
||||||
const { reactionsModal } = this.state;
|
|
||||||
const {
|
const {
|
||||||
status, editingMessage, item, _updatedAt, navigation
|
status, item, _updatedAt, previousItem
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
|
||||||
if (reactionsModal !== nextState.reactionsModal) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (status !== nextProps.status) {
|
if (status !== nextProps.status) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -89,65 +59,68 @@ export default class MessageContainer extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
|
if (!previousItem && !!nextProps.previousItem) {
|
||||||
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
|
return true;
|
||||||
return true;
|
|
||||||
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
|
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPress = debounce(() => {
|
||||||
|
const { item } = this.props;
|
||||||
|
KeyboardUtils.dismiss();
|
||||||
|
|
||||||
|
if ((item.tlm || item.tmid)) {
|
||||||
|
this.onThreadPress();
|
||||||
|
}
|
||||||
|
}, 300, true);
|
||||||
|
|
||||||
onLongPress = () => {
|
onLongPress = () => {
|
||||||
const { onLongPress } = this.props;
|
const { archived, onLongPress } = this.props;
|
||||||
onLongPress(this.parseMessage());
|
if (this.isInfo || this.hasError || archived) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (onLongPress) {
|
||||||
|
onLongPress(this.parseMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onErrorPress = () => {
|
onErrorPress = () => {
|
||||||
const { errorActionsShow } = this.props;
|
const { errorActionsShow } = this.props;
|
||||||
errorActionsShow(this.parseMessage());
|
if (errorActionsShow) {
|
||||||
|
errorActionsShow(this.parseMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionPress = (emoji) => {
|
onReactionPress = (emoji) => {
|
||||||
const { onReactionPress, item } = this.props;
|
const { onReactionPress, item } = this.props;
|
||||||
onReactionPress(emoji, item._id);
|
if (onReactionPress) {
|
||||||
|
onReactionPress(emoji, item._id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onReactionLongPress = () => {
|
onReactionLongPress = () => {
|
||||||
this.setState({ reactionsModal: true });
|
const { onReactionLongPress, item } = this.props;
|
||||||
vibrate();
|
if (onReactionLongPress) {
|
||||||
|
onReactionLongPress(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onDiscussionPress = () => {
|
onDiscussionPress = () => {
|
||||||
const { onDiscussionPress, item } = this.props;
|
const { onDiscussionPress, item } = this.props;
|
||||||
onDiscussionPress(item);
|
if (onDiscussionPress) {
|
||||||
}
|
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'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}, 1000, true)
|
|
||||||
|
|
||||||
get timeFormat() {
|
|
||||||
const { customTimeFormat, Message_TimeFormat } = this.props;
|
|
||||||
return customTimeFormat || Message_TimeFormat;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
closeReactions = () => {
|
onThreadPress = () => {
|
||||||
this.setState({ reactionsModal: false });
|
const { onThreadPress, item } = this.props;
|
||||||
|
if (onThreadPress) {
|
||||||
|
onThreadPress(item);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
isHeader = () => {
|
get isHeader() {
|
||||||
const {
|
const {
|
||||||
item, previousItem, broadcast, Message_GroupingPeriod
|
item, previousItem, broadcast, Message_GroupingPeriod
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -163,7 +136,7 @@ export default class MessageContainer extends React.Component {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
isThreadReply = () => {
|
get isThreadReply() {
|
||||||
const {
|
const {
|
||||||
item, previousItem
|
item, previousItem
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -173,7 +146,7 @@ export default class MessageContainer extends React.Component {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
isThreadSequential = () => {
|
get isThreadSequential() {
|
||||||
const {
|
const {
|
||||||
item, previousItem
|
item, previousItem
|
||||||
} = this.props;
|
} = this.props;
|
||||||
|
@ -183,6 +156,21 @@ export default class MessageContainer extends React.Component {
|
||||||
return false;
|
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 = () => {
|
parseMessage = () => {
|
||||||
const { item } = this.props;
|
const { item } = this.props;
|
||||||
return JSON.parse(JSON.stringify(item));
|
return JSON.parse(JSON.stringify(item));
|
||||||
|
@ -190,23 +178,26 @@ export default class MessageContainer extends React.Component {
|
||||||
|
|
||||||
toggleReactionPicker = () => {
|
toggleReactionPicker = () => {
|
||||||
const { toggleReactionPicker } = this.props;
|
const { toggleReactionPicker } = this.props;
|
||||||
toggleReactionPicker(this.parseMessage());
|
if (toggleReactionPicker) {
|
||||||
|
toggleReactionPicker(this.parseMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
replyBroadcast = () => {
|
replyBroadcast = () => {
|
||||||
const { replyBroadcast } = this.props;
|
const { replyBroadcast } = this.props;
|
||||||
replyBroadcast(this.parseMessage());
|
if (replyBroadcast) {
|
||||||
|
replyBroadcast(this.parseMessage());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { reactionsModal } = this.state;
|
|
||||||
const {
|
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;
|
} = this.props;
|
||||||
const {
|
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;
|
} = item;
|
||||||
const isEditing = editingMessage._id === item._id;
|
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
id={_id}
|
id={_id}
|
||||||
|
@ -214,26 +205,18 @@ export default class MessageContainer extends React.Component {
|
||||||
author={u}
|
author={u}
|
||||||
ts={ts}
|
ts={ts}
|
||||||
type={t}
|
type={t}
|
||||||
status={status}
|
|
||||||
attachments={attachments}
|
attachments={attachments}
|
||||||
urls={urls}
|
urls={urls}
|
||||||
reactions={reactions}
|
reactions={reactions}
|
||||||
alias={alias}
|
alias={alias}
|
||||||
editing={isEditing}
|
|
||||||
header={this.isHeader()}
|
|
||||||
isThreadReply={this.isThreadReply()}
|
|
||||||
isThreadSequential={this.isThreadSequential()}
|
|
||||||
avatar={avatar}
|
avatar={avatar}
|
||||||
user={user}
|
user={user}
|
||||||
edited={editedBy && !!editedBy.username}
|
timeFormat={timeFormat}
|
||||||
timeFormat={this.timeFormat}
|
|
||||||
customThreadTimeFormat={customThreadTimeFormat}
|
customThreadTimeFormat={customThreadTimeFormat}
|
||||||
style={style}
|
style={style}
|
||||||
archived={archived}
|
archived={archived}
|
||||||
broadcast={broadcast}
|
broadcast={broadcast}
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
customEmojis={customEmojis}
|
|
||||||
reactionsModal={reactionsModal}
|
|
||||||
useRealName={useRealName}
|
useRealName={useRealName}
|
||||||
role={role}
|
role={role}
|
||||||
drid={drid}
|
drid={drid}
|
||||||
|
@ -243,16 +226,27 @@ export default class MessageContainer extends React.Component {
|
||||||
tcount={tcount}
|
tcount={tcount}
|
||||||
tlm={tlm}
|
tlm={tlm}
|
||||||
tmsg={tmsg}
|
tmsg={tmsg}
|
||||||
|
useMarkdown={useMarkdown}
|
||||||
fetchThreadName={fetchThreadName}
|
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}
|
onErrorPress={this.onErrorPress}
|
||||||
|
onPress={this.onPress}
|
||||||
onLongPress={this.onLongPress}
|
onLongPress={this.onLongPress}
|
||||||
onReactionLongPress={this.onReactionLongPress}
|
onReactionLongPress={this.onReactionLongPress}
|
||||||
onReactionPress={this.onReactionPress}
|
onReactionPress={this.onReactionPress}
|
||||||
replyBroadcast={this.replyBroadcast}
|
replyBroadcast={this.replyBroadcast}
|
||||||
toggleReactionPicker={this.toggleReactionPicker}
|
toggleReactionPicker={this.toggleReactionPicker}
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
onDiscussionPress={this.onDiscussionPress}
|
||||||
onThreadPress={this.onThreadPress}
|
onOpenFileModal={onOpenFileModal}
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,8 +18,7 @@ export default StyleSheet.create({
|
||||||
paddingVertical: 4,
|
paddingVertical: 4,
|
||||||
width: '100%',
|
width: '100%',
|
||||||
paddingHorizontal: 14,
|
paddingHorizontal: 14,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column'
|
||||||
flex: 1
|
|
||||||
},
|
},
|
||||||
messageContent: {
|
messageContent: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
|
@ -32,8 +31,8 @@ export default StyleSheet.create({
|
||||||
marginLeft: 0
|
marginLeft: 0
|
||||||
},
|
},
|
||||||
flex: {
|
flex: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row'
|
||||||
flex: 1
|
// flex: 1
|
||||||
},
|
},
|
||||||
text: {
|
text: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
|
@ -46,9 +45,6 @@ export default StyleSheet.create({
|
||||||
...sharedStyles.textColorDescription,
|
...sharedStyles.textColorDescription,
|
||||||
...sharedStyles.textRegular
|
...sharedStyles.textRegular
|
||||||
},
|
},
|
||||||
editing: {
|
|
||||||
backgroundColor: '#fff5df'
|
|
||||||
},
|
|
||||||
customEmoji: {
|
customEmoji: {
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20
|
height: 20
|
||||||
|
@ -161,7 +157,7 @@ export default StyleSheet.create({
|
||||||
justifyContent: 'flex-start'
|
justifyContent: 'flex-start'
|
||||||
},
|
},
|
||||||
imageContainer: {
|
imageContainer: {
|
||||||
flex: 1,
|
// flex: 1,
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
borderRadius: 4
|
borderRadius: 4
|
||||||
},
|
},
|
||||||
|
@ -173,6 +169,9 @@ export default StyleSheet.create({
|
||||||
borderColor: COLOR_BORDER,
|
borderColor: COLOR_BORDER,
|
||||||
borderWidth: 1
|
borderWidth: 1
|
||||||
},
|
},
|
||||||
|
imagePressed: {
|
||||||
|
opacity: 0.5
|
||||||
|
},
|
||||||
inlineImage: {
|
inlineImage: {
|
||||||
width: 300,
|
width: 300,
|
||||||
height: 300,
|
height: 300,
|
||||||
|
@ -220,7 +219,7 @@ export default StyleSheet.create({
|
||||||
},
|
},
|
||||||
repliedThread: {
|
repliedThread: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
flex: 1,
|
// flex: 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
marginTop: 6,
|
marginTop: 6,
|
||||||
marginBottom: 12
|
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?',
|
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?',
|
Do_you_really_want_to_key_this_room_question_mark: 'Do you really want to {{key}} this room?',
|
||||||
edit: 'edit',
|
edit: 'edit',
|
||||||
erasing_room: 'erasing room',
|
edited: 'edited',
|
||||||
Edit: 'Edit',
|
Edit: 'Edit',
|
||||||
Email_or_password_field_is_empty: 'Email or password field is empty',
|
Email_or_password_field_is_empty: 'Email or password field is empty',
|
||||||
Email: 'Email',
|
Email: 'Email',
|
||||||
email: 'e-mail',
|
email: 'e-mail',
|
||||||
Enable_notifications: 'Enable notifications',
|
Enable_notifications: 'Enable notifications',
|
||||||
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
Everyone_can_access_this_channel: 'Everyone can access this channel',
|
||||||
|
erasing_room: 'erasing room',
|
||||||
Error_uploading: 'Error uploading',
|
Error_uploading: 'Error uploading',
|
||||||
Favorites: 'Favorites',
|
Favorites: 'Favorites',
|
||||||
Files: 'Files',
|
Files: 'Files',
|
||||||
|
|
|
@ -154,6 +154,7 @@ export default {
|
||||||
Dont_Have_An_Account: 'Não tem uma conta?',
|
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?',
|
Do_you_really_want_to_key_this_room_question_mark: 'Você quer realmente {{key}} esta sala?',
|
||||||
edit: 'editar',
|
edit: 'editar',
|
||||||
|
edited: 'editado',
|
||||||
erasing_room: 'apagando sala',
|
erasing_room: 'apagando sala',
|
||||||
Edit: 'Editar',
|
Edit: 'Editar',
|
||||||
Email_or_password_field_is_empty: 'Email ou senha estão vazios',
|
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 reduxStore from '../createStore';
|
||||||
import database from '../realm';
|
import database from '../realm';
|
||||||
import * as actions from '../../actions';
|
|
||||||
import log from '../../utils/log';
|
import log from '../../utils/log';
|
||||||
|
|
||||||
const getUpdatedSince = () => {
|
const getUpdatedSince = () => {
|
||||||
|
@ -17,7 +16,7 @@ const create = (customEmojis) => {
|
||||||
try {
|
try {
|
||||||
database.create('customEmojis', emoji, true);
|
database.create('customEmojis', emoji, true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log('getEmojis create', e);
|
// log('getEmojis create', e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -40,7 +39,6 @@ export default async function() {
|
||||||
database.write(() => {
|
database.write(() => {
|
||||||
create(emojis);
|
create(emojis);
|
||||||
});
|
});
|
||||||
reduxStore.dispatch(actions.setCustomEmojis(this.parseEmojis(result.emojis)));
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const params = {};
|
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) {
|
if (!subscription.roles || !subscription.roles.length) {
|
||||||
subscription.roles = [];
|
subscription.roles = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (room.muted && room.muted.length) {
|
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 {
|
} else {
|
||||||
subscription.muted = [];
|
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 })) }));
|
// msg.reactions = Object.keys(msg.reactions).map(key => ({ emoji: key, usernames: msg.reactions[key].usernames.map(username => ({ value: username })) }));
|
||||||
// }
|
// }
|
||||||
if (!Array.isArray(msg.reactions)) {
|
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.urls = msg.urls ? parseUrls(msg.urls) : [];
|
||||||
msg._updatedAt = new Date();
|
msg._updatedAt = new Date();
|
||||||
|
|
|
@ -125,7 +125,7 @@ export default function subscribeRoom({ rid }) {
|
||||||
|
|
||||||
const read = debounce(() => {
|
const read = debounce(() => {
|
||||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||||
if (room._id) {
|
if (room && room._id) {
|
||||||
this.readMessages(rid);
|
this.readMessages(rid);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
|
|
|
@ -43,18 +43,11 @@ const roomsSchema = {
|
||||||
primaryKey: '_id',
|
primaryKey: '_id',
|
||||||
properties: {
|
properties: {
|
||||||
_id: 'string',
|
_id: 'string',
|
||||||
|
name: 'string?',
|
||||||
broadcast: { type: 'bool', optional: true }
|
broadcast: { type: 'bool', optional: true }
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const userMutedInRoomSchema = {
|
|
||||||
name: 'usersMuted',
|
|
||||||
primaryKey: 'value',
|
|
||||||
properties: {
|
|
||||||
value: 'string'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const subscriptionSchema = {
|
const subscriptionSchema = {
|
||||||
name: 'subscriptions',
|
name: 'subscriptions',
|
||||||
primaryKey: '_id',
|
primaryKey: '_id',
|
||||||
|
@ -85,7 +78,7 @@ const subscriptionSchema = {
|
||||||
archived: { type: 'bool', optional: true },
|
archived: { type: 'bool', optional: true },
|
||||||
joinCodeRequired: { type: 'bool', optional: true },
|
joinCodeRequired: { type: 'bool', optional: true },
|
||||||
notifications: { type: 'bool', optional: true },
|
notifications: { type: 'bool', optional: true },
|
||||||
muted: { type: 'list', objectType: 'usersMuted' },
|
muted: 'string[]',
|
||||||
broadcast: { type: 'bool', optional: true },
|
broadcast: { type: 'bool', optional: true },
|
||||||
prid: { type: 'string', optional: true },
|
prid: { type: 'string', optional: true },
|
||||||
draftMessage: { type: 'string', optional: true },
|
draftMessage: { type: 'string', optional: true },
|
||||||
|
@ -99,8 +92,7 @@ const usersSchema = {
|
||||||
properties: {
|
properties: {
|
||||||
_id: 'string',
|
_id: 'string',
|
||||||
username: 'string',
|
username: 'string',
|
||||||
name: { type: 'string', optional: true },
|
name: { type: 'string', optional: true }
|
||||||
avatarVersion: { type: 'int', optional: true }
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -155,21 +147,13 @@ const url = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const messagesReactionsUsernamesSchema = {
|
|
||||||
name: 'messagesReactionsUsernames',
|
|
||||||
primaryKey: 'value',
|
|
||||||
properties: {
|
|
||||||
value: 'string'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const messagesReactionsSchema = {
|
const messagesReactionsSchema = {
|
||||||
name: 'messagesReactions',
|
name: 'messagesReactions',
|
||||||
primaryKey: '_id',
|
primaryKey: '_id',
|
||||||
properties: {
|
properties: {
|
||||||
_id: 'string',
|
_id: 'string',
|
||||||
emoji: 'string',
|
emoji: 'string',
|
||||||
usernames: { type: 'list', objectType: 'messagesReactionsUsernames' }
|
usernames: 'string[]'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -211,7 +195,9 @@ const messagesSchema = {
|
||||||
tmid: { type: 'string', optional: true },
|
tmid: { type: 'string', optional: true },
|
||||||
tcount: { type: 'int', optional: true },
|
tcount: { type: 'int', optional: true },
|
||||||
tlm: { type: 'date', 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,
|
frequentlyUsedEmojiSchema,
|
||||||
customEmojisSchema,
|
customEmojisSchema,
|
||||||
messagesReactionsSchema,
|
messagesReactionsSchema,
|
||||||
messagesReactionsUsernamesSchema,
|
|
||||||
rolesSchema,
|
rolesSchema,
|
||||||
userMutedInRoomSchema,
|
|
||||||
uploadsSchema
|
uploadsSchema
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -374,9 +358,9 @@ class DB {
|
||||||
schema: [
|
schema: [
|
||||||
serversSchema
|
serversSchema
|
||||||
],
|
],
|
||||||
schemaVersion: 6,
|
schemaVersion: 8,
|
||||||
migration: (oldRealm, newRealm) => {
|
migration: (oldRealm, newRealm) => {
|
||||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 6) {
|
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 8) {
|
||||||
const newServers = newRealm.objects('servers');
|
const newServers = newRealm.objects('servers');
|
||||||
|
|
||||||
// eslint-disable-next-line no-plusplus
|
// eslint-disable-next-line no-plusplus
|
||||||
|
@ -431,16 +415,11 @@ class DB {
|
||||||
return this.databases.activeDB = new Realm({
|
return this.databases.activeDB = new Realm({
|
||||||
path: `${ path }.realm`,
|
path: `${ path }.realm`,
|
||||||
schema,
|
schema,
|
||||||
schemaVersion: 9,
|
schemaVersion: 11,
|
||||||
migration: (oldRealm, newRealm) => {
|
migration: (oldRealm, newRealm) => {
|
||||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
|
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 11) {
|
||||||
const newSubs = newRealm.objects('subscriptions');
|
const newSubs = newRealm.objects('subscriptions');
|
||||||
|
newRealm.delete(newSubs);
|
||||||
// eslint-disable-next-line no-plusplus
|
|
||||||
for (let i = 0; i < newSubs.length; i++) {
|
|
||||||
newSubs[i].lastOpen = null;
|
|
||||||
newSubs[i].ls = null;
|
|
||||||
}
|
|
||||||
const newMessages = newRealm.objects('messages');
|
const newMessages = newRealm.objects('messages');
|
||||||
newRealm.delete(newMessages);
|
newRealm.delete(newMessages);
|
||||||
const newThreads = newRealm.objects('threads');
|
const newThreads = newRealm.objects('threads');
|
||||||
|
@ -449,8 +428,6 @@ class DB {
|
||||||
newRealm.delete(newThreadMessages);
|
newRealm.delete(newThreadMessages);
|
||||||
}
|
}
|
||||||
if (newRealm.schemaVersion === 9) {
|
if (newRealm.schemaVersion === 9) {
|
||||||
const newSubs = newRealm.objects('subscriptions');
|
|
||||||
newRealm.delete(newSubs);
|
|
||||||
const newEmojis = newRealm.objects('customEmojis');
|
const newEmojis = newRealm.objects('customEmojis');
|
||||||
newRealm.delete(newEmojis);
|
newRealm.delete(newEmojis);
|
||||||
const newSettings = newRealm.objects('settings');
|
const newSettings = newRealm.objects('settings');
|
||||||
|
|
|
@ -472,19 +472,6 @@ const RocketChat = {
|
||||||
return setting;
|
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) {
|
deleteMessage(message) {
|
||||||
const { _id, rid } = message;
|
const { _id, rid } = message;
|
||||||
// RC 0.48.0
|
// 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 selectedUsers from './selectedUsers';
|
||||||
import createChannel from './createChannel';
|
import createChannel from './createChannel';
|
||||||
import app from './app';
|
import app from './app';
|
||||||
import customEmojis from './customEmojis';
|
|
||||||
import sortPreferences from './sortPreferences';
|
import sortPreferences from './sortPreferences';
|
||||||
|
|
||||||
export default combineReducers({
|
export default combineReducers({
|
||||||
|
@ -21,6 +20,5 @@ export default combineReducers({
|
||||||
createChannel,
|
createChannel,
|
||||||
app,
|
app,
|
||||||
rooms,
|
rooms,
|
||||||
customEmojis,
|
|
||||||
sortPreferences
|
sortPreferences
|
||||||
});
|
});
|
||||||
|
|
|
@ -19,7 +19,13 @@ const handleRoomsRequest = function* handleRoomsRequest() {
|
||||||
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
|
const { subscriptions } = mergeSubscriptionsRooms(subscriptionsResult, roomsResult);
|
||||||
|
|
||||||
database.write(() => {
|
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(() => {
|
database.databases.serversDB.write(() => {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -51,8 +51,6 @@ const handleSelectServer = function* handleSelectServer({ server, version, fetch
|
||||||
|
|
||||||
const settings = database.objects('settings');
|
const settings = database.objects('settings');
|
||||||
yield put(actions.setAllSettings(RocketChat.parseSettings(settings.slice(0, settings.length))));
|
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));
|
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -14,13 +14,13 @@ import I18n from '../../i18n';
|
||||||
import RocketChat from '../../lib/rocketchat';
|
import RocketChat from '../../lib/rocketchat';
|
||||||
import StatusBar from '../../containers/StatusBar';
|
import StatusBar from '../../containers/StatusBar';
|
||||||
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
|
import getFileUrlFromMessage from '../../lib/methods/helpers/getFileUrlFromMessage';
|
||||||
|
import FileModal from '../../containers/FileModal';
|
||||||
|
|
||||||
const ACTION_INDEX = 0;
|
const ACTION_INDEX = 0;
|
||||||
const CANCEL_INDEX = 1;
|
const CANCEL_INDEX = 1;
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||||
customEmojis: state.customEmojis,
|
|
||||||
user: {
|
user: {
|
||||||
id: state.login.user && state.login.user.id,
|
id: state.login.user && state.login.user.id,
|
||||||
username: state.login.user && state.login.user.username,
|
username: state.login.user && state.login.user.username,
|
||||||
|
@ -36,7 +36,6 @@ export default class MessagesView extends LoggedView {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string,
|
||||||
customEmojis: PropTypes.object,
|
|
||||||
navigation: PropTypes.object
|
navigation: PropTypes.object
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,7 +43,9 @@ export default class MessagesView extends LoggedView {
|
||||||
super('MessagesView', props);
|
super('MessagesView', props);
|
||||||
this.state = {
|
this.state = {
|
||||||
loading: false,
|
loading: false,
|
||||||
messages: []
|
messages: [],
|
||||||
|
selectedAttachment: {},
|
||||||
|
photoModalVisible: false
|
||||||
};
|
};
|
||||||
this.rid = props.navigation.getParam('rid');
|
this.rid = props.navigation.getParam('rid');
|
||||||
this.t = props.navigation.getParam('t');
|
this.t = props.navigation.getParam('t');
|
||||||
|
@ -56,10 +57,13 @@ export default class MessagesView extends LoggedView {
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const { loading, messages } = this.state;
|
const { loading, messages, photoModalVisible } = this.state;
|
||||||
if (nextState.loading !== loading) {
|
if (nextState.loading !== loading) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (nextState.photoModalVisible !== photoModalVisible) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!equal(nextState.messages, messages)) {
|
if (!equal(nextState.messages, messages)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -68,18 +72,18 @@ export default class MessagesView extends LoggedView {
|
||||||
|
|
||||||
defineMessagesViewContent = (name) => {
|
defineMessagesViewContent = (name) => {
|
||||||
const { messages } = this.state;
|
const { messages } = this.state;
|
||||||
const { user, baseUrl, customEmojis } = this.props;
|
const { user, baseUrl } = this.props;
|
||||||
|
|
||||||
const renderItemCommonProps = item => ({
|
const renderItemCommonProps = item => ({
|
||||||
customEmojis,
|
|
||||||
baseUrl,
|
baseUrl,
|
||||||
user,
|
user,
|
||||||
author: item.u || item.user,
|
author: item.u || item.user,
|
||||||
ts: item.ts || item.uploadedAt,
|
ts: item.ts || item.uploadedAt,
|
||||||
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
timeFormat: 'MMM Do YYYY, h:mm:ss a',
|
||||||
edited: !!item.editedAt,
|
isEdited: !!item.editedAt,
|
||||||
header: true,
|
isHeader: true,
|
||||||
attachments: item.attachments || []
|
attachments: item.attachments || [],
|
||||||
|
onOpenFileModal: this.onOpenFileModal
|
||||||
});
|
});
|
||||||
|
|
||||||
return ({
|
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) => {
|
onLongPress = (message) => {
|
||||||
this.setState({ message });
|
this.setState({ message });
|
||||||
this.showActionSheet();
|
this.showActionSheet();
|
||||||
|
@ -232,7 +244,10 @@ export default class MessagesView extends LoggedView {
|
||||||
renderItem = ({ item }) => this.content.renderItem(item)
|
renderItem = ({ item }) => this.content.renderItem(item)
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { messages, loading } = this.state;
|
const {
|
||||||
|
messages, loading, selectedAttachment, photoModalVisible
|
||||||
|
} = this.state;
|
||||||
|
const { user, baseUrl } = this.props;
|
||||||
|
|
||||||
if (!loading && messages.length === 0) {
|
if (!loading && messages.length === 0) {
|
||||||
return this.renderEmpty();
|
return this.renderEmpty();
|
||||||
|
@ -249,6 +264,13 @@ export default class MessagesView extends LoggedView {
|
||||||
onEndReached={this.load}
|
onEndReached={this.load}
|
||||||
ListFooterComponent={loading ? <RCActivityIndicator /> : null}
|
ListFooterComponent={loading ? <RCActivityIndicator /> : null}
|
||||||
/>
|
/>
|
||||||
|
<FileModal
|
||||||
|
attachment={selectedAttachment}
|
||||||
|
isVisible={photoModalVisible}
|
||||||
|
onClose={this.onCloseFileModal}
|
||||||
|
user={user}
|
||||||
|
baseUrl={baseUrl}
|
||||||
|
/>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,9 @@ import { isIOS } from '../utils/deviceInfo';
|
||||||
import { CloseModalButton } from '../containers/HeaderButton';
|
import { CloseModalButton } from '../containers/HeaderButton';
|
||||||
import StatusBar from '../containers/StatusBar';
|
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
|
||||||
const userAgent = isIOS ? 'UserAgent' : 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'
|
||||||
|
: '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 => ({
|
@connect(state => ({
|
||||||
server: state.server.server
|
server: state.server.server
|
||||||
|
@ -62,6 +63,7 @@ export default class OAuthView extends React.PureComponent {
|
||||||
<React.Fragment>
|
<React.Fragment>
|
||||||
<StatusBar />
|
<StatusBar />
|
||||||
<WebView
|
<WebView
|
||||||
|
useWebKit
|
||||||
source={{ uri: oAuthUrl }}
|
source={{ uri: oAuthUrl }}
|
||||||
userAgent={userAgent}
|
userAgent={userAgent}
|
||||||
onNavigationStateChange={(webViewState) => {
|
onNavigationStateChange={(webViewState) => {
|
||||||
|
|
|
@ -158,7 +158,7 @@ export default class RoomMembersView extends LoggedView {
|
||||||
const { muted } = room;
|
const { muted } = room;
|
||||||
|
|
||||||
this.actionSheetOptions = [I18n.t('Cancel')];
|
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;
|
user.muted = userIsMuted;
|
||||||
if (userIsMuted) {
|
if (userIsMuted) {
|
||||||
this.actionSheetOptions.push(I18n.t('Unmute'));
|
this.actionSheetOptions.push(I18n.t('Unmute'));
|
||||||
|
|
|
@ -46,7 +46,9 @@ class RightButtonsContainer extends React.PureComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
safeAddListener(this.thread, this.updateThread);
|
if (this.thread) {
|
||||||
|
safeAddListener(this.thread, this.updateThread);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
|
|
@ -147,11 +147,11 @@ export class List extends React.PureComponent {
|
||||||
style={styles.list}
|
style={styles.list}
|
||||||
inverted
|
inverted
|
||||||
removeClippedSubviews
|
removeClippedSubviews
|
||||||
initialNumToRender={5}
|
initialNumToRender={7}
|
||||||
onEndReached={this.onEndReached}
|
onEndReached={this.onEndReached}
|
||||||
onEndReachedThreshold={0.5}
|
onEndReachedThreshold={5}
|
||||||
maxToRenderPerBatch={5}
|
maxToRenderPerBatch={5}
|
||||||
windowSize={21}
|
windowSize={10}
|
||||||
ListFooterComponent={this.renderFooter}
|
ListFooterComponent={this.renderFooter}
|
||||||
{...scrollPersistTaps}
|
{...scrollPersistTaps}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -13,8 +13,10 @@ import EJSON from 'ejson';
|
||||||
import {
|
import {
|
||||||
toggleReactionPicker as toggleReactionPickerAction,
|
toggleReactionPicker as toggleReactionPickerAction,
|
||||||
actionsShow as actionsShowAction,
|
actionsShow as actionsShowAction,
|
||||||
|
errorActionsShow as errorActionsShowAction,
|
||||||
editCancel as editCancelAction,
|
editCancel as editCancelAction,
|
||||||
replyCancel as replyCancelAction
|
replyCancel as replyCancelAction,
|
||||||
|
replyBroadcast as replyBroadcastAction
|
||||||
} from '../../actions/messages';
|
} from '../../actions/messages';
|
||||||
import LoggedView from '../View';
|
import LoggedView from '../View';
|
||||||
import { List } from './List';
|
import { List } from './List';
|
||||||
|
@ -37,6 +39,9 @@ import Separator from './Separator';
|
||||||
import { COLOR_WHITE } from '../../constants/colors';
|
import { COLOR_WHITE } from '../../constants/colors';
|
||||||
import debounce from '../../utils/debounce';
|
import debounce from '../../utils/debounce';
|
||||||
import buildMessage from '../../lib/methods/helpers/buildMessage';
|
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';
|
import { Toast } from '../../utils/info';
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
|
@ -52,12 +57,17 @@ import { Toast } from '../../utils/info';
|
||||||
showErrorActions: state.messages.showErrorActions,
|
showErrorActions: state.messages.showErrorActions,
|
||||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
||||||
useRealName: state.settings.UI_Use_Real_Name,
|
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 => ({
|
}), dispatch => ({
|
||||||
editCancel: () => dispatch(editCancelAction()),
|
editCancel: () => dispatch(editCancelAction()),
|
||||||
replyCancel: () => dispatch(replyCancelAction()),
|
replyCancel: () => dispatch(replyCancelAction()),
|
||||||
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
|
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 */
|
/** @extends React.Component */
|
||||||
export default class RoomView extends LoggedView {
|
export default class RoomView extends LoggedView {
|
||||||
|
@ -105,12 +115,17 @@ export default class RoomView extends LoggedView {
|
||||||
appState: PropTypes.string,
|
appState: PropTypes.string,
|
||||||
useRealName: PropTypes.bool,
|
useRealName: PropTypes.bool,
|
||||||
isAuthenticated: PropTypes.bool,
|
isAuthenticated: PropTypes.bool,
|
||||||
|
Message_GroupingPeriod: PropTypes.number,
|
||||||
|
Message_TimeFormat: PropTypes.string,
|
||||||
editing: PropTypes.bool,
|
editing: PropTypes.bool,
|
||||||
replying: PropTypes.bool,
|
replying: PropTypes.bool,
|
||||||
toggleReactionPicker: PropTypes.func.isRequired,
|
baseUrl: PropTypes.string,
|
||||||
|
toggleReactionPicker: PropTypes.func,
|
||||||
actionsShow: PropTypes.func,
|
actionsShow: PropTypes.func,
|
||||||
editCancel: PropTypes.func,
|
editCancel: PropTypes.func,
|
||||||
replyCancel: PropTypes.func
|
replyCancel: PropTypes.func,
|
||||||
|
replyBroadcast: PropTypes.func,
|
||||||
|
errorActionsShow: PropTypes.func
|
||||||
};
|
};
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -120,16 +135,20 @@ export default class RoomView extends LoggedView {
|
||||||
this.rid = props.navigation.getParam('rid');
|
this.rid = props.navigation.getParam('rid');
|
||||||
this.t = props.navigation.getParam('t');
|
this.t = props.navigation.getParam('t');
|
||||||
this.tmid = props.navigation.getParam('tmid');
|
this.tmid = props.navigation.getParam('tmid');
|
||||||
|
this.useMarkdown = props.navigation.getParam('useMarkdown', true);
|
||||||
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
|
||||||
this.state = {
|
this.state = {
|
||||||
joined: this.rooms.length > 0,
|
joined: this.rooms.length > 0,
|
||||||
room: this.rooms[0] || { rid: this.rid, t: this.t },
|
room: this.rooms[0] || { rid: this.rid, t: this.t },
|
||||||
lastOpen: null
|
lastOpen: null,
|
||||||
|
photoModalVisible: false,
|
||||||
|
reactionsModalVisible: false,
|
||||||
|
selectedAttachment: {},
|
||||||
|
selectedMessage: {}
|
||||||
};
|
};
|
||||||
this.beginAnimating = false;
|
this.beginAnimating = false;
|
||||||
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
|
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
|
||||||
this.messagebox = React.createRef();
|
this.messagebox = React.createRef();
|
||||||
safeAddListener(this.rooms, this.updateRoom);
|
|
||||||
this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false);
|
this.willBlurListener = props.navigation.addListener('willBlur', () => this.mounted = false);
|
||||||
this.mounted = false;
|
this.mounted = false;
|
||||||
console.timeEnd(`${ this.constructor.name } init`);
|
console.timeEnd(`${ this.constructor.name } init`);
|
||||||
|
@ -152,6 +171,7 @@ export default class RoomView extends LoggedView {
|
||||||
} else {
|
} else {
|
||||||
EventEmitter.addEventListener('connected', this.handleConnected);
|
EventEmitter.addEventListener('connected', this.handleConnected);
|
||||||
}
|
}
|
||||||
|
safeAddListener(this.rooms, this.updateRoom);
|
||||||
this.mounted = true;
|
this.mounted = true;
|
||||||
});
|
});
|
||||||
console.timeEnd(`${ this.constructor.name } mount`);
|
console.timeEnd(`${ this.constructor.name } mount`);
|
||||||
|
@ -159,12 +179,16 @@ export default class RoomView extends LoggedView {
|
||||||
|
|
||||||
shouldComponentUpdate(nextProps, nextState) {
|
shouldComponentUpdate(nextProps, nextState) {
|
||||||
const {
|
const {
|
||||||
room, joined, lastOpen
|
room, joined, lastOpen, photoModalVisible, reactionsModalVisible
|
||||||
} = this.state;
|
} = this.state;
|
||||||
const { showActions, showErrorActions, appState } = this.props;
|
const { showActions, showErrorActions, appState } = this.props;
|
||||||
|
|
||||||
if (lastOpen !== nextState.lastOpen) {
|
if (lastOpen !== nextState.lastOpen) {
|
||||||
return true;
|
return true;
|
||||||
|
} else if (photoModalVisible !== nextState.photoModalVisible) {
|
||||||
|
return true;
|
||||||
|
} else if (reactionsModalVisible !== nextState.reactionsModalVisible) {
|
||||||
|
return true;
|
||||||
} else if (room.ro !== nextState.room.ro) {
|
} else if (room.ro !== nextState.room.ro) {
|
||||||
return true;
|
return true;
|
||||||
} else if (room.f !== nextState.room.f) {
|
} else if (room.f !== nextState.room.f) {
|
||||||
|
@ -285,6 +309,14 @@ export default class RoomView extends LoggedView {
|
||||||
actionsShow({ ...message, rid: this.rid });
|
actionsShow({ ...message, rid: this.rid });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onOpenFileModal = (attachment) => {
|
||||||
|
this.setState({ selectedAttachment: attachment, photoModalVisible: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
onCloseFileModal = () => {
|
||||||
|
this.setState({ selectedAttachment: {}, photoModalVisible: false });
|
||||||
|
}
|
||||||
|
|
||||||
onReactionPress = (shortname, messageId) => {
|
onReactionPress = (shortname, messageId) => {
|
||||||
const { actionMessage, toggleReactionPicker } = this.props;
|
const { actionMessage, toggleReactionPicker } = this.props;
|
||||||
try {
|
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) => {
|
onDiscussionPress = debounce((item) => {
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.push('RoomView', {
|
navigation.push('RoomView', {
|
||||||
|
@ -305,6 +346,35 @@ export default class RoomView extends LoggedView {
|
||||||
});
|
});
|
||||||
}, 1000, true)
|
}, 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 = () => {
|
handleConnected = () => {
|
||||||
this.init();
|
this.init();
|
||||||
EventEmitter.removeListener('connected', this.handleConnected);
|
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() => {
|
joinRoom = async() => {
|
||||||
try {
|
try {
|
||||||
|
@ -388,7 +458,7 @@ export default class RoomView extends LoggedView {
|
||||||
isMuted = () => {
|
isMuted = () => {
|
||||||
const { room } = this.state;
|
const { room } = this.state;
|
||||||
const { user } = this.props;
|
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 = () => {
|
isReadOnly = () => {
|
||||||
|
@ -433,7 +503,9 @@ export default class RoomView extends LoggedView {
|
||||||
|
|
||||||
renderItem = (item, previousItem) => {
|
renderItem = (item, previousItem) => {
|
||||||
const { room, lastOpen } = this.state;
|
const { room, lastOpen } = this.state;
|
||||||
const { user, navigation } = this.props;
|
const {
|
||||||
|
user, Message_GroupingPeriod, Message_TimeFormat, useRealName, baseUrl
|
||||||
|
} = this.props;
|
||||||
let dateSeparator = null;
|
let dateSeparator = null;
|
||||||
let showUnreadSeparator = false;
|
let showUnreadSeparator = false;
|
||||||
|
|
||||||
|
@ -459,11 +531,21 @@ export default class RoomView extends LoggedView {
|
||||||
status={item.status}
|
status={item.status}
|
||||||
_updatedAt={item._updatedAt}
|
_updatedAt={item._updatedAt}
|
||||||
previousItem={previousItem}
|
previousItem={previousItem}
|
||||||
navigation={navigation}
|
|
||||||
fetchThreadName={this.fetchThreadName}
|
fetchThreadName={this.fetchThreadName}
|
||||||
onReactionPress={this.onReactionPress}
|
onReactionPress={this.onReactionPress}
|
||||||
|
onReactionLongPress={this.onReactionLongPress}
|
||||||
onLongPress={this.onMessageLongPress}
|
onLongPress={this.onMessageLongPress}
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
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() {
|
render() {
|
||||||
console.count(`${ this.constructor.name }.render calls`);
|
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;
|
const { rid, t } = room;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -559,6 +644,20 @@ export default class RoomView extends LoggedView {
|
||||||
{this.renderActions()}
|
{this.renderActions()}
|
||||||
<ReactionPicker onEmojiSelected={this.onReactionPress} />
|
<ReactionPicker onEmojiSelected={this.onReactionPress} />
|
||||||
<UploadProgress rid={this.rid} />
|
<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} />
|
<Toast ref={toast => this.toast = toast} />
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
);
|
);
|
||||||
|
|
|
@ -66,6 +66,7 @@ export default class RoomsListView extends LoggedView {
|
||||||
const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid');
|
const cancelSearchingAndroid = navigation.getParam('cancelSearchingAndroid');
|
||||||
const onPressItem = navigation.getParam('onPressItem', () => {});
|
const onPressItem = navigation.getParam('onPressItem', () => {});
|
||||||
const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {});
|
const initSearchingAndroid = navigation.getParam('initSearchingAndroid', () => {});
|
||||||
|
const toggleUseMarkdown = navigation.getParam('toggleUseMarkdown', () => {});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
headerLeft: (
|
headerLeft: (
|
||||||
|
@ -75,7 +76,7 @@ export default class RoomsListView extends LoggedView {
|
||||||
<Item title='cancel' iconName='cross' onPress={cancelSearchingAndroid} />
|
<Item title='cancel' iconName='cross' onPress={cancelSearchingAndroid} />
|
||||||
</CustomHeaderButtons>
|
</CustomHeaderButtons>
|
||||||
)
|
)
|
||||||
: <DrawerButton navigation={navigation} testID='rooms-list-view-sidebar' />
|
: <DrawerButton navigation={navigation} testID='rooms-list-view-sidebar' onLongPress={toggleUseMarkdown} />
|
||||||
),
|
),
|
||||||
headerTitle: <RoomsListHeaderView />,
|
headerTitle: <RoomsListHeaderView />,
|
||||||
headerRight: (
|
headerRight: (
|
||||||
|
@ -124,6 +125,7 @@ export default class RoomsListView extends LoggedView {
|
||||||
searching: false,
|
searching: false,
|
||||||
search: [],
|
search: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
|
useMarkdown: true,
|
||||||
chats: [],
|
chats: [],
|
||||||
unread: [],
|
unread: [],
|
||||||
favorites: [],
|
favorites: [],
|
||||||
|
@ -142,7 +144,10 @@ export default class RoomsListView extends LoggedView {
|
||||||
this.getSubscriptions();
|
this.getSubscriptions();
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.setParams({
|
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`);
|
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)
|
// this is necessary during development (enables Cmd + r)
|
||||||
hasActiveDB = () => database && database.databases && database.databases.activeDB;
|
hasActiveDB = () => database && database.databases && database.databases.activeDB;
|
||||||
|
|
||||||
|
@ -341,9 +355,10 @@ export default class RoomsListView extends LoggedView {
|
||||||
|
|
||||||
goRoom = (item) => {
|
goRoom = (item) => {
|
||||||
this.cancelSearchingAndroid();
|
this.cancelSearchingAndroid();
|
||||||
|
const { useMarkdown } = this.state;
|
||||||
const { navigation } = this.props;
|
const { navigation } = this.props;
|
||||||
navigation.navigate('RoomView', {
|
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 => ({
|
@connect(state => ({
|
||||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||||
customEmojis: state.customEmojis,
|
|
||||||
user: {
|
user: {
|
||||||
id: state.login.user && state.login.user.id,
|
id: state.login.user && state.login.user.id,
|
||||||
username: state.login.user && state.login.user.username,
|
username: state.login.user && state.login.user.username,
|
||||||
|
@ -35,8 +34,7 @@ export default class SearchMessagesView extends LoggedView {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
navigation: PropTypes.object,
|
navigation: PropTypes.object,
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
baseUrl: PropTypes.string,
|
baseUrl: PropTypes.string
|
||||||
customEmojis: PropTypes.object
|
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -96,10 +94,9 @@ export default class SearchMessagesView extends LoggedView {
|
||||||
)
|
)
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const { user, customEmojis, baseUrl } = this.props;
|
const { user, baseUrl } = this.props;
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
customEmojis={customEmojis}
|
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
user={user}
|
user={user}
|
||||||
author={item.u}
|
author={item.u}
|
||||||
|
@ -107,8 +104,9 @@ export default class SearchMessagesView extends LoggedView {
|
||||||
msg={item.msg}
|
msg={item.msg}
|
||||||
attachments={item.attachments || []}
|
attachments={item.attachments || []}
|
||||||
timeFormat='MMM Do YYYY, h:mm:ss a'
|
timeFormat='MMM Do YYYY, h:mm:ss a'
|
||||||
edited={!!item.editedAt}
|
isEdited={!!item.editedAt}
|
||||||
header
|
isHeader
|
||||||
|
onOpenFileModal={() => {}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -145,7 +143,7 @@ export default class SearchMessagesView extends LoggedView {
|
||||||
placeholder={I18n.t('Search_Messages')}
|
placeholder={I18n.t('Search_Messages')}
|
||||||
testID='search-message-view-input'
|
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 style={styles.divider} />
|
||||||
</View>
|
</View>
|
||||||
{this.renderList()}
|
{this.renderList()}
|
||||||
|
|
|
@ -24,12 +24,12 @@ const API_FETCH_COUNT = 50;
|
||||||
|
|
||||||
@connect(state => ({
|
@connect(state => ({
|
||||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||||
customEmojis: state.customEmojis,
|
|
||||||
user: {
|
user: {
|
||||||
id: state.login.user && state.login.user.id,
|
id: state.login.user && state.login.user.id,
|
||||||
username: state.login.user && state.login.user.username,
|
username: state.login.user && state.login.user.username,
|
||||||
token: state.login.user && state.login.user.token
|
token: state.login.user && state.login.user.token
|
||||||
}
|
},
|
||||||
|
useRealName: state.settings.UI_Use_Real_Name
|
||||||
}))
|
}))
|
||||||
/** @extends React.Component */
|
/** @extends React.Component */
|
||||||
export default class ThreadMessagesView extends LoggedView {
|
export default class ThreadMessagesView extends LoggedView {
|
||||||
|
@ -39,7 +39,9 @@ export default class ThreadMessagesView extends LoggedView {
|
||||||
|
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
user: PropTypes.object,
|
user: PropTypes.object,
|
||||||
navigation: PropTypes.object
|
navigation: PropTypes.object,
|
||||||
|
baseUrl: PropTypes.string,
|
||||||
|
useRealName: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -82,6 +84,7 @@ export default class ThreadMessagesView extends LoggedView {
|
||||||
this.setState({ messages: this.messages });
|
this.setState({ messages: this.messages });
|
||||||
}, 300)
|
}, 300)
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/sort-comp
|
||||||
init = () => {
|
init = () => {
|
||||||
const [room] = this.rooms;
|
const [room] = this.rooms;
|
||||||
const lastThreadSync = new Date();
|
const lastThreadSync = new Date();
|
||||||
|
@ -186,6 +189,20 @@ export default class ThreadMessagesView extends LoggedView {
|
||||||
}) : null
|
}) : 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 />
|
renderSeparator = () => <Separator />
|
||||||
|
|
||||||
renderEmpty = () => (
|
renderEmpty = () => (
|
||||||
|
@ -195,7 +212,9 @@ export default class ThreadMessagesView extends LoggedView {
|
||||||
)
|
)
|
||||||
|
|
||||||
renderItem = ({ item }) => {
|
renderItem = ({ item }) => {
|
||||||
const { user, navigation } = this.props;
|
const {
|
||||||
|
user, navigation, baseUrl, useRealName
|
||||||
|
} = this.props;
|
||||||
if (item.isValid && item.isValid()) {
|
if (item.isValid && item.isValid()) {
|
||||||
return (
|
return (
|
||||||
<Message
|
<Message
|
||||||
|
@ -207,10 +226,11 @@ export default class ThreadMessagesView extends LoggedView {
|
||||||
status={item.status}
|
status={item.status}
|
||||||
_updatedAt={item._updatedAt}
|
_updatedAt={item._updatedAt}
|
||||||
navigation={navigation}
|
navigation={navigation}
|
||||||
customTimeFormat='MMM D'
|
timeFormat='MMM D'
|
||||||
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
|
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
|
||||||
fetchThreadName={this.fetchThreadName}
|
onThreadPress={this.onThreadPress}
|
||||||
onDiscussionPress={this.onDiscussionPress}
|
baseUrl={baseUrl}
|
||||||
|
useRealName={useRealName}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,10 +39,6 @@
|
||||||
[self.window makeKeyAndVisible];
|
[self.window makeKeyAndVisible];
|
||||||
[Fabric with:@[[Crashlytics class]]];
|
[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];
|
[RNSplashScreen show];
|
||||||
|
|
||||||
return YES;
|
return YES;
|
||||||
|
|
|
@ -59,6 +59,7 @@
|
||||||
"react-native-screens": "^1.0.0-alpha.22",
|
"react-native-screens": "^1.0.0-alpha.22",
|
||||||
"react-native-scrollable-tab-view": "0.10.0",
|
"react-native-scrollable-tab-view": "0.10.0",
|
||||||
"react-native-slider": "^0.11.0",
|
"react-native-slider": "^0.11.0",
|
||||||
|
"react-native-slowlog": "^1.0.2",
|
||||||
"react-native-splash-screen": "^3.2.0",
|
"react-native-splash-screen": "^3.2.0",
|
||||||
"react-native-vector-icons": "^6.4.2",
|
"react-native-vector-icons": "^6.4.2",
|
||||||
"react-native-video": "^4.4.1",
|
"react-native-video": "^4.4.1",
|
||||||
|
|
|
@ -24,19 +24,27 @@ const author = {
|
||||||
username: 'diego.mello'
|
username: 'diego.mello'
|
||||||
};
|
};
|
||||||
const baseUrl = 'https://open.rocket.chat';
|
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 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 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 => (
|
const Message = props => (
|
||||||
<MessageComponent
|
<MessageComponent
|
||||||
baseUrl={baseUrl}
|
baseUrl={baseUrl}
|
||||||
customEmojis={customEmojis}
|
|
||||||
user={user}
|
user={user}
|
||||||
author={author}
|
author={author}
|
||||||
ts={date}
|
ts={date}
|
||||||
timeFormat='LT'
|
timeFormat='LT'
|
||||||
header
|
isHeader
|
||||||
|
getCustomEmoji={getCustomEmoji}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -62,12 +70,12 @@ export default (
|
||||||
username: longText
|
username: longText
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Message msg='This is the third message' header={false} />
|
<Message msg='This is the third message' isHeader={false} />
|
||||||
<Message msg='This is the second message' header={false} />
|
<Message msg='This is the second message' isHeader={false} />
|
||||||
<Message msg='This is the first message' />
|
<Message msg='This is the first message' />
|
||||||
|
|
||||||
<Separator title='Without header' />
|
<Separator title='Without header' />
|
||||||
<Message msg='Message' header={false} />
|
<Message msg='Message' isHeader={false} />
|
||||||
|
|
||||||
<Separator title='With alias' />
|
<Separator title='With alias' />
|
||||||
<Message msg='Message' alias='Diego Mello' />
|
<Message msg='Message' alias='Diego Mello' />
|
||||||
|
@ -101,7 +109,21 @@ export default (
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator title='Mentions' />
|
<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' />
|
<Separator title='Emojis' />
|
||||||
<Message msg='👊🤙👏' />
|
<Message msg='👊🤙👏' />
|
||||||
|
@ -194,7 +216,7 @@ export default (
|
||||||
...author,
|
...author,
|
||||||
username: 'rocket.cat'
|
username: 'rocket.cat'
|
||||||
}}
|
}}
|
||||||
header={false}
|
isHeader={false}
|
||||||
/>
|
/>
|
||||||
<Message
|
<Message
|
||||||
msg='Second message'
|
msg='Second message'
|
||||||
|
@ -217,7 +239,7 @@ export default (
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
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'
|
image_url: '/file-upload/sxLXBzjwuqxMnebyP/Clipboard%20-%2029%20de%20Agosto%20de%202018%20%C3%A0s%2018:10'
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
|
@ -226,7 +248,13 @@ export default (
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
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'
|
video_url: '/file-upload/cqnKqb6kdajky5Rxj/WhatsApp%20Video%202018-08-22%20at%2019.09.55.mp4'
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
|
@ -235,32 +263,32 @@ export default (
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
title: 'This is a title',
|
||||||
description: 'This is a description',
|
description: 'This is a description :nyan_rocket:',
|
||||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
<Message msg='First message' header={false} />
|
<Message msg='First message' isHeader={false} />
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
title: 'This is a title',
|
||||||
description: 'This is a description',
|
description: 'This is a description',
|
||||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||||
}]}
|
}]}
|
||||||
header={false}
|
isHeader={false}
|
||||||
/>
|
/>
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
title: 'This is a title',
|
||||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||||
}]}
|
}]}
|
||||||
header={false}
|
isHeader={false}
|
||||||
/>
|
/>
|
||||||
<Message
|
<Message
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
title: 'This is a title',
|
||||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||||
}]}
|
}]}
|
||||||
header={false}
|
isHeader={false}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator title='Message with reply' />
|
<Separator title='Message with reply' />
|
||||||
|
@ -279,7 +307,7 @@ export default (
|
||||||
author_name: 'rocket.cat',
|
author_name: 'rocket.cat',
|
||||||
ts: date,
|
ts: date,
|
||||||
timeFormat: 'LT',
|
timeFormat: 'LT',
|
||||||
text: 'How are you?'
|
text: 'How are you? :nyan_rocket:'
|
||||||
}]}
|
}]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -335,7 +363,7 @@ export default (
|
||||||
tmsg='Thread with attachment'
|
tmsg='Thread with attachment'
|
||||||
attachments={[{
|
attachments={[{
|
||||||
title: 'This is a title',
|
title: 'This is a title',
|
||||||
description: 'This is a description',
|
description: 'This is a description :nyan_rocket:',
|
||||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||||
}]}
|
}]}
|
||||||
isThreadReply
|
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.'
|
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' />
|
<Separator title='Custom fields' />
|
||||||
<Message
|
<Message
|
||||||
|
@ -556,28 +600,29 @@ export default (
|
||||||
<Message msg='This message is inside an archived room' archived />
|
<Message msg='This message is inside an archived room' archived />
|
||||||
|
|
||||||
<Separator title='Error' />
|
<Separator title='Error' />
|
||||||
<Message msg='This message has error too' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} header={false} />
|
<Message hasError msg='This message has error' status={messagesStatus.ERROR} onErrorPress={() => alert('Error pressed')} />
|
||||||
<Message 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' />
|
<Separator title='Temp' />
|
||||||
<Message msg='Temp message' status={messagesStatus.TEMP} />
|
<Message msg='Temp message' status={messagesStatus.TEMP} isTemp />
|
||||||
|
|
||||||
<Separator title='Editing' />
|
<Separator title='Editing' />
|
||||||
<Message msg='Message being edited' editing />
|
<Message msg='Message being edited' editing />
|
||||||
|
|
||||||
<Separator title='Removed' />
|
<Separator title='Removed' />
|
||||||
<Message type='rm' />
|
<Message type='rm' isInfo />
|
||||||
|
|
||||||
<Separator title='Joined' />
|
<Separator title='Joined' />
|
||||||
<Message type='uj' />
|
<Message type='uj' isInfo />
|
||||||
|
|
||||||
<Separator title='Room name changed' />
|
<Separator title='Room name changed' />
|
||||||
<Message msg='New name' type='r' />
|
<Message msg='New name' type='r' isInfo />
|
||||||
|
|
||||||
<Separator title='Message pinned' />
|
<Separator title='Message pinned' />
|
||||||
<Message
|
<Message
|
||||||
msg='New name'
|
msg='New name'
|
||||||
type='message_pinned'
|
type='message_pinned'
|
||||||
|
isInfo
|
||||||
attachments={[{
|
attachments={[{
|
||||||
author_name: 'rocket.cat',
|
author_name: 'rocket.cat',
|
||||||
ts: date,
|
ts: date,
|
||||||
|
@ -587,25 +632,26 @@ export default (
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator title='Has left the channel' />
|
<Separator title='Has left the channel' />
|
||||||
<Message type='ul' />
|
<Message type='ul' isInfo />
|
||||||
|
|
||||||
<Separator title='User removed' />
|
<Separator title='User removed' />
|
||||||
<Message msg='rocket.cat' type='ru' />
|
<Message msg='rocket.cat' type='ru' isInfo />
|
||||||
|
|
||||||
<Separator title='User added' />
|
<Separator title='User added' />
|
||||||
<Message msg='rocket.cat' type='au' />
|
<Message msg='rocket.cat' type='au' isInfo />
|
||||||
|
|
||||||
<Separator title='User muted' />
|
<Separator title='User muted' />
|
||||||
<Message msg='rocket.cat' type='user-muted' />
|
<Message msg='rocket.cat' type='user-muted' isInfo />
|
||||||
|
|
||||||
<Separator title='User unmuted' />
|
<Separator title='User unmuted' />
|
||||||
<Message msg='rocket.cat' type='user-unmuted' />
|
<Message msg='rocket.cat' type='user-unmuted' isInfo />
|
||||||
|
|
||||||
<Separator title='Role added' />
|
<Separator title='Role added' />
|
||||||
<Message
|
<Message
|
||||||
msg='rocket.cat'
|
msg='rocket.cat'
|
||||||
role='admin' // eslint-disable-line
|
role='admin' // eslint-disable-line
|
||||||
type='subscription-role-added'
|
type='subscription-role-added'
|
||||||
|
isInfo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator title='Role removed' />
|
<Separator title='Role removed' />
|
||||||
|
@ -613,19 +659,20 @@ export default (
|
||||||
msg='rocket.cat'
|
msg='rocket.cat'
|
||||||
role='admin' // eslint-disable-line
|
role='admin' // eslint-disable-line
|
||||||
type='subscription-role-removed'
|
type='subscription-role-removed'
|
||||||
|
isInfo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Separator title='Changed description' />
|
<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' />
|
<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' />
|
<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' />
|
<Separator title='Changed type' />
|
||||||
<Message msg='public' type='room_changed_privacy' />
|
<Message msg='public' type='room_changed_privacy' isInfo />
|
||||||
|
|
||||||
<Separator title='Custom style' />
|
<Separator title='Custom style' />
|
||||||
<Message msg='Message' style={[styles.normalize, { backgroundColor: '#ddd' }]} />
|
<Message msg='Message' style={[styles.normalize, { backgroundColor: '#ddd' }]} />
|
||||||
|
|
|
@ -10526,6 +10526,11 @@ react-native-slider@^0.11.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
prop-types "^15.5.6"
|
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:
|
react-native-splash-screen@^3.2.0:
|
||||||
version "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"
|
resolved "https://registry.yarnpkg.com/react-native-splash-screen/-/react-native-splash-screen-3.2.0.tgz#d47ec8557b1ba988ee3ea98d01463081b60fff45"
|
||||||
|
|
Loading…
Reference in New Issue