[NEW] Discussions (#696)

This commit is contained in:
Diego Sampaio 2019-04-08 09:35:28 -03:00 committed by Diego Mello
parent 7a80550d61
commit 1d9acdb700
52 changed files with 4589 additions and 726 deletions

File diff suppressed because it is too large Load Diff

View File

@ -26,17 +26,7 @@ export const ROOMS = createRequestTypes('ROOMS', [
'OPEN_SEARCH_HEADER', 'OPEN_SEARCH_HEADER',
'CLOSE_SEARCH_HEADER' 'CLOSE_SEARCH_HEADER'
]); ]);
export const ROOM = createRequestTypes('ROOM', [ export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
'ADD_USER_TYPING',
'REMOVE_USER_TYPING',
'SOMEONE_TYPING',
'OPEN',
'CLOSE',
'LEAVE',
'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED'
]);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']); export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [ export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes, ...defaultTypes,

View File

@ -1,40 +1,5 @@
import * as types from './actionsTypes'; import * as types from './actionsTypes';
export function removeUserTyping(username) {
return {
type: types.ROOM.REMOVE_USER_TYPING,
username
};
}
export function someoneTyping(data) {
return {
type: types.ROOM.SOMEONE_TYPING,
...data
};
}
export function addUserTyping(username) {
return {
type: types.ROOM.ADD_USER_TYPING,
username
};
}
export function openRoom(room) {
return {
type: types.ROOM.OPEN,
room
};
}
export function closeRoom() {
return {
type: types.ROOM.CLOSE
};
}
export function leaveRoom(rid, t) { export function leaveRoom(rid, t) {
return { return {
type: types.ROOM.LEAVE, type: types.ROOM.LEAVE,
@ -51,16 +16,10 @@ export function eraseRoom(rid, t) {
}; };
} }
export function userTyping(status = true) { export function userTyping(rid, status = true) {
return { return {
type: types.ROOM.USER_TYPING, type: types.ROOM.USER_TYPING,
rid,
status status
}; };
} }
export function roomMessageReceived(message) {
return {
type: types.ROOM.MESSAGE_RECEIVED,
message
};
}

View File

@ -9,7 +9,7 @@ import I18n from '../i18n';
import debounce from '../utils/debounce'; import debounce from '../utils/debounce';
import sharedStyles from '../views/Styles'; import sharedStyles from '../views/Styles';
import { import {
COLOR_BACKGROUND_CONTAINER, COLOR_DANGER, COLOR_SUCCESS, COLOR_WHITE COLOR_BACKGROUND_CONTAINER, COLOR_DANGER, COLOR_SUCCESS, COLOR_WHITE, COLOR_TEXT_DESCRIPTION
} from '../constants/colors'; } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -117,7 +117,7 @@ class ConnectionBadge extends Component {
if (connecting) { if (connecting) {
return ( return (
<Animated.View style={[styles.container, { transform: [{ translateY }] }]}> <Animated.View style={[styles.container, { transform: [{ translateY }] }]}>
<ActivityIndicator color='#9EA2A8' style={styles.activityIndicator} /> <ActivityIndicator color={COLOR_TEXT_DESCRIPTION} style={styles.activityIndicator} />
<Text style={[styles.text, styles.textConnecting]}>{I18n.t('Connecting')}</Text> <Text style={[styles.text, styles.textConnecting]}>{I18n.t('Connecting')}</Text>
</Animated.View> </Animated.View>
); );

View File

@ -48,25 +48,7 @@ const imagePickerConfig = {
cropperCancelText: I18n.t('Cancel') cropperCancelText: I18n.t('Cancel')
}; };
@connect(state => ({ class MessageBox extends Component {
roomType: state.room.t,
message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
}), dispatch => ({
editCancel: () => dispatch(editCancelAction()),
editRequest: message => dispatch(editRequestAction(message)),
typing: status => dispatch(userTypingAction(status)),
closeReply: () => dispatch(replyCancelAction())
}))
export default class MessageBox extends Component {
static propTypes = { static propTypes = {
rid: PropTypes.string.isRequired, rid: PropTypes.string.isRequired,
baseUrl: PropTypes.string.isRequired, baseUrl: PropTypes.string.isRequired,
@ -113,6 +95,7 @@ export default class MessageBox extends Component {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid); const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room.draftMessage && room.draftMessage !== '') { if (room.draftMessage && room.draftMessage !== '') {
this.setInput(room.draftMessage); this.setInput(room.draftMessage);
this.setShowSend(true);
} }
} }
@ -120,6 +103,9 @@ export default class MessageBox extends Component {
const { message, replyMessage } = this.props; const { message, replyMessage } = this.props;
if (message !== nextProps.message && nextProps.message.msg) { if (message !== nextProps.message && nextProps.message.msg) {
this.setInput(nextProps.message.msg); this.setInput(nextProps.message.msg);
if (this.text) {
this.setShowSend(true);
}
this.focus(); this.focus();
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) { } else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
this.focus(); this.focus();
@ -165,14 +151,6 @@ export default class MessageBox extends Component {
return false; return false;
} }
componentWillUnmount() {
const { rid } = this.props;
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
database.write(() => {
room.draftMessage = this.text;
});
}
onChangeText = (text) => { onChangeText = (text) => {
const isTextEmpty = text.length === 0; const isTextEmpty = text.length === 0;
this.setShowSend(!isTextEmpty); this.setShowSend(!isTextEmpty);
@ -461,13 +439,13 @@ export default class MessageBox extends Component {
} }
handleTyping = (isTyping) => { handleTyping = (isTyping) => {
const { typing } = this.props; const { typing, rid } = this.props;
if (!isTyping) { if (!isTyping) {
if (this.typingTimeout) { if (this.typingTimeout) {
clearTimeout(this.typingTimeout); clearTimeout(this.typingTimeout);
this.typingTimeout = false; this.typingTimeout = false;
} }
typing(false); typing(rid, false);
return; return;
} }
@ -476,7 +454,7 @@ export default class MessageBox extends Component {
} }
this.typingTimeout = setTimeout(() => { this.typingTimeout = setTimeout(() => {
typing(true); typing(rid, true);
this.typingTimeout = false; this.typingTimeout = false;
}, 1000); }, 1000);
} }
@ -835,3 +813,25 @@ export default class MessageBox extends Component {
); );
} }
} }
const mapStateToProps = state => ({
message: state.messages.message,
replyMessage: state.messages.replyMessage,
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
});
const dispatchToProps = ({
editCancel: () => editCancelAction(),
editRequest: message => editRequestAction(message),
typing: (rid, status) => userTypingAction(rid, status),
closeReply: () => replyCancelAction()
});
export default connect(mapStateToProps, dispatchToProps, null, { forwardRef: true })(MessageBox);

View File

@ -1,12 +1,18 @@
import React from 'react'; import React from 'react';
import { Image, StyleSheet } from 'react-native'; import { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { COLOR_TEXT_DESCRIPTION } from '../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
style: { style: {
marginRight: 7, marginRight: 7,
marginTop: 3, marginTop: 3,
tintColor: '#9EA2A8' tintColor: COLOR_TEXT_DESCRIPTION,
color: COLOR_TEXT_DESCRIPTION
},
discussion: {
marginRight: 6
} }
}); });
@ -15,6 +21,11 @@ const RoomTypeIcon = React.memo(({ type, size, style }) => {
return null; return null;
} }
if (type === 'discussion') {
// FIXME: These are temporary only. We should have all room icons on <Customicon />, but our design team is still working on this.
return <CustomIcon name='chat' size={13} style={[styles.style, styles.discussion]} />;
}
if (type === 'c') { if (type === 'c') {
return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size }]} />; return <Image source={{ uri: 'hashtag' }} style={[styles.style, style, { width: size, height: size }]} />;
} }

View File

@ -1,13 +1,12 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
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 { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import PhotoModal from './PhotoModal'; import PhotoModal from './PhotoModal';
import Markdown from './Markdown'; import Markdown from './Markdown';
import styles from './styles'; import styles from './styles';
import { COLOR_WHITE } from '../../constants/colors';
export default class extends Component { export default class extends Component {
static propTypes = { static propTypes = {
@ -37,7 +36,7 @@ export default class extends Component {
return false; return false;
} }
onPressButton() { onPressButton = () => {
this.setState({ this.setState({
modalVisible: true modalVisible: true
}); });
@ -67,20 +66,21 @@ export default class extends Component {
return ( return (
[ [
<RectButton <Touchable
key='image' key='image'
onPress={() => this.onPressButton()} onPress={this.onPressButton}
onActiveStateChange={this.isPressed}
style={styles.imageContainer} style={styles.imageContainer}
underlayColor={COLOR_WHITE} background={Touchable.Ripple('#fff')}
> >
<FastImage <React.Fragment>
style={[styles.image, isPressed && { opacity: 0.5 }]} <FastImage
source={{ uri: encodeURI(img) }} style={[styles.image, isPressed && { opacity: 0.5 }]}
resizeMode={FastImage.resizeMode.cover} source={{ uri: encodeURI(img) }}
/> resizeMode={FastImage.resizeMode.cover}
{this.getDescription()} />
</RectButton>, {this.getDescription()}
</React.Fragment>
</Touchable>,
<PhotoModal <PhotoModal
key='modal' key='modal'
title={file.title} title={file.title}

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Text, Image, Platform } from 'react-native'; import { Text, Image } from 'react-native';
import PropTypes from 'prop-types'; 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';
@ -8,20 +8,12 @@ import styles from './styles';
import CustomEmoji from '../EmojiPicker/CustomEmoji'; import CustomEmoji from '../EmojiPicker/CustomEmoji';
import MarkdownEmojiPlugin from './MarkdownEmojiPlugin'; import MarkdownEmojiPlugin from './MarkdownEmojiPlugin';
import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY } from '../../constants/colors';
// Support <http://link|Text> // Support <http://link|Text>
const formatText = text => text.replace( const formatText = text => text.replace(
new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'), new RegExp('(?:<|<)((?:https|http):\\/\\/[^\\|]+)\\|(.+?)(?=>|>)(?:>|>)', 'gm'),
(match, url, title) => `[${ title }](${ url })` (match, url, title) => `[${ title }](${ url })`
); );
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default class Markdown extends React.Component { export default class Markdown extends React.Component {
shouldComponentUpdate(nextProps) { shouldComponentUpdate(nextProps) {
const { msg } = this.props; const { msg } = this.props;
@ -94,31 +86,10 @@ export default class Markdown extends React.Component {
}} }}
style={{ style={{
paragraph: styles.paragraph, paragraph: styles.paragraph,
text: { text: styles.text,
fontSize: 16, codeInline: styles.codeInline,
...sharedStyles.textColorNormal, codeBlock: styles.codeBlock,
...sharedStyles.textRegular link: styles.link,
},
codeInline: {
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderRadius: 4
},
codeBlock: {
...sharedStyles.textRegular,
...codeFontFamily,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
...style ...style
}} }}
plugins={[ plugins={[

View File

@ -6,8 +6,9 @@ import {
import moment from 'moment'; import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import { import {
State, RectButton, LongPressGestureHandler, BorderlessButton BorderlessButton
} from 'react-native-gesture-handler'; } from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable';
import Image from './Image'; import Image from './Image';
import User from './User'; import User from './User';
@ -23,7 +24,7 @@ import styles from './styles';
import I18n from '../../i18n'; import I18n from '../../i18n';
import messagesStatus from '../../constants/messagesStatus'; import messagesStatus from '../../constants/messagesStatus';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
import { COLOR_DANGER, COLOR_TEXT_DESCRIPTION, COLOR_WHITE } from '../../constants/colors'; import { COLOR_DANGER } from '../../constants/colors';
const SYSTEM_MESSAGES = [ const SYSTEM_MESSAGES = [
'r', 'r',
@ -31,6 +32,7 @@ const SYSTEM_MESSAGES = [
'ru', 'ru',
'ul', 'ul',
'uj', 'uj',
'ut',
'rm', 'rm',
'user-muted', 'user-muted',
'user-unmuted', 'user-unmuted',
@ -41,7 +43,8 @@ const SYSTEM_MESSAGES = [
'room_changed_announcement', 'room_changed_announcement',
'room_changed_topic', 'room_changed_topic',
'room_changed_privacy', 'room_changed_privacy',
'message_snippeted' 'message_snippeted',
'thread-created'
]; ];
const getInfoMessage = ({ const getInfoMessage = ({
@ -52,6 +55,8 @@ const getInfoMessage = ({
return I18n.t('Message_removed'); return I18n.t('Message_removed');
} else if (type === 'uj') { } else if (type === 'uj') {
return I18n.t('Has_joined_the_channel'); return I18n.t('Has_joined_the_channel');
} else if (type === 'ut') {
return I18n.t('Has_joined_the_conversation');
} else if (type === 'r') { } else if (type === 'r') {
return I18n.t('Room_name_changed', { name: msg, userBy: username }); return I18n.t('Room_name_changed', { name: msg, userBy: username });
} else if (type === 'message_pinned') { } else if (type === 'message_pinned') {
@ -80,9 +85,14 @@ const getInfoMessage = ({
return I18n.t('Room_changed_privacy', { type: msg, userBy: username }); return I18n.t('Room_changed_privacy', { type: msg, userBy: username });
} else if (type === 'message_snippeted') { } else if (type === 'message_snippeted') {
return I18n.t('Created_snippet'); return I18n.t('Created_snippet');
} else if (type === 'thread-created') {
return I18n.t('Thread_created', { name: msg });
} }
return ''; return '';
}; };
const BUTTON_HIT_SLOP = {
top: 4, right: 4, bottom: 4, left: 4
};
export default class Message extends PureComponent { export default class Message extends PureComponent {
static propTypes = { static propTypes = {
@ -125,12 +135,15 @@ export default class Message extends PureComponent {
PropTypes.object PropTypes.object
]), ]),
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
dcount: PropTypes.number,
dlm: PropTypes.instanceOf(Date),
// methods // methods
closeReactions: PropTypes.func, closeReactions: PropTypes.func,
onErrorPress: PropTypes.func, onErrorPress: PropTypes.func,
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onReactionLongPress: PropTypes.func, onReactionLongPress: PropTypes.func,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
replyBroadcast: PropTypes.func, replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func toggleReactionPicker: PropTypes.func
} }
@ -282,31 +295,27 @@ export default class Message extends PureComponent {
user, onReactionLongPress, onReactionPress, customEmojis, baseUrl user, onReactionLongPress, onReactionPress, customEmojis, baseUrl
} = this.props; } = this.props;
const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1; const reacted = reaction.usernames.findIndex(item => item.value === user.username) !== -1;
const underlayColor = reacted ? COLOR_WHITE : COLOR_TEXT_DESCRIPTION;
return ( return (
<LongPressGestureHandler <Touchable
onPress={() => onReactionPress(reaction.emoji)}
onLongPress={onReactionLongPress}
key={reaction.emoji} key={reaction.emoji}
onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()} testID={`message-reaction-${ reaction.emoji }`}
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
background={Touchable.Ripple('#fff')}
hitSlop={BUTTON_HIT_SLOP}
> >
<RectButton <View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
onPress={() => onReactionPress(reaction.emoji)} <Emoji
testID={`message-reaction-${ reaction.emoji }`} content={reaction.emoji}
style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]} customEmojis={customEmojis}
activeOpacity={0.8} standardEmojiStyle={styles.reactionEmoji}
underlayColor={underlayColor} customEmojiStyle={styles.reactionCustomEmoji}
> baseUrl={baseUrl}
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}> />
<Emoji <Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
content={reaction.emoji} </View>
customEmojis={customEmojis} </Touchable>
standardEmojiStyle={styles.reactionEmoji}
customEmojiStyle={styles.reactionCustomEmoji}
baseUrl={baseUrl}
/>
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
</View>
</RectButton>
</LongPressGestureHandler>
); );
} }
@ -318,18 +327,18 @@ export default class Message extends PureComponent {
return ( return (
<View style={styles.reactionsContainer}> <View style={styles.reactionsContainer}>
{reactions.map(this.renderReaction)} {reactions.map(this.renderReaction)}
<RectButton <Touchable
onPress={toggleReactionPicker} onPress={toggleReactionPicker}
key='message-add-reaction' key='message-add-reaction'
testID='message-add-reaction' testID='message-add-reaction'
style={styles.reactionButton} style={styles.reactionButton}
activeOpacity={0.8} background={Touchable.Ripple('#fff')}
underlayColor='#e1e5e8' hitSlop={BUTTON_HIT_SLOP}
> >
<View style={styles.reactionContainer}> <View style={styles.reactionContainer}>
<CustomIcon name='add-reaction' size={21} style={styles.addReaction} /> <CustomIcon name='add-reaction' size={21} style={styles.addReaction} />
</View> </View>
</RectButton> </Touchable>
</View> </View>
); );
} }
@ -338,20 +347,86 @@ export default class Message extends PureComponent {
const { broadcast, replyBroadcast } = this.props; const { broadcast, replyBroadcast } = this.props;
if (broadcast && !this.isOwn()) { if (broadcast && !this.isOwn()) {
return ( return (
<RectButton <View style={styles.buttonContainer}>
onPress={replyBroadcast} <Touchable
style={styles.broadcastButton} onPress={replyBroadcast}
activeOpacity={0.5} background={Touchable.Ripple('#fff')}
underlayColor={COLOR_WHITE} style={styles.button}
> hitSlop={BUTTON_HIT_SLOP}
<CustomIcon name='back' size={20} style={styles.broadcastButtonIcon} /> >
<Text style={styles.broadcastButtonText}>{I18n.t('Reply')}</Text> <React.Fragment>
</RectButton> <CustomIcon name='back' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
</React.Fragment>
</Touchable>
</View>
); );
} }
return null; return null;
} }
renderDiscussion = () => {
const {
msg, dcount, dlm, onDiscussionPress
} = this.props;
const time = dlm ? moment(dlm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
let buttonText = 'No messages yet';
if (dcount === 1) {
buttonText = `${ dcount } message`;
} else if (dcount > 1 && dcount < 1000) {
buttonText = `${ dcount } messages`;
} else if (dcount > 999) {
buttonText = '+999 messages';
}
return (
<React.Fragment>
<Text style={styles.textInfo}>{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>
);
}
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.renderReactions()}
{this.renderBroadcastReply()}
</React.Fragment>
);
}
render() { render() {
const { const {
editing, style, header, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl editing, style, header, reactionsModal, closeReactions, msg, ts, reactions, author, user, timeFormat, customEmojis, baseUrl
@ -380,12 +455,7 @@ export default class Message extends PureComponent {
this.isTemp() && styles.temp this.isTemp() && styles.temp
]} ]}
> >
{this.renderUsername()} {this.renderInner()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</View> </View>
</View> </View>
{reactionsModal {reactionsModal

View File

@ -2,12 +2,12 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import moment from 'moment'; import moment from 'moment';
import { RectButton } from 'react-native-gesture-handler'; import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from './Markdown';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_WHITE } from '../../constants/colors'; import { COLOR_BACKGROUND_CONTAINER, COLOR_BORDER } from '../../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
button: { button: {
@ -140,18 +140,17 @@ const Reply = ({
}; };
return ( return (
<RectButton <Touchable
onPress={() => onPress(attachment, baseUrl, user)} onPress={() => onPress(attachment, baseUrl, user)}
style={[styles.button, index > 0 && styles.marginTop]} style={[styles.button, index > 0 && styles.marginTop]}
activeOpacity={0.5} background={Touchable.Ripple('#fff')}
underlayColor={COLOR_WHITE}
> >
<View style={styles.attachmentContainer}> <View style={styles.attachmentContainer}>
{renderTitle()} {renderTitle()}
{renderText()} {renderText()}
{renderFields()} {renderFields()}
</View> </View>
</RectButton> </Touchable>
); );
}; };

View File

@ -2,12 +2,12 @@ import React from 'react';
import { View, Text, StyleSheet } from 'react-native'; import { View, Text, StyleSheet } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image'; import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler'; import Touchable from 'react-native-platform-touchable';
import openLink from '../../utils/openLink'; import openLink from '../../utils/openLink';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { import {
COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE COLOR_BACKGROUND_CONTAINER, COLOR_BORDER, COLOR_PRIMARY
} from '../../constants/colors'; } from '../../constants/colors';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
@ -58,18 +58,19 @@ const Url = ({ url, index }) => {
return null; return null;
} }
return ( return (
<RectButton <Touchable
onPress={() => onPress(url.url)} onPress={() => onPress(url.url)}
style={[styles.button, index > 0 && styles.marginTop, styles.container]} style={[styles.button, index > 0 && styles.marginTop, styles.container]}
activeOpacity={0.5} background={Touchable.Ripple('#fff')}
underlayColor={COLOR_WHITE}
> >
{url.image ? <FastImage source={{ uri: url.image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} /> : null} <React.Fragment>
<View style={styles.textContainer}> {url.image ? <FastImage source={{ uri: url.image }} style={styles.image} resizeMode={FastImage.resizeMode.cover} /> : null}
<Text style={styles.title} numberOfLines={2}>{url.title}</Text> <View style={styles.textContainer}>
<Text style={styles.description} numberOfLines={2}>{url.description}</Text> <Text style={styles.title} numberOfLines={2}>{url.title}</Text>
</View> <Text style={styles.description} numberOfLines={2}>{url.description}</Text>
</RectButton> </View>
</React.Fragment>
</Touchable>
); );
}; };

View File

@ -4,6 +4,7 @@ import { View, Text, StyleSheet } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import messageStyles from './styles';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
container: { container: {
@ -26,14 +27,6 @@ const styles = StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textColorDescription, ...sharedStyles.textColorDescription,
...sharedStyles.textRegular ...sharedStyles.textRegular
},
time: {
fontSize: 12,
paddingLeft: 10,
lineHeight: 22,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
} }
}); });
@ -70,7 +63,7 @@ export default class User extends React.PureComponent {
{aliasUsername} {aliasUsername}
</Text> </Text>
</View> </View>
<Text style={styles.time}>{time}</Text> <Text style={messageStyles.time}>{time}</Text>
</View> </View>
); );
} }

View File

@ -3,13 +3,12 @@ import PropTypes from 'prop-types';
import { StyleSheet, View } from 'react-native'; import { StyleSheet, View } from 'react-native';
import Modal from 'react-native-modal'; import Modal from 'react-native-modal';
import VideoPlayer from 'react-native-video-controls'; import VideoPlayer from 'react-native-video-controls';
import { RectButton } from 'react-native-gesture-handler'; import Touchable from 'react-native-platform-touchable';
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 { COLOR_WHITE } from '../../constants/colors';
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;
@ -49,13 +48,13 @@ export default class Video extends React.PureComponent {
return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`; return `${ baseUrl }${ video_url }?rc_uid=${ user.id }&rc_token=${ user.token }`;
} }
toggleModal() { toggleModal = () => {
this.setState(prevState => ({ this.setState(prevState => ({
isVisible: !prevState.isVisible isVisible: !prevState.isVisible
})); }));
} }
open() { open = () => {
const { file } = this.props; const { file } = this.props;
if (isTypeSupported(file.video_type)) { if (isTypeSupported(file.video_type)) {
return this.toggleModal(); return this.toggleModal();
@ -77,18 +76,17 @@ export default class Video extends React.PureComponent {
return ( return (
[ [
<View key='button'> <View key='button'>
<RectButton <Touchable
onPress={this.open}
style={styles.button} style={styles.button}
onPress={() => this.open()} background={Touchable.Ripple('#fff')}
activeOpacity={0.5}
underlayColor={COLOR_WHITE}
> >
<CustomIcon <CustomIcon
name='play' name='play'
size={54} size={54}
style={styles.image} style={styles.image}
/> />
</RectButton> </Touchable>
<Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} /> <Markdown msg={description} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} />
</View>, </View>,
<Modal <Modal
@ -100,7 +98,7 @@ export default class Video extends React.PureComponent {
> >
<VideoPlayer <VideoPlayer
source={{ uri: this.uri }} source={{ uri: this.uri }}
onBack={() => this.toggleModal()} onBack={this.toggleModal}
disableVolume disableVolume
/> />
</Modal> </Modal>

View File

@ -50,6 +50,7 @@ export default class MessageContainer extends React.Component {
// methods - props // methods - props
onLongPress: PropTypes.func, onLongPress: PropTypes.func,
onReactionPress: PropTypes.func, onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
// methods - redux // methods - redux
errorActionsShow: PropTypes.func, errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func, replyBroadcast: PropTypes.func,
@ -116,12 +117,16 @@ export default class MessageContainer extends React.Component {
onReactionPress(emoji, item._id); onReactionPress(emoji, item._id);
} }
onReactionLongPress = () => { onReactionLongPress = () => {
this.setState({ reactionsModal: true }); this.setState({ reactionsModal: true });
vibrate(); vibrate();
} }
onDiscussionPress = () => {
const { onDiscussionPress, item } = this.props;
onDiscussionPress(item);
}
get timeFormat() { get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props; const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat; return customTimeFormat || Message_TimeFormat;
@ -167,7 +172,7 @@ export default class MessageContainer extends React.Component {
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
} = this.props; } = this.props;
const { const {
msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm
} = item; } = item;
const isEditing = editingMessage._id === item._id; const isEditing = editingMessage._id === item._id;
return ( return (
@ -195,6 +200,9 @@ export default class MessageContainer extends React.Component {
reactionsModal={reactionsModal} reactionsModal={reactionsModal}
useRealName={useRealName} useRealName={useRealName}
role={role} role={role}
drid={drid}
dcount={dcount}
dlm={dlm}
closeReactions={this.closeReactions} closeReactions={this.closeReactions}
onErrorPress={this.onErrorPress} onErrorPress={this.onErrorPress}
onLongPress={this.onLongPress} onLongPress={this.onLongPress}
@ -202,6 +210,7 @@ export default class MessageContainer extends React.Component {
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast} replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker} toggleReactionPicker={this.toggleReactionPicker}
onDiscussionPress={this.onDiscussionPress}
/> />
); );
} }

View File

@ -1,7 +1,14 @@
import { StyleSheet } from 'react-native'; import { StyleSheet, Platform } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE } from '../../constants/colors'; import {
COLOR_BORDER, COLOR_PRIMARY, COLOR_WHITE, COLOR_BACKGROUND_CONTAINER
} from '../../constants/colors';
const codeFontFamily = Platform.select({
ios: { fontFamily: 'Courier New' },
android: { fontFamily: 'monospace' }
});
export default StyleSheet.create({ export default StyleSheet.create({
root: { root: {
@ -28,6 +35,11 @@ export default StyleSheet.create({
flexDirection: 'row', flexDirection: 'row',
flex: 1 flex: 1
}, },
text: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textInfo: { textInfo: {
fontStyle: 'italic', fontStyle: 'italic',
fontSize: 16, fontSize: 16,
@ -55,6 +67,9 @@ export default StyleSheet.create({
marginBottom: 6, marginBottom: 6,
borderRadius: 2 borderRadius: 2
}, },
reactionButtonReacted: {
backgroundColor: '#e8f2ff'
},
reactionContainer: { reactionContainer: {
flexDirection: 'row', flexDirection: 'row',
justifyContent: 'center', justifyContent: 'center',
@ -94,22 +109,28 @@ export default StyleSheet.create({
paddingHorizontal: 15, paddingHorizontal: 15,
paddingVertical: 5 paddingVertical: 5
}, },
broadcastButton: { buttonContainer: {
width: 107, marginTop: 6,
flexDirection: 'row',
alignItems: 'center'
},
button: {
paddingHorizontal: 15,
height: 44, height: 44,
marginTop: 15,
flex: 1,
flexDirection: 'row', flexDirection: 'row',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
backgroundColor: COLOR_PRIMARY, backgroundColor: COLOR_PRIMARY,
borderRadius: 4 borderRadius: 4
}, },
broadcastButtonIcon: { smallButton: {
color: COLOR_WHITE, height: 30
marginRight: 11
}, },
broadcastButtonText: { buttonIcon: {
color: COLOR_WHITE,
marginRight: 6
},
buttonText: {
color: COLOR_WHITE, color: COLOR_WHITE,
fontSize: 14, fontSize: 14,
...sharedStyles.textMedium ...sharedStyles.textMedium
@ -139,8 +160,6 @@ export default StyleSheet.create({
imageContainer: { imageContainer: {
flex: 1, flex: 1,
flexDirection: 'column', flexDirection: 'column',
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4 borderRadius: 4
}, },
image: { image: {
@ -148,7 +167,8 @@ export default StyleSheet.create({
maxWidth: 400, maxWidth: 400,
minHeight: 200, minHeight: 200,
borderRadius: 4, borderRadius: 4,
marginBottom: 6 borderColor: COLOR_BORDER,
borderWidth: 1
}, },
inlineImage: { inlineImage: {
width: 300, width: 300,
@ -159,5 +179,33 @@ export default StyleSheet.create({
fontSize: 14, fontSize: 14,
...sharedStyles.textColorDescription, ...sharedStyles.textColorDescription,
...sharedStyles.textRegular ...sharedStyles.textRegular
},
codeInline: {
...sharedStyles.textRegular,
...codeFontFamily,
borderWidth: 1,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderRadius: 4
},
codeBlock: {
...sharedStyles.textRegular,
...codeFontFamily,
backgroundColor: COLOR_BACKGROUND_CONTAINER,
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4,
padding: 4
},
link: {
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
time: {
fontSize: 12,
paddingLeft: 10,
lineHeight: 22,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
} }
}); });

View File

@ -141,6 +141,7 @@ export default {
description: 'description', description: 'description',
Description: 'Description', Description: 'Description',
Disable_notifications: 'Disable notifications', Disable_notifications: 'Disable notifications',
Discussions: 'Discussions',
Direct_Messages: 'Direct Messages', Direct_Messages: 'Direct Messages',
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?',
@ -166,6 +167,7 @@ export default {
Group_by_favorites: 'Group favorites', Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type', Group_by_type: 'Group by type',
Has_joined_the_channel: 'Has joined the channel', Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel', Has_left_the_channel: 'Has left the channel',
Invisible: 'Invisible', Invisible: 'Invisible',
Invite: 'Invite', Invite: 'Invite',
@ -299,6 +301,7 @@ export default {
starred: 'starred', starred: 'starred',
Starred: 'Starred', Starred: 'Starred',
Start_of_conversation: 'Start of conversation', Start_of_conversation: 'Start of conversation',
Started_discussion: 'Started a discussion:',
Submit: 'Submit', Submit: 'Submit',
Take_a_photo: 'Take a photo', Take_a_photo: 'Take a photo',
tap_to_change_status: 'tap to change status', tap_to_change_status: 'tap to change status',
@ -308,6 +311,7 @@ export default {
There_was_an_error_while_action: 'There was an error while {{action}}!', There_was_an_error_while_action: 'There was an error while {{action}}!',
This_room_is_blocked: 'This room is blocked', This_room_is_blocked: 'This room is blocked',
This_room_is_read_only: 'This room is read only', This_room_is_read_only: 'This room is read only',
Thread_created: 'Started a new thread: "{{name}}"',
Timezone: 'Timezone', Timezone: 'Timezone',
Toggle_Drawer: 'Toggle_Drawer', Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic', topic: 'topic',

View File

@ -148,6 +148,7 @@ export default {
description: 'descrição', description: 'descrição',
Description: 'Descrição', Description: 'Descrição',
Disable_notifications: 'Desabilitar notificações', Disable_notifications: 'Desabilitar notificações',
Discussions: 'Discussões',
Direct_Messages: 'Mensagens Diretas', Direct_Messages: 'Mensagens Diretas',
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?',
@ -173,6 +174,7 @@ export default {
Group_by_favorites: 'Agrupar favoritos', Group_by_favorites: 'Agrupar favoritos',
Group_by_type: 'Agrupar por tipo', Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal', Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu da conversa', Has_left_the_channel: 'Saiu da conversa',
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',
@ -300,6 +302,7 @@ export default {
starred: 'favoritou', starred: 'favoritou',
Starred: 'Mensagens Favoritas', Starred: 'Mensagens Favoritas',
Start_of_conversation: 'Início da conversa', Start_of_conversation: 'Início da conversa',
Started_discussion: 'Iniciou uma discussão:',
Submit: 'Enviar', Submit: 'Enviar',
Take_a_photo: 'Tirar uma foto', Take_a_photo: 'Tirar uma foto',
Terms_of_Service: ' Termos de Serviço ', Terms_of_Service: ' Termos de Serviço ',
@ -307,6 +310,7 @@ export default {
There_was_an_error_while_action: 'Aconteceu um erro {{action}}!', There_was_an_error_while_action: 'Aconteceu um erro {{action}}!',
This_room_is_blocked: 'Este quarto está bloqueado', This_room_is_blocked: 'Este quarto está bloqueado',
This_room_is_read_only: 'Este quarto é apenas de leitura', This_room_is_read_only: 'Este quarto é apenas de leitura',
Thread_created: 'Iniciou uma thread: "{{name}}"',
Timezone: 'Fuso horário', Timezone: 'Fuso horário',
topic: 'tópico', topic: 'tópico',
Topic: 'Tópico', Topic: 'Tópico',

View File

@ -166,6 +166,7 @@ export default {
Group_by_favorites: 'Agrupar por favoritos', Group_by_favorites: 'Agrupar por favoritos',
Group_by_type: 'Agrupar por tipo', Group_by_type: 'Agrupar por tipo',
Has_joined_the_channel: 'Entrou no canal', Has_joined_the_channel: 'Entrou no canal',
Has_joined_the_conversation: 'Entrou na conversa',
Has_left_the_channel: 'Saiu do canal', Has_left_the_channel: 'Saiu do canal',
Invisible: 'Invisível', Invisible: 'Invisível',
Invite: 'Convidar', Invite: 'Convidar',

View File

@ -19,7 +19,7 @@ async function load({ rid: roomId, lastOpen }) {
lastUpdate = getLastUpdate(roomId); lastUpdate = getLastUpdate(roomId);
} }
// RC 0.60.0 // RC 0.60.0
const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate, count: 50 }); const { result } = await this.sdk.get('chat.syncMessages', { roomId, lastUpdate });
return result; return result;
} }

View File

@ -39,11 +39,10 @@ export async function sendMessageCall(message) {
export default async function(rid, msg) { export default async function(rid, msg) {
try { try {
const message = getMessage(rid, msg); const message = getMessage(rid, msg);
const room = database.objects('subscriptions').filtered('rid == $0', rid); const [room] = database.objects('subscriptions').filtered('rid == $0', rid);
// TODO: do we need this?
database.write(() => { database.write(() => {
room.lastMessage = message; room.draftMessage = null;
}); });
try { try {

View File

@ -1,18 +1,21 @@
import EJSON from 'ejson';
import log from '../../../utils/log'; import log from '../../../utils/log';
import protectedFunction from '../helpers/protectedFunction';
import buildMessage from '../helpers/buildMessage';
import database from '../../realm';
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom'))); const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
const removeListener = listener => listener.stop(); const removeListener = listener => listener.stop();
let promises;
let timer = null;
let connectedListener;
let disconnectedListener;
export default function subscribeRoom({ rid }) { export default function subscribeRoom({ rid }) {
if (promises) { let promises;
promises.then(unsubscribe); let timer = null;
promises = false; let connectedListener;
} let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
const typingTimeouts = {};
const loop = () => { const loop = () => {
if (timer) { if (timer) {
return; return;
@ -41,6 +44,96 @@ export default function subscribeRoom({ rid }) {
} }
}; };
const getUserTyping = username => (
database
.memoryDatabase.objects('usersTyping')
.filtered('rid = $0 AND username = $1', rid, username)
);
const removeUserTyping = (username) => {
const userTyping = getUserTyping(username);
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.delete(userTyping);
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
} catch (error) {
console.log('TCL: removeUserTyping -> error', error);
}
};
const addUserTyping = (username) => {
const userTyping = getUserTyping(username);
// prevent duplicated
if (userTyping.length === 0) {
try {
database.memoryDatabase.write(() => {
database.memoryDatabase.create('usersTyping', { rid, username });
});
if (typingTimeouts[username]) {
clearTimeout(typingTimeouts[username]);
typingTimeouts[username] = null;
}
typingTimeouts[username] = setTimeout(() => {
removeUserTyping(username);
}, 10000);
} catch (error) {
console.log('TCL: addUserTyping -> error', error);
}
}
};
const handleNotifyRoomReceived = protectedFunction((ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (rid !== _rid) {
return;
}
if (ev === 'typing') {
const [username, typing] = ddpMessage.fields.args;
if (typing) {
addUserTyping(username);
} else {
removeUserTyping(username);
}
} else if (ev === 'deleteMessage') {
database.write(() => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id);
database.delete(message);
}
});
}
});
const handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(ddpMessage.fields.args[0]);
if (rid !== message.rid) {
return;
}
requestAnimationFrame(() => {
try {
database.write(() => {
database.create('messages', EJSON.fromJSONValue(message), true);
});
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room._id) {
this.readMessages(rid);
}
} catch (e) {
console.warn('handleMessageReceived', e);
}
});
});
const stop = () => { const stop = () => {
if (promises) { if (promises) {
promises.then(unsubscribe); promises.then(unsubscribe);
@ -54,12 +147,28 @@ export default function subscribeRoom({ rid }) {
disconnectedListener.then(removeListener); disconnectedListener.then(removeListener);
disconnectedListener = false; disconnectedListener = false;
} }
if (notifyRoomListener) {
notifyRoomListener.then(removeListener);
notifyRoomListener = false;
}
if (messageReceivedListener) {
messageReceivedListener.then(removeListener);
messageReceivedListener = false;
}
clearTimeout(timer); clearTimeout(timer);
timer = false; timer = false;
Object.keys(typingTimeouts).forEach((key) => {
if (typingTimeouts[key]) {
clearTimeout(typingTimeouts[key]);
typingTimeouts[key] = null;
}
});
}; };
connectedListener = this.sdk.onStreamData('connected', handleConnected); connectedListener = this.sdk.onStreamData('connected', handleConnected);
disconnectedListener = this.sdk.onStreamData('close', handleDisconnected); disconnectedListener = this.sdk.onStreamData('close', handleDisconnected);
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
try { try {
promises = this.sdk.subscribeRoom(rid); promises = this.sdk.subscribeRoom(rid);

View File

@ -102,6 +102,7 @@ const subscriptionSchema = {
notifications: { type: 'bool', optional: true }, notifications: { type: 'bool', optional: true },
muted: { type: 'list', objectType: 'usersMuted' }, muted: { type: 'list', objectType: 'usersMuted' },
broadcast: { type: 'bool', optional: true }, broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true } draftMessage: { type: 'string', optional: true }
} }
}; };
@ -219,7 +220,10 @@ const messagesSchema = {
starred: { type: 'bool', optional: true }, starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy', editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' }, reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true } role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true }
} }
}; };
@ -279,6 +283,14 @@ const uploadsSchema = {
} }
}; };
const usersTypingSchema = {
name: 'usersTyping',
properties: {
rid: { type: 'string', indexed: true },
username: { type: 'string', optional: true }
}
};
const schema = [ const schema = [
settingsSchema, settingsSchema,
subscriptionSchema, subscriptionSchema,
@ -302,6 +314,8 @@ const schema = [
uploadsSchema uploadsSchema
]; ];
const inMemorySchema = [usersTypingSchema];
class DB { class DB {
databases = { databases = {
serversDB: new Realm({ serversDB: new Realm({
@ -309,7 +323,23 @@ class DB {
schema: [ schema: [
serversSchema serversSchema
], ],
schemaVersion: 1 schemaVersion: 2,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion === 1 && newRealm.schemaVersion === 2) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < newServers.length; i++) {
newServers[i].roomsUpdatedAt = null;
}
}
}
}),
inMemoryDB: new Realm({
path: 'memory.realm',
schema: inMemorySchema,
schemaVersion: 1,
inMemory: true
}) })
} }
@ -337,12 +367,29 @@ class DB {
return this.databases.activeDB; return this.databases.activeDB;
} }
get memoryDatabase() {
return this.databases.inMemoryDB;
}
setActiveDB(database = '') { setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, ''); const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({ return this.databases.activeDB = new Realm({
path: `${ path }.realm`, path: `${ path }.realm`,
schema, schema,
schemaVersion: 2 schemaVersion: 4,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion === 3 && newRealm.schemaVersion === 4) {
const newSubs = newRealm.objects('subscriptions');
// eslint-disable-next-line no-plusplus
for (let i = 0; i < newSubs.length; i++) {
newSubs[i].lastOpen = null;
newSubs[i].ls = null;
}
const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages);
}
}
}); });
} }
} }

View File

@ -15,7 +15,6 @@ import {
} from '../actions/login'; } from '../actions/login';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect'; import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
import { setActiveUser } from '../actions/activeUsers'; import { setActiveUser } from '../actions/activeUsers';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setRoles } from '../actions/roles'; import { setRoles } from '../actions/roles';
import subscribeRooms from './methods/subscriptions/rooms'; import subscribeRooms from './methods/subscriptions/rooms';
@ -30,7 +29,6 @@ import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis'; import getCustomEmoji from './methods/getCustomEmojis';
import canOpenRoom from './methods/canOpenRoom'; import canOpenRoom from './methods/canOpenRoom';
import _buildMessage from './methods/helpers/buildMessage';
import loadMessagesForRoom from './methods/loadMessagesForRoom'; import loadMessagesForRoom from './methods/loadMessagesForRoom';
import loadMissedMessages from './methods/loadMissedMessages'; import loadMissedMessages from './methods/loadMissedMessages';
@ -202,27 +200,6 @@ const RocketChat = {
this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage))); this.sdk.onStreamData('users', protectedFunction(ddpMessage => RocketChat._setUser(ddpMessage)));
this.sdk.onStreamData('stream-room-messages', (ddpMessage) => {
// TODO: debounce
const message = _buildMessage(ddpMessage.fields.args[0]);
requestAnimationFrame(() => reduxStore.dispatch(roomMessageReceived(message)));
});
this.sdk.onStreamData('stream-notify-room', protectedFunction((ddpMessage) => {
const [_rid, ev] = ddpMessage.fields.eventName.split('/');
if (ev === 'typing') {
reduxStore.dispatch(someoneTyping({ _rid, username: ddpMessage.fields.args[0], typing: ddpMessage.fields.args[1] }));
} else if (ev === 'deleteMessage') {
database.write(() => {
if (ddpMessage && ddpMessage.fields && ddpMessage.fields.args.length > 0) {
const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id);
database.delete(message);
}
});
}
}));
this.sdk.onStreamData('rocketchat_roles', protectedFunction((ddpMessage) => { this.sdk.onStreamData('rocketchat_roles', protectedFunction((ddpMessage) => {
this.roles = this.roles || {}; this.roles = this.roles || {};
@ -567,6 +544,9 @@ const RocketChat = {
unsubscribe(subscription) { unsubscribe(subscription) {
return this.sdk.unsubscribe(subscription); return this.sdk.unsubscribe(subscription);
}, },
onStreamData(...args) {
return this.sdk.onStreamData(...args);
},
emitTyping(room, t = true) { emitTyping(room, t = true) {
const { login } = reduxStore.getState(); const { login } = reduxStore.getState();
return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t); return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
@ -600,6 +580,10 @@ const RocketChat = {
// RC 0.65.0 // RC 0.65.0
return this.sdk.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId }); return this.sdk.get(`${ this.roomTypeToApiType(t) }.counters`, { roomId });
}, },
getChannelInfo(roomId) {
// RC 0.48.0
return this.sdk.get('channels.info', { roomId });
},
async getRoomMember(rid, currentUserId) { async getRoomMember(rid, currentUserId) {
try { try {
if (rid === `${ currentUserId }${ currentUserId }`) { if (rid === `${ currentUserId }${ currentUserId }`) {
@ -669,7 +653,13 @@ const RocketChat = {
let roles = []; let roles = [];
try { try {
// get the room from realm // get the room from realm
const room = database.objects('subscriptions').filtered('rid = $0', rid)[0]; const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (!room) {
return permissions.reduce((result, permission) => {
result[permission] = false;
return result;
}, {});
}
// get room roles // get room roles
roles = room.roles; // eslint-disable-line prefer-destructuring roles = room.roles; // eslint-disable-line prefer-destructuring
} catch (error) { } catch (error) {

File diff suppressed because one or more lines are too long

View File

@ -137,6 +137,7 @@ export default class RoomItem extends React.Component {
unread: PropTypes.number, unread: PropTypes.number,
userMentions: PropTypes.number, userMentions: PropTypes.number,
id: PropTypes.string, id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func, onPress: PropTypes.func,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
@ -209,11 +210,11 @@ export default class RoomItem extends React.Component {
} }
get type() { get type() {
const { type, id } = this.props; const { type, id, prid } = this.props;
if (type === 'd') { if (type === 'd') {
return <Status style={styles.status} size={10} id={id} />; return <Status style={styles.status} size={10} id={id} />;
} }
return <RoomTypeIcon type={type} />; return <RoomTypeIcon type={prid ? 'discussion' : type} />;
} }
formatDate = date => moment(date).calendar(null, { formatDate = date => moment(date).calendar(null, {

View File

@ -3,7 +3,6 @@ import settings from './reducers';
import login from './login'; import login from './login';
import meteor from './connect'; import meteor from './connect';
import messages from './messages'; import messages from './messages';
import room from './room';
import rooms from './rooms'; import rooms from './rooms';
import server from './server'; import server from './server';
import selectedUsers from './selectedUsers'; import selectedUsers from './selectedUsers';
@ -23,7 +22,6 @@ export default combineReducers({
selectedUsers, selectedUsers,
createChannel, createChannel,
app, app,
room,
rooms, rooms,
customEmojis, customEmojis,
activeUsers, activeUsers,

View File

@ -1,31 +0,0 @@
import * as types from '../actions/actionsTypes';
const initialState = {
usersTyping: []
};
export default function room(state = initialState, action) {
switch (action.type) {
case types.ROOM.OPEN:
return {
...initialState,
...action.room
};
case types.ROOM.CLOSE:
return {
...initialState
};
case types.ROOM.ADD_USER_TYPING:
return {
...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username), action.username]
};
case types.ROOM.REMOVE_USER_TYPING:
return {
...state,
usersTyping: [...state.usersTyping.filter(user => user !== action.username)]
};
default:
return state;
}
}

View File

@ -1,122 +1,30 @@
import { Alert } from 'react-native'; import { Alert } from 'react-native';
import { import {
put, call, takeLatest, take, select, race, fork, cancel, takeEvery call, takeLatest, take, select
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import { delay } from 'redux-saga'; import { delay } from 'redux-saga';
import EJSON from 'ejson';
import Navigation from '../lib/Navigation'; import Navigation from '../lib/Navigation';
import * as types from '../actions/actionsTypes'; import * as types from '../actions/actionsTypes';
import { addUserTyping, removeUserTyping } from '../actions/room';
import { messagesRequest, editCancel, replyCancel } from '../actions/messages';
import RocketChat from '../lib/rocketchat'; import RocketChat from '../lib/rocketchat';
import database from '../lib/realm';
import log from '../utils/log'; import log from '../utils/log';
import I18n from '../i18n'; import I18n from '../i18n';
const watchUserTyping = function* watchUserTyping({ rid, status }) {
let sub;
let thread;
const cancelTyping = function* cancelTyping(username) {
while (true) {
const { typing, timeout } = yield race({
typing: take(types.ROOM.SOMEONE_TYPING),
timeout: call(delay, 5000)
});
if (timeout || (typing.username === username && !typing.typing)) {
return yield put(removeUserTyping(username));
}
}
};
const usersTyping = function* usersTyping({ rid }) {
while (true) {
const { _rid, username, typing } = yield take(types.ROOM.SOMEONE_TYPING);
if (_rid === rid) {
yield (typing ? put(addUserTyping(username)) : put(removeUserTyping(username)));
if (typing) {
yield fork(cancelTyping, username);
}
}
}
};
const handleMessageReceived = function* handleMessageReceived({ message }) {
try {
const room = yield select(state => state.room);
if (message.rid === room.rid) {
database.write(() => {
database.create('messages', EJSON.fromJSONValue(message), true);
});
if (room._id) {
RocketChat.readMessages(room.rid);
}
}
} catch (e) {
console.warn('handleMessageReceived', e);
}
};
let opened = false;
const watchRoomOpen = function* watchRoomOpen({ room }) {
try {
if (opened) {
return;
}
opened = true;
const auth = yield select(state => state.login.isAuthenticated);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
yield put(messagesRequest({ ...room }));
if (room._id) {
RocketChat.readMessages(room.rid);
}
sub = yield RocketChat.subscribeRoom(room);
thread = yield fork(usersTyping, { rid: room.rid });
yield race({
open: take(types.ROOM.OPEN),
close: take(types.ROOM.CLOSE)
});
opened = false;
cancel(thread);
sub.stop();
yield put(editCancel());
yield put(replyCancel());
} catch (e) {
log('watchRoomOpen', e);
}
};
const watchuserTyping = function* watchuserTyping({ status }) {
const auth = yield select(state => state.login.isAuthenticated); const auth = yield select(state => state.login.isAuthenticated);
if (!auth) { if (!auth) {
yield take(types.LOGIN.SUCCESS); yield take(types.LOGIN.SUCCESS);
} }
const room = yield select(state => state.room);
if (!room) {
return;
}
try { try {
yield RocketChat.emitTyping(room.rid, status); yield RocketChat.emitTyping(rid, status);
if (status) { if (status) {
yield call(delay, 5000); yield call(delay, 5000);
yield RocketChat.emitTyping(room.rid, false); yield RocketChat.emitTyping(rid, false);
} }
} catch (e) { } catch (e) {
log('watchuserTyping', e); log('watchUserTyping', e);
} }
}; };
@ -147,9 +55,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
}; };
const root = function* root() { const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping); yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
yield takeEvery(types.ROOM.OPEN, watchRoomOpen);
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom); yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom); yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
}; };

View File

@ -16,7 +16,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, customEmojis: state.customEmojis,
room: state.room,
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,
@ -33,7 +32,7 @@ export default class MentionedMessagesView extends LoggedView {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
room: PropTypes.object navigation: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -42,6 +41,8 @@ export default class MentionedMessagesView extends LoggedView {
loading: false, loading: false,
messages: [] messages: []
}; };
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
} }
componentDidMount() { componentDidMount() {
@ -71,10 +72,9 @@ export default class MentionedMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getMessages( const result = await RocketChat.getMessages(
room.rid, this.rid,
room.t, this.t,
{ 'mentions._id': { $in: [user.id] } }, { 'mentions._id': { $in: [user.id] } },
messages.length messages.length
); );

View File

@ -21,7 +21,6 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
@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, customEmojis: state.customEmojis,
room: state.room,
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,
@ -38,7 +37,7 @@ export default class PinnedMessagesView extends LoggedView {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
room: PropTypes.object navigation: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -47,6 +46,8 @@ export default class PinnedMessagesView extends LoggedView {
loading: false, loading: false,
messages: [] messages: []
}; };
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
} }
componentDidMount() { componentDidMount() {
@ -114,8 +115,7 @@ export default class PinnedMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props; const result = await RocketChat.getMessages(this.rid, this.t, { pinned: true }, messages.length);
const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length);
if (result.success) { if (result.success) {
this.setState(prevState => ({ this.setState(prevState => ({
messages: [...prevState.messages, ...result.messages], messages: [...prevState.messages, ...result.messages],

View File

@ -32,8 +32,7 @@ const renderSeparator = () => <View style={styles.separator} />;
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
}, },
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
room: state.room
}), dispatch => ({ }), dispatch => ({
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t)) leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t))
})) }))
@ -50,25 +49,36 @@ export default class RoomActionsView extends LoggedView {
id: PropTypes.string, id: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
room: PropTypes.object,
leaveRoom: PropTypes.func leaveRoom: PropTypes.func
} }
constructor(props) { constructor(props) {
super('RoomActionsView', props); super('RoomActionsView', props);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
room: this.rooms[0] || props.room, room: this.rooms[0] || { rid: this.rid, t: this.t },
membersCount: 0, membersCount: 0,
member: {}, member: {},
joined: false, joined: this.rooms.length > 0,
canViewMembers: false canViewMembers: false
}; };
} }
async componentDidMount() { async componentDidMount() {
const { room } = this.state; const { room } = this.state;
if (!room._id) {
try {
const result = await RocketChat.getChannelInfo(room.rid);
if (result.success) {
this.setState({ room: { ...result.channel, rid: result.channel._id } });
}
} catch (error) {
console.log('RoomActionsView -> getChannelInfo -> error', error);
}
}
if (room && room.t !== 'd' && this.canViewMembers) { if (room && room.t !== 'd' && this.canViewMembers) {
try { try {
const counters = await RocketChat.getRoomCounters(room.rid, room.t); const counters = await RocketChat.getRoomCounters(room.rid, room.t);
@ -120,6 +130,7 @@ export default class RoomActionsView extends LoggedView {
} }
} }
// TODO: move to componentDidMount
get canAddUser() { get canAddUser() {
const { room, joined } = this.state; const { room, joined } = this.state;
const { rid, t } = room; const { rid, t } = room;
@ -139,6 +150,7 @@ export default class RoomActionsView extends LoggedView {
return false; return false;
} }
// TODO: move to componentDidMount
get canViewMembers() { get canViewMembers() {
const { room } = this.state; const { room } = this.state;
const { rid, t, broadcast } = room; const { rid, t, broadcast } = room;
@ -177,7 +189,8 @@ export default class RoomActionsView extends LoggedView {
icon: 'star', icon: 'star',
name: I18n.t('Room_Info'), name: I18n.t('Room_Info'),
route: 'RoomInfoView', route: 'RoomInfoView',
params: { rid }, // forward room only if room isn't joined
params: { rid, t, room: joined ? null : room },
testID: 'room-actions-info' testID: 'room-actions-info'
}], }],
renderItem: this.renderRoomInfo renderItem: this.renderRoomInfo
@ -203,18 +216,21 @@ export default class RoomActionsView extends LoggedView {
icon: 'file-generic', icon: 'file-generic',
name: I18n.t('Files'), name: I18n.t('Files'),
route: 'RoomFilesView', route: 'RoomFilesView',
params: { rid, t },
testID: 'room-actions-files' testID: 'room-actions-files'
}, },
{ {
icon: 'at', icon: 'at',
name: I18n.t('Mentions'), name: I18n.t('Mentions'),
route: 'MentionedMessagesView', route: 'MentionedMessagesView',
params: { rid, t },
testID: 'room-actions-mentioned' testID: 'room-actions-mentioned'
}, },
{ {
icon: 'star', icon: 'star',
name: I18n.t('Starred'), name: I18n.t('Starred'),
route: 'StarredMessagesView', route: 'StarredMessagesView',
params: { rid, t },
testID: 'room-actions-starred' testID: 'room-actions-starred'
}, },
{ {
@ -234,6 +250,7 @@ export default class RoomActionsView extends LoggedView {
icon: 'pin', icon: 'pin',
name: I18n.t('Pinned'), name: I18n.t('Pinned'),
route: 'PinnedMessagesView', route: 'PinnedMessagesView',
params: { rid, t },
testID: 'room-actions-pinned' testID: 'room-actions-pinned'
} }
], ],
@ -389,8 +406,8 @@ export default class RoomActionsView extends LoggedView {
? <Text style={styles.roomTitle}>{room.fname}</Text> ? <Text style={styles.roomTitle}>{room.fname}</Text>
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} />
<Text style={styles.roomTitle}>{room.name}</Text> <Text style={styles.roomTitle}>{room.prid ? room.fname : room.name}</Text>
</View> </View>
) )
} }

View File

@ -16,7 +16,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, customEmojis: state.customEmojis,
room: state.room,
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,
@ -33,7 +32,7 @@ export default class RoomFilesView extends LoggedView {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
room: PropTypes.object navigation: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -42,6 +41,8 @@ export default class RoomFilesView extends LoggedView {
loading: false, loading: false,
messages: [] messages: []
}; };
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
} }
componentDidMount() { componentDidMount() {
@ -70,8 +71,7 @@ export default class RoomFilesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props; const result = await RocketChat.getFiles(this.rid, this.t, messages.length);
const result = await RocketChat.getFiles(room.rid, room.t, messages.length);
if (result.success) { if (result.success) {
this.setState(prevState => ({ this.setState(prevState => ({
messages: [...prevState.messages, ...result.files], messages: [...prevState.messages, ...result.files],

View File

@ -26,8 +26,8 @@ const getRoomTitle = room => (room.t === 'd'
? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text> ? <Text testID='room-info-view-name' style={styles.roomTitle}>{room.fname}</Text>
: ( : (
<View style={styles.roomTitleRow}> <View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} key='room-info-type' /> <RoomTypeIcon type={room.prid ? 'discussion' : room.t} key='room-info-type' />
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text> <Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.prid ? room.fname : room.name}</Text>
</View> </View>
) )
); );
@ -40,8 +40,7 @@ const getRoomTitle = room => (room.t === 'd'
}, },
activeUsers: state.activeUsers, // TODO: remove it activeUsers: state.activeUsers, // TODO: remove it
Message_TimeFormat: state.settings.Message_TimeFormat, Message_TimeFormat: state.settings.Message_TimeFormat,
allRoles: state.roles, allRoles: state.roles
room: state.room
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomInfoView extends LoggedView { export default class RoomInfoView extends LoggedView {
@ -75,12 +74,13 @@ export default class RoomInfoView extends LoggedView {
constructor(props) { constructor(props) {
super('RoomInfoView', props); super('RoomInfoView', props);
const rid = props.navigation.getParam('rid'); const rid = props.navigation.getParam('rid');
const room = props.navigation.getParam('room');
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = { this.sub = {
unsubscribe: () => {} unsubscribe: () => {}
}; };
this.state = { this.state = {
room: this.rooms[0] || {}, room: this.rooms[0] || room || {},
roomUser: {}, roomUser: {},
roles: [] roles: []
}; };
@ -90,7 +90,7 @@ export default class RoomInfoView extends LoggedView {
safeAddListener(this.rooms, this.updateRoom); safeAddListener(this.rooms, this.updateRoom);
const { room } = this.state; const { room } = this.state;
const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid); const permissions = RocketChat.hasPermission([PERMISSION_EDIT_ROOM], room.rid);
if (permissions[PERMISSION_EDIT_ROOM]) { if (permissions[PERMISSION_EDIT_ROOM] && !room.prid) {
const { navigation } = this.props; const { navigation } = this.props;
navigation.setParams({ showEdit: true }); navigation.setParams({ showEdit: true });
} }

View File

@ -23,7 +23,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 : '',
room: state.room,
user: { user: {
id: state.login.user && state.login.user.id, id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
@ -191,10 +190,15 @@ export default class RoomMembersView extends LoggedView {
this.setState({ isLoading: true }); this.setState({ isLoading: true });
const { rid } = this.state; const { rid } = this.state;
const { navigation } = this.props; const { navigation } = this.props;
const membersResult = await RocketChat.getRoomMembers(rid, status); try {
const members = membersResult.records; const membersResult = await RocketChat.getRoomMembers(rid, status);
this.setState({ allUsers: status, members, isLoading: false }); const members = membersResult.records;
navigation.setParams({ allUsers: status }); this.setState({ allUsers: status, members, isLoading: false });
navigation.setParams({ allUsers: status });
} catch (error) {
console.log('TCL: fetchMembers -> error', error);
this.setState({ isLoading: false });
}
} }
updateRoom = () => { updateRoom = () => {

View File

@ -0,0 +1,107 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, ScrollView
} from 'react-native';
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
import { isIOS } from '../../../utils/deviceInfo';
import Icon from './Icon';
import { COLOR_TEXT_DESCRIPTION, HEADER_TITLE, COLOR_WHITE } from '../../../constants/colors';
const TITLE_SIZE = 16;
const styles = StyleSheet.create({
container: {
flex: 1,
height: '100%'
},
titleContainer: {
flex: 6,
flexDirection: 'row'
},
title: {
...sharedStyles.textSemibold,
color: HEADER_TITLE,
fontSize: TITLE_SIZE
},
scroll: {
alignItems: 'center'
},
typing: {
...sharedStyles.textRegular,
color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE,
fontSize: 12,
flex: 4
},
typingUsers: {
...sharedStyles.textSemibold
}
});
const Typing = React.memo(({ usersTyping }) => {
const users = usersTyping.map(item => item.username);
let usersText;
if (!users.length) {
return null;
} else if (users.length === 2) {
usersText = users.join(` ${ I18n.t('and') } `);
} else {
usersText = users.join(', ');
}
return (
<Text style={styles.typing} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text>
{ users.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
</Text>
);
});
Typing.propTypes = {
usersTyping: PropTypes.array
};
const Header = React.memo(({
prid, title, type, status, usersTyping, width, height
}) => {
const portrait = height > width;
let scale = 1;
if (!portrait) {
if (usersTyping.length > 0) {
scale = 0.8;
}
}
return (
<View style={styles.container}>
<View style={styles.titleContainer}>
<ScrollView
showsHorizontalScrollIndicator={false}
horizontal
bounces={false}
contentContainerStyle={styles.scroll}
>
<Icon type={prid ? 'discussion' : type} status={status} />
<Text style={[styles.title, { fontSize: TITLE_SIZE * scale }]} numberOfLines={1}>{title}</Text>
</ScrollView>
</View>
<Typing usersTyping={usersTyping} />
</View>
);
});
Header.propTypes = {
title: PropTypes.string.isRequired,
type: PropTypes.string.isRequired,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
prid: PropTypes.string,
status: PropTypes.string,
usersTyping: PropTypes.array
};
Header.defaultProps = {
usersTyping: []
};
export default Header;

View File

@ -17,7 +17,8 @@ const styles = StyleSheet.create({
color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE
}, },
status: { status: {
marginRight: 8 marginLeft: 4,
marginRight: 12
} }
}); });
@ -26,7 +27,14 @@ const Icon = React.memo(({ type, status }) => {
return <Status size={10} style={styles.status} status={status} />; return <Status size={10} style={styles.status} status={status} />;
} }
const icon = type === 'c' ? 'hashtag' : 'lock'; let icon;
if (type === 'discussion') {
icon = 'chat';
} else if (type === 'c') {
icon = 'hashtag';
} else {
icon = 'lock';
}
return ( return (
<CustomIcon <CustomIcon
name={icon} name={icon}

View File

@ -1,57 +1,20 @@
import React, { Component } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, ScrollView
} from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { responsive } from 'react-native-responsive-ui'; import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal'; import equal from 'deep-equal';
import I18n from '../../../i18n'; import database from '../../../lib/realm';
import sharedStyles from '../../Styles'; import Header from './Header';
import { isIOS } from '../../../utils/deviceInfo';
import { headerIconSize } from '../../../containers/HeaderButton';
import Icon from './Icon';
import { COLOR_TEXT_DESCRIPTION, HEADER_TITLE, COLOR_WHITE } from '../../../constants/colors';
const TITLE_SIZE = 16;
const styles = StyleSheet.create({
container: {
flex: 1,
height: '100%'
},
titleContainer: {
flex: 1,
flexDirection: 'row'
},
title: {
...sharedStyles.textSemibold,
color: HEADER_TITLE,
fontSize: TITLE_SIZE
},
scroll: {
alignItems: 'center'
},
typing: {
...sharedStyles.textRegular,
color: isIOS ? COLOR_TEXT_DESCRIPTION : COLOR_WHITE,
fontSize: 12,
marginBottom: 2
},
typingUsers: {
...sharedStyles.textSemibold
}
});
@responsive @responsive
@connect((state) => { @connect((state, ownProps) => {
let status = ''; let status = '';
let title = ''; const { rid, type } = ownProps;
const roomType = state.room.t; if (type === 'd') {
if (roomType === 'd') {
if (state.login.user && state.login.user.id) { if (state.login.user && state.login.user.id) {
const { id: loggedUserId } = state.login.user; const { id: loggedUserId } = state.login.user;
const userId = state.room.rid.replace(loggedUserId, '').trim(); const userId = rid.replace(loggedUserId, '').trim();
if (userId === loggedUserId) { if (userId === loggedUserId) {
status = state.login.user.status; // eslint-disable-line status = state.login.user.status; // eslint-disable-line
} else { } else {
@ -59,22 +22,9 @@ const styles = StyleSheet.create({
status = (user && user.status) || 'offline'; status = (user && user.status) || 'offline';
} }
} }
title = state.settings.UI_Use_Real_Name ? state.room.fname : state.room.name;
} else {
title = state.room.name;
}
let otherUsersTyping = [];
if (state.login.user && state.login.user.username) {
const { username } = state.login.user;
const { usersTyping } = state.room;
otherUsersTyping = usersTyping.filter(_username => _username !== username);
} }
return { return {
usersTyping: otherUsersTyping,
type: roomType,
title,
status status
}; };
}) })
@ -82,14 +32,25 @@ export default class RoomHeaderView extends Component {
static propTypes = { static propTypes = {
title: PropTypes.string, title: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
prid: PropTypes.string,
rid: PropTypes.string,
window: PropTypes.object, window: PropTypes.object,
usersTyping: PropTypes.array,
status: PropTypes.string status: PropTypes.string
}; };
shouldComponentUpdate(nextProps) { constructor(props) {
super(props);
this.usersTyping = database.memoryDatabase.objects('usersTyping').filtered('rid = $0', props.rid);
this.state = {
usersTyping: this.usersTyping.slice() || []
};
this.usersTyping.addListener(this.updateState);
}
shouldComponentUpdate(nextProps, nextState) {
const { usersTyping } = this.state;
const { const {
type, title, status, usersTyping, window type, title, status, window
} = this.props; } = this.props;
if (nextProps.type !== type) { if (nextProps.type !== type) {
return true; return true;
@ -106,7 +67,7 @@ export default class RoomHeaderView extends Component {
if (nextProps.window.height !== window.height) { if (nextProps.window.height !== window.height) {
return true; return true;
} }
if (!equal(nextProps.usersTyping, usersTyping)) { if (!equal(nextState.usersTyping, usersTyping)) {
return true; return true;
} }
return false; return false;
@ -121,53 +82,26 @@ export default class RoomHeaderView extends Component {
// } // }
// } // }
get typing() { updateState = () => {
const { usersTyping } = this.props; this.setState({ usersTyping: this.usersTyping.slice() });
let usersText;
if (!usersTyping.length) {
return null;
} else if (usersTyping.length === 2) {
usersText = usersTyping.join(` ${ I18n.t('and') } `);
} else {
usersText = usersTyping.join(', ');
}
return (
<Text style={styles.typing} numberOfLines={1}>
<Text style={styles.typingUsers}>{usersText} </Text>
{ usersTyping.length > 1 ? I18n.t('are_typing') : I18n.t('is_typing') }...
</Text>
);
} }
render() { render() {
const { usersTyping } = this.state;
const { const {
window, title, usersTyping, type, status window, title, type, status, prid
} = this.props; } = this.props;
const portrait = window.height > window.width;
const widthScrollView = window.width - 6.5 * headerIconSize;
let scale = 1;
if (!portrait) {
if (usersTyping.length > 0) {
scale = 0.8;
}
}
return ( return (
<View style={styles.container}> <Header
<View style={[styles.titleContainer, { width: widthScrollView }]}> prid={prid}
<ScrollView title={title}
showsHorizontalScrollIndicator={false} type={type}
horizontal status={status}
bounces={false} width={window.width}
contentContainerStyle={styles.scroll} height={window.height}
> usersTyping={usersTyping}
<Icon type={type} status={status} /> />
<Text style={[styles.title, { fontSize: TITLE_SIZE * scale }]} numberOfLines={1}>{title}</Text>
</ScrollView>
</View>
{this.typing}
</View>
); );
} }
} }

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, FlatList } from 'react-native'; import { ActivityIndicator, FlatList, InteractionManager } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { responsive } from 'react-native-responsive-ui';
import styles from './styles'; import styles from './styles';
import database, { safeAddListener } from '../../lib/realm'; import database, { safeAddListener } from '../../lib/realm';
@ -10,33 +9,35 @@ import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat'; import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log'; import log from '../../utils/log';
import EmptyRoom from './EmptyRoom'; import EmptyRoom from './EmptyRoom';
import ScrollBottomButton from './ScrollBottomButton'; // import ScrollBottomButton from './ScrollBottomButton';
import { isNotch } from '../../utils/deviceInfo';
@responsive
export class List extends React.Component { export class List extends React.Component {
static propTypes = { static propTypes = {
onEndReached: PropTypes.func, onEndReached: PropTypes.func,
renderFooter: PropTypes.func, renderFooter: PropTypes.func,
renderRow: PropTypes.func, renderRow: PropTypes.func,
room: PropTypes.object, rid: PropTypes.string,
t: PropTypes.string,
window: PropTypes.object window: PropTypes.object
}; };
constructor(props) { constructor(props) {
super(props); super(props);
console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`);
this.data = database this.data = database
.objects('messages') .objects('messages')
.filtered('rid = $0', props.room.rid) .filtered('rid = $0', props.rid)
.sorted('ts', true); .sorted('ts', true);
this.state = { this.state = {
loading: true, loading: true,
loadingMore: false, loadingMore: false,
end: false, end: false,
messages: this.data.slice(), messages: this.data.slice()
showScollToBottomButton: false // showScollToBottomButton: false
}; };
safeAddListener(this.data, this.updateState); safeAddListener(this.data, this.updateState);
console.timeEnd(`${ this.constructor.name } init`);
} }
// shouldComponentUpdate(nextProps, nextState) { // shouldComponentUpdate(nextProps, nextState) {
@ -53,14 +54,26 @@ export class List extends React.Component {
// || window.width !== nextProps.window.width; // || window.width !== nextProps.window.width;
// } // }
componentDidMount() {
console.timeEnd(`${ this.constructor.name } mount`);
}
componentWillUnmount() { componentWillUnmount() {
this.data.removeAllListeners(); this.data.removeAllListeners();
this.updateState.stop(); if (this.updateState && this.updateState.stop) {
this.updateState.stop();
}
if (this.interactionManager && this.interactionManager.cancel) {
this.interactionManager.cancel();
}
console.countReset(`${ this.constructor.name }.render calls`);
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
updateState = debounce(() => { updateState = debounce(() => {
this.setState({ messages: this.data.slice(), loading: false, loadingMore: false }); this.interactionManager = InteractionManager.runAfterInteractions(() => {
this.setState({ messages: this.data.slice(), loading: false, loadingMore: false });
});
}, 300); }, 300);
onEndReached = async() => { onEndReached = async() => {
@ -72,9 +85,9 @@ export class List extends React.Component {
} }
this.setState({ loadingMore: true }); this.setState({ loadingMore: true });
const { room } = this.props; const { rid, t } = this.props;
try { try {
const result = await RocketChat.loadMessagesForRoom({ rid: room.rid, t: room.t, latest: this.data[this.data.length - 1].ts }); const result = await RocketChat.loadMessagesForRoom({ rid, t, latest: this.data[this.data.length - 1].ts });
this.setState({ end: result.length < 50 }); this.setState({ end: result.length < 50 });
} catch (e) { } catch (e) {
this.setState({ loadingMore: false }); this.setState({ loadingMore: false });
@ -82,19 +95,19 @@ export class List extends React.Component {
} }
} }
scrollToBottom = () => { // scrollToBottom = () => {
requestAnimationFrame(() => { // requestAnimationFrame(() => {
this.list.scrollToOffset({ offset: isNotch ? -90 : -60 }); // this.list.scrollToOffset({ offset: isNotch ? -90 : -60 });
}); // });
} // }
handleScroll = (event) => { // handleScroll = (event) => {
if (event.nativeEvent.contentOffset.y > 0) { // if (event.nativeEvent.contentOffset.y > 0) {
this.setState({ showScollToBottomButton: true }); // this.setState({ showScollToBottomButton: true });
} else { // } else {
this.setState({ showScollToBottomButton: false }); // this.setState({ showScollToBottomButton: false });
} // }
} // }
renderFooter = () => { renderFooter = () => {
const { loadingMore, loading } = this.state; const { loadingMore, loading } = this.state;
@ -105,8 +118,9 @@ export class List extends React.Component {
} }
render() { render() {
const { renderRow, window } = this.props; console.count(`${ this.constructor.name }.render calls`);
const { showScollToBottomButton, messages } = this.state; const { renderRow } = this.props;
const { messages } = this.state;
return ( return (
<React.Fragment> <React.Fragment>
<EmptyRoom length={messages.length} /> <EmptyRoom length={messages.length} />
@ -119,21 +133,22 @@ export class List extends React.Component {
renderItem={({ item, index }) => renderRow(item, messages[index + 1])} renderItem={({ item, index }) => renderRow(item, messages[index + 1])}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}
style={styles.list} style={styles.list}
onScroll={this.handleScroll} // onScroll={this.handleScroll}
inverted inverted
removeClippedSubviews removeClippedSubviews
initialNumToRender={10} initialNumToRender={1}
onEndReached={this.onEndReached} onEndReached={this.onEndReached}
onEndReachedThreshold={0.5} onEndReachedThreshold={0.5}
maxToRenderPerBatch={20} maxToRenderPerBatch={5}
windowSize={21}
ListFooterComponent={this.renderFooter} ListFooterComponent={this.renderFooter}
{...scrollPersistTaps} {...scrollPersistTaps}
/> />
<ScrollBottomButton {/* <ScrollBottomButton
show={showScollToBottomButton} show={showScollToBottomButton}
onPress={this.scrollToBottom} onPress={this.scrollToBottom}
landscape={window.width > window.height} landscape={window.width > window.height}
/> /> */}
</React.Fragment> </React.Fragment>
); );
} }

View File

@ -1,16 +1,22 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { import {
Text, View, LayoutAnimation, ActivityIndicator Text, View, LayoutAnimation, InteractionManager
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { RectButton } from 'react-native-gesture-handler'; import { RectButton } from 'react-native-gesture-handler';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal'; import equal from 'deep-equal';
import moment from 'moment'; import moment from 'moment';
import 'react-native-console-time-polyfill';
import { openRoom as openRoomAction, closeRoom as closeRoomAction } from '../../actions/room'; import {
import { toggleReactionPicker as toggleReactionPickerAction, actionsShow as actionsShowAction } from '../../actions/messages'; toggleReactionPicker as toggleReactionPickerAction,
actionsShow as actionsShowAction,
messagesRequest as messagesRequestAction,
editCancel as editCancelAction,
replyCancel as replyCancelAction
} from '../../actions/messages';
import LoggedView from '../View'; import LoggedView from '../View';
import { List } from './List'; import { List } from './List';
import database, { safeAddListener } from '../../lib/realm'; import database, { safeAddListener } from '../../lib/realm';
@ -31,6 +37,7 @@ import RoomHeaderView from './Header';
import StatusBar from '../../containers/StatusBar'; import StatusBar from '../../containers/StatusBar';
import Separator from './Separator'; import Separator from './Separator';
import { COLOR_WHITE } from '../../constants/colors'; import { COLOR_WHITE } from '../../constants/colors';
import debounce from '../../utils/debounce';
@connect(state => ({ @connect(state => ({
user: { user: {
@ -41,29 +48,29 @@ import { COLOR_WHITE } from '../../constants/colors';
actionMessage: state.messages.actionMessage, actionMessage: state.messages.actionMessage,
showActions: state.messages.showActions, showActions: state.messages.showActions,
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
}), dispatch => ({ }), dispatch => ({
openRoom: room => dispatch(openRoomAction(room)), editCancel: () => dispatch(editCancelAction()),
replyCancel: () => dispatch(replyCancelAction()),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)), toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)), actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
closeRoom: () => dispatch(closeRoomAction()) messagesRequest: room => dispatch(messagesRequestAction(room))
})) }))
/** @extends React.Component */ /** @extends React.Component */
export default class RoomView extends LoggedView { export default class RoomView extends LoggedView {
static navigationOptions = ({ navigation }) => { static navigationOptions = ({ navigation }) => {
const rid = navigation.getParam('rid'); const rid = navigation.getParam('rid');
const prid = navigation.getParam('prid');
const title = navigation.getParam('name');
const t = navigation.getParam('t'); const t = navigation.getParam('t');
const f = navigation.getParam('f');
const toggleFav = navigation.getParam('toggleFav', () => {});
const starIcon = f ? 'Star-filled' : 'star';
return { return {
headerTitle: <RoomHeaderView />, headerTitle: <RoomHeaderView rid={rid} prid={prid} title={title} type={t} />,
headerRight: t === 'l' headerRight: t === 'l'
? null ? null
: ( : (
<CustomHeaderButtons> <CustomHeaderButtons>
<Item title='star' iconName={starIcon} onPress={toggleFav} testID='room-view-header-star' /> <Item title='more' iconName='menu' onPress={() => navigation.navigate('RoomActionsView', { rid, t })} testID='room-view-header-actions' />
<Item title='more' iconName='menu' onPress={() => navigation.navigate('RoomActionsView', { rid })} testID='room-view-header-actions' />
</CustomHeaderButtons> </CustomHeaderButtons>
) )
}; };
@ -71,7 +78,6 @@ export default class RoomView extends LoggedView {
static propTypes = { static propTypes = {
navigation: PropTypes.object, navigation: PropTypes.object,
openRoom: PropTypes.func.isRequired,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string.isRequired, id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired, username: PropTypes.string.isRequired,
@ -81,46 +87,57 @@ export default class RoomView extends LoggedView {
showErrorActions: PropTypes.bool, showErrorActions: PropTypes.bool,
actionMessage: PropTypes.object, actionMessage: PropTypes.object,
appState: PropTypes.string, appState: PropTypes.string,
useRealName: PropTypes.bool,
toggleReactionPicker: PropTypes.func.isRequired, toggleReactionPicker: PropTypes.func.isRequired,
actionsShow: PropTypes.func, actionsShow: PropTypes.func,
closeRoom: PropTypes.func messagesRequest: PropTypes.func,
editCancel: PropTypes.func,
replyCancel: PropTypes.func
}; };
constructor(props) { constructor(props) {
super('RoomView', props); super('RoomView', props);
console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = { this.state = {
loaded: false,
joined: this.rooms.length > 0, joined: this.rooms.length > 0,
room: {}, room: this.rooms[0] || { rid: this.rid, t: this.t },
lastOpen: null lastOpen: null
}; };
this.beginAnimating = false; this.beginAnimating = false;
this.onReactionPress = this.onReactionPress.bind(this); this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
setTimeout(() => { this.messagebox = React.createRef();
this.beginAnimating = true; console.timeEnd(`${ this.constructor.name } init`);
}, 300);
} }
componentDidMount() { componentDidMount() {
const { navigation } = this.props; this.didMountInteraction = InteractionManager.runAfterInteractions(async() => {
navigation.setParams({ toggleFav: this.toggleFav }); const { room } = this.state;
const { messagesRequest, navigation } = this.props;
messagesRequest(room);
if (this.rooms.length === 0 && this.rid) { // if room is joined
const { rid, name, t } = navigation.state.params; if (room._id) {
this.setState( navigation.setParams({ name: this.getRoomTitle(room), t: room.t });
{ room: { rid, name, t } }, this.sub = await RocketChat.subscribeRoom(room);
() => this.updateRoom() RocketChat.readMessages(room.rid);
); if (room.alert || room.unread || room.userMentions) {
} this.setLastOpen(room.ls);
safeAddListener(this.rooms, this.updateRoom); } else {
this.internalSetState({ loaded: true }); this.setLastOpen(null);
}
}
safeAddListener(this.rooms, this.updateRoom);
});
console.timeEnd(`${ this.constructor.name } mount`);
} }
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { const {
room, loaded, joined room, joined
} = this.state; } = this.state;
const { showActions, showErrorActions, appState } = this.props; const { showActions, showErrorActions, appState } = this.props;
@ -134,8 +151,6 @@ export default class RoomView extends LoggedView {
return true; return true;
} else if (room.archived !== nextState.room.archived) { } else if (room.archived !== nextState.room.archived) {
return true; return true;
} else if (loaded !== nextState.loaded) {
return true;
} else if (joined !== nextState.joined) { } else if (joined !== nextState.joined) {
return true; return true;
} else if (showActions !== nextProps.showActions) { } else if (showActions !== nextProps.showActions) {
@ -150,22 +165,46 @@ export default class RoomView extends LoggedView {
return false; return false;
} }
componentDidUpdate(prevProps, prevState) { componentDidUpdate(prevProps) {
const { room } = this.state; const { room } = this.state;
const { appState, navigation } = this.props; const { appState } = this.props;
if (prevState.room.f !== room.f) { if (appState === 'foreground' && appState !== prevProps.appState) {
navigation.setParams({ f: room.f }); this.onForegroundInteraction = InteractionManager.runAfterInteractions(() => {
} else if (appState === 'foreground' && appState !== prevProps.appState) { RocketChat.loadMissedMessages(room).catch(e => console.log(e));
RocketChat.loadMissedMessages(room).catch(e => console.log(e)); RocketChat.readMessages(room.rid).catch(e => console.log(e));
RocketChat.readMessages(room.rid).catch(e => console.log(e)); });
} }
} }
componentWillUnmount() { componentWillUnmount() {
const { closeRoom } = this.props; if (this.messagebox && this.messagebox.current && this.messagebox.current.text) {
closeRoom(); const { text } = this.messagebox.current;
database.write(() => {
const [room] = this.rooms;
room.draftMessage = text;
});
}
this.rooms.removeAllListeners(); this.rooms.removeAllListeners();
if (this.sub && this.sub.stop) {
this.sub.stop();
}
if (this.beginAnimatingTimeout) {
clearTimeout(this.beginAnimatingTimeout);
}
const { editCancel, replyCancel } = this.props;
editCancel();
replyCancel();
if (this.didMountInteraction && this.didMountInteraction.cancel) {
this.didMountInteraction.cancel();
}
if (this.onForegroundInteraction && this.onForegroundInteraction.cancel) {
this.onForegroundInteraction.cancel();
}
if (this.updateStateInteraction && this.updateStateInteraction.cancel) {
this.updateStateInteraction.cancel();
}
console.countReset(`${ this.constructor.name }.render calls`);
} }
onMessageLongPress = (message) => { onMessageLongPress = (message) => {
@ -186,6 +225,13 @@ export default class RoomView extends LoggedView {
} }
}; };
onDiscussionPress = debounce((item) => {
const { navigation } = this.props;
navigation.push('RoomView', {
rid: item.drid, prid: item.rid, name: item.msg, t: 'p'
});
}, 1000, true)
internalSetState = (...args) => { internalSetState = (...args) => {
if (isIOS && this.beginAnimating) { if (isIOS && this.beginAnimating) {
LayoutAnimation.easeInEaseOut(); LayoutAnimation.easeInEaseOut();
@ -193,42 +239,11 @@ export default class RoomView extends LoggedView {
this.setState(...args); this.setState(...args);
} }
// eslint-disable-next-line react/sort-comp
updateRoom = () => { updateRoom = () => {
const { openRoom } = this.props; this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
if (this.rooms.length > 0) {
const { room: prevRoom } = this.state;
const room = JSON.parse(JSON.stringify(this.rooms[0] || {})); const room = JSON.parse(JSON.stringify(this.rooms[0] || {}));
this.internalSetState({ room }); this.internalSetState({ room });
});
if (!prevRoom._id) {
openRoom({
...room
});
if (room.alert || room.unread || room.userMentions) {
this.setLastOpen(room.ls);
} else {
this.setLastOpen(null);
}
}
} else {
const { room } = this.state;
if (room.rid) {
openRoom(room);
this.internalSetState({ joined: false });
}
}
}
toggleFav = () => {
try {
const { room } = this.state;
const { rid, f } = room;
RocketChat.toggleFavorite(rid, !f);
} catch (e) {
log('toggleFavorite', e);
}
} }
sendMessage = (message) => { sendMessage = (message) => {
@ -238,6 +253,11 @@ export default class RoomView extends LoggedView {
}); });
}; };
getRoomTitle = (room) => {
const { useRealName } = this.props;
return ((room.prid || useRealName) && room.fname) || room.name;
}
setLastOpen = lastOpen => this.setState({ lastOpen }); setLastOpen = lastOpen => this.setState({ lastOpen });
joinRoom = async() => { joinRoom = async() => {
@ -295,7 +315,7 @@ export default class RoomView extends LoggedView {
&& moment(item.ts).isAfter(lastOpen) && moment(item.ts).isAfter(lastOpen)
&& moment(previousItem.ts).isBefore(lastOpen); && moment(previousItem.ts).isBefore(lastOpen);
if (!moment(item.ts).isSame(previousItem.ts, 'day')) { if (!moment(item.ts).isSame(previousItem.ts, 'day')) {
dateSeparator = previousItem.ts; dateSeparator = item.ts;
} }
} }
@ -314,6 +334,7 @@ export default class RoomView extends LoggedView {
_updatedAt={item._updatedAt} _updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress} onLongPress={this.onMessageLongPress}
onDiscussionPress={this.onDiscussionPress}
/> />
<Separator <Separator
ts={dateSeparator} ts={dateSeparator}
@ -336,12 +357,13 @@ export default class RoomView extends LoggedView {
_updatedAt={item._updatedAt} _updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress} onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress} onLongPress={this.onMessageLongPress}
onDiscussionPress={this.onDiscussionPress}
/> />
); );
} }
renderFooter = () => { renderFooter = () => {
const { joined } = this.state; const { joined, room } = this.state;
if (!joined) { if (!joined) {
return ( return (
@ -360,39 +382,34 @@ export default class RoomView extends LoggedView {
} }
if (this.isReadOnly()) { if (this.isReadOnly()) {
return ( return (
<View style={styles.readOnly} key='room-view-read-only'> <View style={styles.readOnly}>
<Text style={styles.previewMode}>{I18n.t('This_room_is_read_only')}</Text> <Text style={styles.previewMode}>{I18n.t('This_room_is_read_only')}</Text>
</View> </View>
); );
} }
if (this.isBlocked()) { if (this.isBlocked()) {
return ( return (
<View style={styles.readOnly} key='room-view-block'> <View style={styles.readOnly}>
<Text style={styles.previewMode}>{I18n.t('This_room_is_blocked')}</Text> <Text style={styles.previewMode}>{I18n.t('This_room_is_blocked')}</Text>
</View> </View>
); );
} }
return <MessageBox key='room-view-messagebox' onSubmit={this.sendMessage} rid={this.rid} />; return <MessageBox ref={this.messagebox} onSubmit={this.sendMessage} rid={this.rid} roomType={room.t} />;
}; };
renderList = () => { renderList = () => {
const { loaded, room } = this.state; const { room } = this.state;
if (!loaded || !room.rid) { const { rid, t } = room;
return <ActivityIndicator style={styles.loading} />;
}
return ( return (
[ <React.Fragment>
<List <List rid={rid} t={t} renderRow={this.renderItem} />
key='room-view-messages' {this.renderFooter()}
room={room} </React.Fragment>
renderRow={this.renderItem}
/>,
this.renderFooter()
]
); );
} }
render() { render() {
console.count(`${ this.constructor.name }.render calls`);
const { room } = this.state; const { room } = this.state;
const { user, showActions, showErrorActions } = this.props; const { user, showActions, showErrorActions } = this.props;

View File

@ -128,6 +128,7 @@ export default class RoomsListView extends LoggedView {
chats: [], chats: [],
unread: [], unread: [],
favorites: [], favorites: [],
discussions: [],
channels: [], channels: [],
privateGroup: [], privateGroup: [],
direct: [], direct: [],
@ -188,8 +189,11 @@ export default class RoomsListView extends LoggedView {
} }
if (groupByType) { if (groupByType) {
const { const {
channels, privateGroup, direct, livechat dicussions, channels, privateGroup, direct, livechat
} = this.state; } = this.state;
if (!isEqual(nextState.dicussions, dicussions)) {
return true;
}
if (!isEqual(nextState.channels, channels)) { if (!isEqual(nextState.channels, channels)) {
return true; return true;
} }
@ -239,6 +243,7 @@ export default class RoomsListView extends LoggedView {
this.removeListener(this.data); this.removeListener(this.data);
this.removeListener(this.unread); this.removeListener(this.unread);
this.removeListener(this.favorites); this.removeListener(this.favorites);
this.removeListener(this.discussions);
this.removeListener(this.channels); this.removeListener(this.channels);
this.removeListener(this.privateGroup); this.removeListener(this.privateGroup);
this.removeListener(this.direct); this.removeListener(this.direct);
@ -268,6 +273,7 @@ export default class RoomsListView extends LoggedView {
let chats = []; let chats = [];
let unread = []; let unread = [];
let favorites = []; let favorites = [];
let discussions = [];
let channels = []; let channels = [];
let privateGroup = []; let privateGroup = [];
let direct = []; let direct = [];
@ -291,22 +297,27 @@ export default class RoomsListView extends LoggedView {
} }
// type // type
if (groupByType) { if (groupByType) {
// discussions
this.discussions = this.data.filtered('prid != null');
discussions = this.removeRealmInstance(this.discussions);
// channels // channels
this.channels = this.data.filtered('t == $0', 'c'); this.channels = this.data.filtered('t == $0 AND prid == null', 'c');
channels = this.removeRealmInstance(this.channels); channels = this.removeRealmInstance(this.channels);
// private // private
this.privateGroup = this.data.filtered('t == $0', 'p'); this.privateGroup = this.data.filtered('t == $0 AND prid == null', 'p');
privateGroup = this.removeRealmInstance(this.privateGroup); privateGroup = this.removeRealmInstance(this.privateGroup);
// direct // direct
this.direct = this.data.filtered('t == $0', 'd'); this.direct = this.data.filtered('t == $0 AND prid == null', 'd');
direct = this.removeRealmInstance(this.direct); direct = this.removeRealmInstance(this.direct);
// livechat // livechat
this.livechat = this.data.filtered('t == $0', 'l'); this.livechat = this.data.filtered('t == $0 AND prid == null', 'l');
livechat = this.removeRealmInstance(this.livechat); livechat = this.removeRealmInstance(this.livechat);
safeAddListener(this.discussions, debounce(() => this.internalSetState({ discussions: this.removeRealmInstance(this.discussions) }), 300));
safeAddListener(this.channels, debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300)); safeAddListener(this.channels, debounce(() => this.internalSetState({ channels: this.removeRealmInstance(this.channels) }), 300));
safeAddListener(this.privateGroup, debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300)); safeAddListener(this.privateGroup, debounce(() => this.internalSetState({ privateGroup: this.removeRealmInstance(this.privateGroup) }), 300));
safeAddListener(this.direct, debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300)); safeAddListener(this.direct, debounce(() => this.internalSetState({ direct: this.removeRealmInstance(this.direct) }), 300));
@ -322,6 +333,7 @@ export default class RoomsListView extends LoggedView {
chats = this.removeRealmInstance(this.chats); chats = this.removeRealmInstance(this.chats);
safeAddListener(this.chats, debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300)); safeAddListener(this.chats, debounce(() => this.internalSetState({ chats: this.removeRealmInstance(this.chats) }), 300));
this.removeListener(this.discussions);
this.removeListener(this.channels); this.removeListener(this.channels);
this.removeListener(this.privateGroup); this.removeListener(this.privateGroup);
this.removeListener(this.direct); this.removeListener(this.direct);
@ -330,7 +342,7 @@ export default class RoomsListView extends LoggedView {
// setState // setState
this.internalSetState({ this.internalSetState({
chats, unread, favorites, channels, privateGroup, direct, livechat, loading: false chats, unread, favorites, discussions, channels, privateGroup, direct, livechat, loading: false
}); });
} }
} }
@ -387,16 +399,22 @@ export default class RoomsListView extends LoggedView {
}); });
} }
goRoom = ({ rid, name, t }) => { getRoomTitle = (item) => {
const { useRealName } = this.props;
return ((item.prid || useRealName) && item.fname) || item.name;
}
goRoom = (item) => {
this.cancelSearchingAndroid(); this.cancelSearchingAndroid();
const { navigation } = this.props; const { navigation } = this.props;
navigation.navigate('RoomView', { rid, name, t }); navigation.navigate('RoomView', {
rid: item.rid, name: this.getRoomTitle(item), t: item.t, prid: item.prid
});
} }
_onPressItem = async(item = {}) => { _onPressItem = async(item = {}) => {
if (!item.search) { if (!item.search) {
const { rid, name, t } = item; return this.goRoom(item);
return this.goRoom({ rid, name, t });
} }
if (item.t === 'd') { if (item.t === 'd') {
// if user is using the search we need first to join/create room // if user is using the search we need first to join/create room
@ -410,8 +428,7 @@ export default class RoomsListView extends LoggedView {
log('RoomsListView._onPressItem', e); log('RoomsListView._onPressItem', e);
} }
} else { } else {
const { rid, name, t } = item; return this.goRoom(item);
return this.goRoom({ rid, name, t });
} }
} }
@ -471,7 +488,7 @@ export default class RoomsListView extends LoggedView {
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { const {
useRealName, userId, baseUrl, StoreLastMessage userId, baseUrl, StoreLastMessage
} = this.props; } = this.props;
const id = item.rid.replace(userId, '').trim(); const id = item.rid.replace(userId, '').trim();
@ -482,12 +499,13 @@ export default class RoomsListView extends LoggedView {
userMentions={item.userMentions} userMentions={item.userMentions}
favorite={item.f} favorite={item.f}
lastMessage={item.lastMessage} lastMessage={item.lastMessage}
name={(useRealName && item.fname) || item.name} name={this.getRoomTitle(item)}
_updatedAt={item.roomUpdatedAt} _updatedAt={item.roomUpdatedAt}
key={item._id} key={item._id}
id={id} id={id}
type={item.t} type={item.t}
baseUrl={baseUrl} baseUrl={baseUrl}
prid={item.prid}
showLastMessage={StoreLastMessage} showLastMessage={StoreLastMessage}
onPress={() => this._onPressItem(item)} onPress={() => this._onPressItem(item)}
testID={`rooms-list-view-item-${ item.name }`} testID={`rooms-list-view-item-${ item.name }`}
@ -509,7 +527,7 @@ export default class RoomsListView extends LoggedView {
return null; return null;
} else if (header === 'Favorites' && !showFavorites) { } else if (header === 'Favorites' && !showFavorites) {
return null; return null;
} else if (['Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !groupByType) { } else if (['Discussions', 'Channels', 'Direct_Messages', 'Private_Groups', 'Livechat'].includes(header) && !groupByType) {
return null; return null;
} else if (header === 'Chats' && groupByType) { } else if (header === 'Chats' && groupByType) {
return null; return null;
@ -537,7 +555,7 @@ export default class RoomsListView extends LoggedView {
renderList = () => { renderList = () => {
const { const {
search, chats, unread, favorites, channels, direct, privateGroup, livechat search, chats, unread, favorites, discussions, channels, direct, privateGroup, livechat
} = this.state; } = this.state;
if (search.length > 0) { if (search.length > 0) {
@ -562,6 +580,7 @@ export default class RoomsListView extends LoggedView {
<View style={styles.container}> <View style={styles.container}>
{this.renderSection(unread, 'Unread')} {this.renderSection(unread, 'Unread')}
{this.renderSection(favorites, 'Favorites')} {this.renderSection(favorites, 'Favorites')}
{this.renderSection(discussions, 'Discussions')}
{this.renderSection(channels, 'Channels')} {this.renderSection(channels, 'Channels')}
{this.renderSection(direct, 'Direct_Messages')} {this.renderSection(direct, 'Direct_Messages')}
{this.renderSection(privateGroup, 'Private_Groups')} {this.renderSection(privateGroup, 'Private_Groups')}
@ -592,11 +611,10 @@ export default class RoomsListView extends LoggedView {
renderItem={this.renderItem} renderItem={this.renderItem}
ListHeaderComponent={this.renderListHeader} ListHeaderComponent={this.renderListHeader}
getItemLayout={getItemLayout} getItemLayout={getItemLayout}
enableEmptySections
removeClippedSubviews removeClippedSubviews
keyboardShouldPersistTaps='always' keyboardShouldPersistTaps='always'
initialNumToRender={12} initialNumToRender={9}
windowSize={7} windowSize={9}
/> />
); );
} }

View File

@ -46,6 +46,7 @@ export default class SearchMessagesView extends LoggedView {
messages: [], messages: [],
searchText: '' searchText: ''
}; };
this.rid = props.navigation.getParam('rid');
} }
componentDidMount() { componentDidMount() {
@ -72,12 +73,10 @@ export default class SearchMessagesView extends LoggedView {
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
search = debounce(async(searchText) => { search = debounce(async(searchText) => {
const { navigation } = this.props;
const rid = navigation.getParam('rid');
this.setState({ searchText, loading: true, messages: [] }); this.setState({ searchText, loading: true, messages: [] });
try { try {
const result = await RocketChat.searchMessages(rid, searchText); const result = await RocketChat.searchMessages(this.rid, searchText);
if (result.success) { if (result.success) {
this.setState({ this.setState({
messages: result.messages || [], messages: result.messages || [],

View File

@ -164,7 +164,7 @@ export default class Sidebar extends Component {
<React.Fragment> <React.Fragment>
<SidebarItem <SidebarItem
text={I18n.t('Chats')} text={I18n.t('Chats')}
left={<CustomIcon name='chat' size={20} color={COLOR_TEXT} />} left={<CustomIcon name='message' size={20} color={COLOR_TEXT} />}
onPress={() => this.sidebarNavigate('RoomsListView')} onPress={() => this.sidebarNavigate('RoomsListView')}
testID='sidebar-chats' testID='sidebar-chats'
current={activeItemKey === 'ChatsStack'} current={activeItemKey === 'ChatsStack'}

View File

@ -21,7 +21,6 @@ const options = [I18n.t('Unstar'), I18n.t('Cancel')];
@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, customEmojis: state.customEmojis,
room: state.room,
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,
@ -38,7 +37,7 @@ export default class StarredMessagesView extends LoggedView {
user: PropTypes.object, user: PropTypes.object,
baseUrl: PropTypes.string, baseUrl: PropTypes.string,
customEmojis: PropTypes.object, customEmojis: PropTypes.object,
room: PropTypes.object navigation: PropTypes.object
} }
constructor(props) { constructor(props) {
@ -47,6 +46,8 @@ export default class StarredMessagesView extends LoggedView {
loading: false, loading: false,
messages: [] messages: []
}; };
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
} }
componentDidMount() { componentDidMount() {
@ -115,10 +116,9 @@ export default class StarredMessagesView extends LoggedView {
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const { room } = this.props;
const result = await RocketChat.getMessages( const result = await RocketChat.getMessages(
room.rid, this.rid,
room.t, this.t,
{ 'starred._id': { $in: [user.id] } }, { 'starred._id': { $in: [user.id] } },
messages.length messages.length
); );

View File

@ -36,10 +36,6 @@ describe('Room screen', () => {
// Render - Header // Render - Header
describe('Header', async() => { describe('Header', async() => {
it('should have star button', async() => {
await expect(element(by.id('room-view-header-star'))).toBeVisible();
});
it('should have actions button ', async() => { it('should have actions button ', async() => {
await expect(element(by.id('room-view-header-actions'))).toBeVisible(); await expect(element(by.id('room-view-header-actions'))).toBeVisible();
}); });

View File

@ -31,8 +31,8 @@
7A32C246206D791D001C80E9 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A32C20F206D791D001C80E9 /* Fabric.framework */; }; 7A32C246206D791D001C80E9 /* Fabric.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A32C20F206D791D001C80E9 /* Fabric.framework */; };
7A32C247206D791D001C80E9 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A32C245206D791D001C80E9 /* Crashlytics.framework */; }; 7A32C247206D791D001C80E9 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A32C245206D791D001C80E9 /* Crashlytics.framework */; };
7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */; }; 7A430E4F20238C46008F55BC /* libRCTCustomInputController.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A430E1E20238C02008F55BC /* libRCTCustomInputController.a */; };
7A55F1C52236D541005109A0 /* custom.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7A55F1C42236D541005109A0 /* custom.ttf */; };
7A8DEB5A20ED0BEC00C5DCE4 /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A8DEB5220ED0BDE00C5DCE4 /* libRNNotifications.a */; }; 7A8DEB5A20ED0BEC00C5DCE4 /* libRNNotifications.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A8DEB5220ED0BDE00C5DCE4 /* libRNNotifications.a */; };
7A9B5BCF221F32FA00478E23 /* custom.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7A9B5BCE221F32F400478E23 /* custom.ttf */; };
7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; }; 7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; };
7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AFB804C205AE63100D004E7 /* libRCTToast.a */; }; 7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AFB804C205AE63100D004E7 /* libRCTToast.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
@ -257,6 +257,13 @@
remoteGlobalIDString = 39DF4FE71E00394E00F5B4B2; remoteGlobalIDString = 39DF4FE71E00394E00F5B4B2;
remoteInfo = RCTCustomInputController; remoteInfo = RCTCustomInputController;
}; };
7A55F1B92236D50F005109A0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B1A58A7ACB0E4453A44AEC38 /* RNGestureHandler.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = B5C32A36220C603B000FFB8D;
remoteInfo = "RNGestureHandler-tvOS";
};
7A770EC120BECDC7001AD51A /* PBXContainerItemProxy */ = { 7A770EC120BECDC7001AD51A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = 1845C223DA364898A8400573 /* FastImage.xcodeproj */; containerPortal = 1845C223DA364898A8400573 /* FastImage.xcodeproj */;
@ -494,8 +501,8 @@
7A32C20F206D791D001C80E9 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Fabric.framework; path = "../../../../Downloads/com.crashlytics.ios-manual/Fabric.framework"; sourceTree = "<group>"; }; 7A32C20F206D791D001C80E9 /* Fabric.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Fabric.framework; path = "../../../../Downloads/com.crashlytics.ios-manual/Fabric.framework"; sourceTree = "<group>"; };
7A32C245206D791D001C80E9 /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crashlytics.framework; path = "../../../../Downloads/com.crashlytics.ios-manual/Crashlytics.framework"; sourceTree = "<group>"; }; 7A32C245206D791D001C80E9 /* Crashlytics.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Crashlytics.framework; path = "../../../../Downloads/com.crashlytics.ios-manual/Crashlytics.framework"; sourceTree = "<group>"; };
7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCustomInputController.xcodeproj; path = "../node_modules/react-native-keyboard-input/lib/ios/RCTCustomInputController.xcodeproj"; sourceTree = "<group>"; }; 7A430E1620238C01008F55BC /* RCTCustomInputController.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTCustomInputController.xcodeproj; path = "../node_modules/react-native-keyboard-input/lib/ios/RCTCustomInputController.xcodeproj"; sourceTree = "<group>"; };
7A55F1C42236D541005109A0 /* custom.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = custom.ttf; sourceTree = "<group>"; };
7A8DEB1B20ED0BDE00C5DCE4 /* RNNotifications.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNNotifications.xcodeproj; path = "../node_modules/react-native-notifications/RNNotifications/RNNotifications.xcodeproj"; sourceTree = "<group>"; }; 7A8DEB1B20ED0BDE00C5DCE4 /* RNNotifications.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RNNotifications.xcodeproj; path = "../node_modules/react-native-notifications/RNNotifications/RNNotifications.xcodeproj"; sourceTree = "<group>"; };
7A9B5BCE221F32F400478E23 /* custom.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = custom.ttf; sourceTree = "<group>"; };
7ACD4853222860DE00442C55 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; }; 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = JavaScriptCore.framework; path = System/Library/Frameworks/JavaScriptCore.framework; sourceTree = SDKROOT; };
7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTToast.xcodeproj; path = "../node_modules/@remobile/react-native-toast/ios/RCTToast.xcodeproj"; sourceTree = "<group>"; }; 7AFB8035205AE63000D004E7 /* RCTToast.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTToast.xcodeproj; path = "../node_modules/@remobile/react-native-toast/ios/RCTToast.xcodeproj"; sourceTree = "<group>"; };
832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; }; 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = "../node_modules/react-native/Libraries/Text/RCTText.xcodeproj"; sourceTree = "<group>"; };
@ -847,7 +854,7 @@
AF5E16F0398347E6A80C8CBE /* Resources */ = { AF5E16F0398347E6A80C8CBE /* Resources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7A9B5BCE221F32F400478E23 /* custom.ttf */, 7A55F1C42236D541005109A0 /* custom.ttf */,
); );
name = Resources; name = Resources;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1297,6 +1304,13 @@
remoteRef = 7A430E1D20238C02008F55BC /* PBXContainerItemProxy */; remoteRef = 7A430E1D20238C02008F55BC /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR; sourceTree = BUILT_PRODUCTS_DIR;
}; };
7A55F1BA2236D50F005109A0 /* libRNGestureHandler-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRNGestureHandler-tvOS.a";
remoteRef = 7A55F1B92236D50F005109A0 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7A770EC220BECDC7001AD51A /* libFastImage.a */ = { 7A770EC220BECDC7001AD51A /* libFastImage.a */ = {
isa = PBXReferenceProxy; isa = PBXReferenceProxy;
fileType = archive.ar; fileType = archive.ar;
@ -1360,6 +1374,13 @@
remoteRef = 7AA7B71B2229AE520039764A /* PBXContainerItemProxy */; remoteRef = 7AA7B71B2229AE520039764A /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR; sourceTree = BUILT_PRODUCTS_DIR;
}; };
7A9B5BC9221F2D0900478E23 /* libRNVectorIcons-tvOS.a */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
path = "libRNVectorIcons-tvOS.a";
remoteRef = 7A9B5BC8221F2D0900478E23 /* PBXContainerItemProxy */;
sourceTree = BUILT_PRODUCTS_DIR;
};
7ACD4880222860DE00442C55 /* libjsi.a */ = { 7ACD4880222860DE00442C55 /* libjsi.a */ = {
isa = PBXReferenceProxy; isa = PBXReferenceProxy;
fileType = archive.ar; fileType = archive.ar;
@ -1486,8 +1507,8 @@
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
7A9B5BCF221F32FA00478E23 /* custom.ttf in Resources */,
7A309C9C20724870000C6B13 /* Fabric.sh in Resources */, 7A309C9C20724870000C6B13 /* Fabric.sh in Resources */,
7A55F1C52236D541005109A0 /* custom.ttf in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;

Binary file not shown.

View File

@ -35,6 +35,7 @@
"react-native": "0.58.6", "react-native": "0.58.6",
"react-native-action-sheet": "^2.1.0", "react-native-action-sheet": "^2.1.0",
"react-native-audio": "^4.3.0", "react-native-audio": "^4.3.0",
"react-native-console-time-polyfill": "^1.2.1",
"react-native-device-info": "^0.25.1", "react-native-device-info": "^0.25.1",
"react-native-dialog": "^5.5.0", "react-native-dialog": "^5.5.0",
"react-native-fabric": "github:corymsmith/react-native-fabric#523a4edab3b2bf55ea9eeea2cf0dde82c5c29dd4", "react-native-fabric": "github:corymsmith/react-native-fabric#523a4edab3b2bf55ea9eeea2cf0dde82c5c29dd4",
@ -52,6 +53,7 @@
"react-native-optimized-flatlist": "^1.0.4", "react-native-optimized-flatlist": "^1.0.4",
"react-native-orientation-locker": "^1.1.3", "react-native-orientation-locker": "^1.1.3",
"react-native-picker-select": "^5.2.3", "react-native-picker-select": "^5.2.3",
"react-native-platform-touchable": "^1.1.1",
"react-native-responsive-ui": "^1.1.1", "react-native-responsive-ui": "^1.1.1",
"react-native-safari-view": "^2.1.0", "react-native-safari-view": "^2.1.0",
"react-native-screens": "^1.0.0-alpha.22", "react-native-screens": "^1.0.0-alpha.22",

View File

@ -1,5 +1,6 @@
import React from 'react'; import React from 'react';
import { ScrollView, StyleSheet } from 'react-native'; import { ScrollView, StyleSheet } from 'react-native';
// import moment from 'moment';
import MessageComponent from '../../app/containers/message/Message'; import MessageComponent from '../../app/containers/message/Message';
import StoriesSeparator from './StoriesSeparator'; import StoriesSeparator from './StoriesSeparator';
@ -359,6 +360,64 @@ export default (
<Separator title='Broadcast' /> <Separator title='Broadcast' />
<Message msg='Broadcasted message' broadcast replyBroadcast={() => alert('broadcast!')} /> <Message msg='Broadcasted message' broadcast replyBroadcast={() => alert('broadcast!')} />
<Separator title='Discussion' />
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={null}
dlm={null}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1}
dlm={date}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={10}
dlm={date}
msg='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={date}
msg='This is a discussion'
/>
{/* <Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(1, 'hour')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(1, 'day')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(5, 'day')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(30, 'day')}
msg='This is a discussion'
/> */}
<Separator title='Archived' /> <Separator title='Archived' />
<Message msg='This message is inside an archived room' archived /> <Message msg='This message is inside an archived room' archived />

View File

@ -0,0 +1,63 @@
import React from 'react';
import { ScrollView, View, StyleSheet } from 'react-native';
import { HeaderBackButton } from 'react-navigation';
import HeaderComponent from '../../app/views/RoomView/Header/Header';
import { CustomHeaderButtons, Item } from '../../app/containers/HeaderButton';
import { COLOR_SEPARATOR, HEADER_BACKGROUND } from '../../app/constants/colors';
import StoriesSeparator from './StoriesSeparator';
import { isIOS } from '../../app/utils/deviceInfo';
const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
height: isIOS ? 44 : 56,
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: COLOR_SEPARATOR,
marginVertical: 6,
backgroundColor: HEADER_BACKGROUND
}
});
const Header = props => (
<View style={styles.container}>
<HeaderBackButton />
<HeaderComponent
title='test'
type='d'
width={375}
height={480}
{...props}
/>
<CustomHeaderButtons>
<Item title='more' iconName='menu' />
</CustomHeaderButtons>
</View>
);
export default (
<ScrollView>
<StoriesSeparator title='Basic' />
<Header />
<StoriesSeparator title='Types' />
<Header type='d' />
<Header type='c' />
<Header type='p' />
<Header type='discussion' />
<StoriesSeparator title='Typing' />
<Header usersTyping={[{ username: 'diego.mello' }]} />
<Header usersTyping={[{ username: 'diego.mello' }, { username: 'rocket.cat' }]} />
<Header usersTyping={[{ username: 'diego.mello' }, { username: 'rocket.cat' }, { username: 'detoxrn' }]} />
<StoriesSeparator title='Title scroll' />
<Header title='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.' />
<Header
title='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
usersTyping={[{ username: 'diego.mello' }, { username: 'rocket.cat' }, { username: 'detoxrn' }]}
/>
</ScrollView>
);

View File

@ -7,6 +7,7 @@ import { storiesOf } from '@storybook/react-native';
import RoomItem from './RoomItem'; import RoomItem from './RoomItem';
import Avatar from './Avatar'; import Avatar from './Avatar';
import Message from './Message'; import Message from './Message';
// import RoomViewHeader from './RoomViewHeader';
const reducers = combineReducers({ const reducers = combineReducers({
settings: () => ({}), settings: () => ({}),
@ -27,3 +28,6 @@ storiesOf('RoomItem', module)
.add('list', () => RoomItem); .add('list', () => RoomItem);
storiesOf('Message', module) storiesOf('Message', module)
.add('list', () => Message); .add('list', () => Message);
// FIXME: I couldn't make these pass on jest :(
// storiesOf('RoomViewHeader', module)
// .add('list', () => RoomViewHeader);

View File

@ -10102,6 +10102,11 @@ react-native-audio@^4.3.0:
resolved "https://registry.yarnpkg.com/react-native-audio/-/react-native-audio-4.3.0.tgz#fae22b81f6a4dda706fd4837d0c6a89c66cf2e7e" resolved "https://registry.yarnpkg.com/react-native-audio/-/react-native-audio-4.3.0.tgz#fae22b81f6a4dda706fd4837d0c6a89c66cf2e7e"
integrity sha512-QQYq28eSJy+y/Ukvry0AkbwMVELAj+LcEwCVRH+7sKLqlnoBBxGd4ilhgJHjwOiC70192LueGbjXJjPPEwW3iA== integrity sha512-QQYq28eSJy+y/Ukvry0AkbwMVELAj+LcEwCVRH+7sKLqlnoBBxGd4ilhgJHjwOiC70192LueGbjXJjPPEwW3iA==
react-native-console-time-polyfill@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/react-native-console-time-polyfill/-/react-native-console-time-polyfill-1.2.1.tgz#3bf9a1d1d1ce3a05325fe1f2e5c4e5a1c25d910f"
integrity sha512-NKWZ1a/kzMOjPYAcCKEws2FAUnB9Eg70rbEhvdWU4Ure0H2v/ffqy47qooCZOWkrlAvlIR/sg1Apml4dzuE54A==
react-native-device-info@^0.25.1: react-native-device-info@^0.25.1:
version "0.25.1" version "0.25.1"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.25.1.tgz#bde3be9fe0e06d0c07ab5837e4fc1af90f66696b" resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.25.1.tgz#bde3be9fe0e06d0c07ab5837e4fc1af90f66696b"