[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',
'CLOSE_SEARCH_HEADER'
]);
export const ROOM = createRequestTypes('ROOM', [
'ADD_USER_TYPING',
'REMOVE_USER_TYPING',
'SOMEONE_TYPING',
'OPEN',
'CLOSE',
'LEAVE',
'ERASE',
'USER_TYPING',
'MESSAGE_RECEIVED'
]);
export const ROOM = createRequestTypes('ROOM', ['LEAVE', 'ERASE', 'USER_TYPING']);
export const APP = createRequestTypes('APP', ['START', 'READY', 'INIT']);
export const MESSAGES = createRequestTypes('MESSAGES', [
...defaultTypes,

View File

@ -1,40 +1,5 @@
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) {
return {
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 {
type: types.ROOM.USER_TYPING,
rid,
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 sharedStyles from '../views/Styles';
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';
const styles = StyleSheet.create({
@ -117,7 +117,7 @@ class ConnectionBadge extends Component {
if (connecting) {
return (
<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>
</Animated.View>
);

View File

@ -48,25 +48,7 @@ const imagePickerConfig = {
cropperCancelText: I18n.t('Cancel')
};
@connect(state => ({
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 {
class MessageBox extends Component {
static propTypes = {
rid: 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);
if (room.draftMessage && room.draftMessage !== '') {
this.setInput(room.draftMessage);
this.setShowSend(true);
}
}
@ -120,6 +103,9 @@ export default class MessageBox extends Component {
const { message, replyMessage } = this.props;
if (message !== nextProps.message && nextProps.message.msg) {
this.setInput(nextProps.message.msg);
if (this.text) {
this.setShowSend(true);
}
this.focus();
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
this.focus();
@ -165,14 +151,6 @@ export default class MessageBox extends Component {
return false;
}
componentWillUnmount() {
const { rid } = this.props;
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
database.write(() => {
room.draftMessage = this.text;
});
}
onChangeText = (text) => {
const isTextEmpty = text.length === 0;
this.setShowSend(!isTextEmpty);
@ -461,13 +439,13 @@ export default class MessageBox extends Component {
}
handleTyping = (isTyping) => {
const { typing } = this.props;
const { typing, rid } = this.props;
if (!isTyping) {
if (this.typingTimeout) {
clearTimeout(this.typingTimeout);
this.typingTimeout = false;
}
typing(false);
typing(rid, false);
return;
}
@ -476,7 +454,7 @@ export default class MessageBox extends Component {
}
this.typingTimeout = setTimeout(() => {
typing(true);
typing(rid, true);
this.typingTimeout = false;
}, 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 { Image, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { CustomIcon } from '../lib/Icons';
import { COLOR_TEXT_DESCRIPTION } from '../constants/colors';
const styles = StyleSheet.create({
style: {
marginRight: 7,
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;
}
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') {
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 PropTypes from 'prop-types';
import FastImage from 'react-native-fast-image';
import { RectButton } from 'react-native-gesture-handler';
import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import PhotoModal from './PhotoModal';
import Markdown from './Markdown';
import styles from './styles';
import { COLOR_WHITE } from '../../constants/colors';
export default class extends Component {
static propTypes = {
@ -37,7 +36,7 @@ export default class extends Component {
return false;
}
onPressButton() {
onPressButton = () => {
this.setState({
modalVisible: true
});
@ -67,20 +66,21 @@ export default class extends Component {
return (
[
<RectButton
<Touchable
key='image'
onPress={() => this.onPressButton()}
onActiveStateChange={this.isPressed}
onPress={this.onPressButton}
style={styles.imageContainer}
underlayColor={COLOR_WHITE}
background={Touchable.Ripple('#fff')}
>
<React.Fragment>
<FastImage
style={[styles.image, isPressed && { opacity: 0.5 }]}
source={{ uri: encodeURI(img) }}
resizeMode={FastImage.resizeMode.cover}
/>
{this.getDescription()}
</RectButton>,
</React.Fragment>
</Touchable>,
<PhotoModal
key='modal'
title={file.title}

View File

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

View File

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

View File

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

View File

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

View File

@ -4,6 +4,7 @@ import { View, Text, StyleSheet } from 'react-native';
import moment from 'moment';
import sharedStyles from '../../views/Styles';
import messageStyles from './styles';
const styles = StyleSheet.create({
container: {
@ -26,14 +27,6 @@ const styles = StyleSheet.create({
fontSize: 14,
...sharedStyles.textColorDescription,
...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}
</Text>
</View>
<Text style={styles.time}>{time}</Text>
<Text style={messageStyles.time}>{time}</Text>
</View>
);
}

View File

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

View File

@ -50,6 +50,7 @@ export default class MessageContainer extends React.Component {
// methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
// methods - redux
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
@ -116,12 +117,16 @@ export default class MessageContainer extends React.Component {
onReactionPress(emoji, item._id);
}
onReactionLongPress = () => {
this.setState({ reactionsModal: true });
vibrate();
}
onDiscussionPress = () => {
const { onDiscussionPress, item } = this.props;
onDiscussionPress(item);
}
get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat;
@ -167,7 +172,7 @@ export default class MessageContainer extends React.Component {
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
} = this.props;
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;
const isEditing = editingMessage._id === item._id;
return (
@ -195,6 +200,9 @@ export default class MessageContainer extends React.Component {
reactionsModal={reactionsModal}
useRealName={useRealName}
role={role}
drid={drid}
dcount={dcount}
dlm={dlm}
closeReactions={this.closeReactions}
onErrorPress={this.onErrorPress}
onLongPress={this.onLongPress}
@ -202,6 +210,7 @@ export default class MessageContainer extends React.Component {
onReactionPress={this.onReactionPress}
replyBroadcast={this.replyBroadcast}
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 { 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({
root: {
@ -28,6 +35,11 @@ export default StyleSheet.create({
flexDirection: 'row',
flex: 1
},
text: {
fontSize: 16,
...sharedStyles.textColorNormal,
...sharedStyles.textRegular
},
textInfo: {
fontStyle: 'italic',
fontSize: 16,
@ -55,6 +67,9 @@ export default StyleSheet.create({
marginBottom: 6,
borderRadius: 2
},
reactionButtonReacted: {
backgroundColor: '#e8f2ff'
},
reactionContainer: {
flexDirection: 'row',
justifyContent: 'center',
@ -94,22 +109,28 @@ export default StyleSheet.create({
paddingHorizontal: 15,
paddingVertical: 5
},
broadcastButton: {
width: 107,
buttonContainer: {
marginTop: 6,
flexDirection: 'row',
alignItems: 'center'
},
button: {
paddingHorizontal: 15,
height: 44,
marginTop: 15,
flex: 1,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: COLOR_PRIMARY,
borderRadius: 4
},
broadcastButtonIcon: {
color: COLOR_WHITE,
marginRight: 11
smallButton: {
height: 30
},
broadcastButtonText: {
buttonIcon: {
color: COLOR_WHITE,
marginRight: 6
},
buttonText: {
color: COLOR_WHITE,
fontSize: 14,
...sharedStyles.textMedium
@ -139,8 +160,6 @@ export default StyleSheet.create({
imageContainer: {
flex: 1,
flexDirection: 'column',
borderColor: COLOR_BORDER,
borderWidth: 1,
borderRadius: 4
},
image: {
@ -148,7 +167,8 @@ export default StyleSheet.create({
maxWidth: 400,
minHeight: 200,
borderRadius: 4,
marginBottom: 6
borderColor: COLOR_BORDER,
borderWidth: 1
},
inlineImage: {
width: 300,
@ -159,5 +179,33 @@ export default StyleSheet.create({
fontSize: 14,
...sharedStyles.textColorDescription,
...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',
Disable_notifications: 'Disable notifications',
Discussions: 'Discussions',
Direct_Messages: 'Direct Messages',
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?',
@ -166,6 +167,7 @@ export default {
Group_by_favorites: 'Group favorites',
Group_by_type: 'Group by type',
Has_joined_the_channel: 'Has joined the channel',
Has_joined_the_conversation: 'Has joined the conversation',
Has_left_the_channel: 'Has left the channel',
Invisible: 'Invisible',
Invite: 'Invite',
@ -299,6 +301,7 @@ export default {
starred: 'starred',
Starred: 'Starred',
Start_of_conversation: 'Start of conversation',
Started_discussion: 'Started a discussion:',
Submit: 'Submit',
Take_a_photo: 'Take a photo',
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}}!',
This_room_is_blocked: 'This room is blocked',
This_room_is_read_only: 'This room is read only',
Thread_created: 'Started a new thread: "{{name}}"',
Timezone: 'Timezone',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic',

View File

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

View File

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

View File

@ -19,7 +19,7 @@ async function load({ rid: roomId, lastOpen }) {
lastUpdate = getLastUpdate(roomId);
}
// 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;
}

View File

@ -39,11 +39,10 @@ export async function sendMessageCall(message) {
export default async function(rid, msg) {
try {
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(() => {
room.lastMessage = message;
room.draftMessage = null;
});
try {

View File

@ -1,18 +1,21 @@
import EJSON from 'ejson';
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 removeListener = listener => listener.stop();
let promises;
let timer = null;
let connectedListener;
let disconnectedListener;
export default function subscribeRoom({ rid }) {
if (promises) {
promises.then(unsubscribe);
promises = false;
}
let promises;
let timer = null;
let connectedListener;
let disconnectedListener;
let notifyRoomListener;
let messageReceivedListener;
const typingTimeouts = {};
const loop = () => {
if (timer) {
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 = () => {
if (promises) {
promises.then(unsubscribe);
@ -54,12 +147,28 @@ export default function subscribeRoom({ rid }) {
disconnectedListener.then(removeListener);
disconnectedListener = false;
}
if (notifyRoomListener) {
notifyRoomListener.then(removeListener);
notifyRoomListener = false;
}
if (messageReceivedListener) {
messageReceivedListener.then(removeListener);
messageReceivedListener = false;
}
clearTimeout(timer);
timer = false;
Object.keys(typingTimeouts).forEach((key) => {
if (typingTimeouts[key]) {
clearTimeout(typingTimeouts[key]);
typingTimeouts[key] = null;
}
});
};
connectedListener = this.sdk.onStreamData('connected', handleConnected);
disconnectedListener = this.sdk.onStreamData('close', handleDisconnected);
notifyRoomListener = this.sdk.onStreamData('stream-notify-room', handleNotifyRoomReceived);
messageReceivedListener = this.sdk.onStreamData('stream-room-messages', handleMessageReceived);
try {
promises = this.sdk.subscribeRoom(rid);

View File

@ -102,6 +102,7 @@ const subscriptionSchema = {
notifications: { type: 'bool', optional: true },
muted: { type: 'list', objectType: 'usersMuted' },
broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true }
}
};
@ -219,7 +220,10 @@ const messagesSchema = {
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
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 = [
settingsSchema,
subscriptionSchema,
@ -302,6 +314,8 @@ const schema = [
uploadsSchema
];
const inMemorySchema = [usersTypingSchema];
class DB {
databases = {
serversDB: new Realm({
@ -309,7 +323,23 @@ class DB {
schema: [
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;
}
get memoryDatabase() {
return this.databases.inMemoryDB;
}
setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, '');
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
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';
import { disconnect, connectSuccess, connectRequest } from '../actions/connect';
import { setActiveUser } from '../actions/activeUsers';
import { someoneTyping, roomMessageReceived } from '../actions/room';
import { setRoles } from '../actions/roles';
import subscribeRooms from './methods/subscriptions/rooms';
@ -30,7 +29,6 @@ import getPermissions from './methods/getPermissions';
import getCustomEmoji from './methods/getCustomEmojis';
import canOpenRoom from './methods/canOpenRoom';
import _buildMessage from './methods/helpers/buildMessage';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
import loadMissedMessages from './methods/loadMissedMessages';
@ -202,27 +200,6 @@ const RocketChat = {
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.roles = this.roles || {};
@ -567,6 +544,9 @@ const RocketChat = {
unsubscribe(subscription) {
return this.sdk.unsubscribe(subscription);
},
onStreamData(...args) {
return this.sdk.onStreamData(...args);
},
emitTyping(room, t = true) {
const { login } = reduxStore.getState();
return this.sdk.methodCall('stream-notify-room', `${ room }/typing`, login.user.username, t);
@ -600,6 +580,10 @@ const RocketChat = {
// RC 0.65.0
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) {
try {
if (rid === `${ currentUserId }${ currentUserId }`) {
@ -669,7 +653,13 @@ const RocketChat = {
let roles = [];
try {
// 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
roles = room.roles; // eslint-disable-line prefer-destructuring
} 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,
userMentions: PropTypes.number,
id: PropTypes.string,
prid: PropTypes.string,
onPress: PropTypes.func,
user: PropTypes.shape({
id: PropTypes.string,
@ -209,11 +210,11 @@ export default class RoomItem extends React.Component {
}
get type() {
const { type, id } = this.props;
const { type, id, prid } = this.props;
if (type === 'd') {
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, {

View File

@ -3,7 +3,6 @@ import settings from './reducers';
import login from './login';
import meteor from './connect';
import messages from './messages';
import room from './room';
import rooms from './rooms';
import server from './server';
import selectedUsers from './selectedUsers';
@ -23,7 +22,6 @@ export default combineReducers({
selectedUsers,
createChannel,
app,
room,
rooms,
customEmojis,
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 {
put, call, takeLatest, take, select, race, fork, cancel, takeEvery
call, takeLatest, take, select
} from 'redux-saga/effects';
import { delay } from 'redux-saga';
import EJSON from 'ejson';
import Navigation from '../lib/Navigation';
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 database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
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 watchUserTyping = function* watchUserTyping({ rid, status }) {
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);
if (!auth) {
yield take(types.LOGIN.SUCCESS);
}
const room = yield select(state => state.room);
if (!room) {
return;
}
try {
yield RocketChat.emitTyping(room.rid, status);
yield RocketChat.emitTyping(rid, status);
if (status) {
yield call(delay, 5000);
yield RocketChat.emitTyping(room.rid, false);
yield RocketChat.emitTyping(rid, false);
}
} catch (e) {
log('watchuserTyping', e);
log('watchUserTyping', e);
}
};
@ -147,9 +55,7 @@ const handleEraseRoom = function* handleEraseRoom({ rid, t }) {
};
const root = function* root() {
yield takeLatest(types.ROOM.USER_TYPING, watchuserTyping);
yield takeEvery(types.ROOM.OPEN, watchRoomOpen);
yield takeEvery(types.ROOM.MESSAGE_RECEIVED, handleMessageReceived);
yield takeLatest(types.ROOM.USER_TYPING, watchUserTyping);
yield takeLatest(types.ROOM.LEAVE, handleLeaveRoom);
yield takeLatest(types.ROOM.ERASE, handleEraseRoom);
};

View File

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

View File

@ -21,7 +21,6 @@ const options = [I18n.t('Unpin'), I18n.t('Cancel')];
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
room: state.room,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
@ -38,7 +37,7 @@ export default class PinnedMessagesView extends LoggedView {
user: PropTypes.object,
baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
room: PropTypes.object
navigation: PropTypes.object
}
constructor(props) {
@ -47,6 +46,8 @@ export default class PinnedMessagesView extends LoggedView {
loading: false,
messages: []
};
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
}
componentDidMount() {
@ -114,8 +115,7 @@ export default class PinnedMessagesView extends LoggedView {
this.setState({ loading: true });
try {
const { room } = this.props;
const result = await RocketChat.getMessages(room.rid, room.t, { pinned: true }, messages.length);
const result = await RocketChat.getMessages(this.rid, this.t, { pinned: true }, messages.length);
if (result.success) {
this.setState(prevState => ({
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,
token: state.login.user && state.login.user.token
},
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
room: state.room
baseUrl: state.settings.Site_Url || state.server ? state.server.server : ''
}), dispatch => ({
leaveRoom: (rid, t) => dispatch(leaveRoomAction(rid, t))
}))
@ -50,25 +49,36 @@ export default class RoomActionsView extends LoggedView {
id: PropTypes.string,
token: PropTypes.string
}),
room: PropTypes.object,
leaveRoom: PropTypes.func
}
constructor(props) {
super('RoomActionsView', props);
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
room: this.rooms[0] || props.room,
room: this.rooms[0] || { rid: this.rid, t: this.t },
membersCount: 0,
member: {},
joined: false,
joined: this.rooms.length > 0,
canViewMembers: false
};
}
async componentDidMount() {
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) {
try {
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() {
const { room, joined } = this.state;
const { rid, t } = room;
@ -139,6 +150,7 @@ export default class RoomActionsView extends LoggedView {
return false;
}
// TODO: move to componentDidMount
get canViewMembers() {
const { room } = this.state;
const { rid, t, broadcast } = room;
@ -177,7 +189,8 @@ export default class RoomActionsView extends LoggedView {
icon: 'star',
name: I18n.t('Room_Info'),
route: 'RoomInfoView',
params: { rid },
// forward room only if room isn't joined
params: { rid, t, room: joined ? null : room },
testID: 'room-actions-info'
}],
renderItem: this.renderRoomInfo
@ -203,18 +216,21 @@ export default class RoomActionsView extends LoggedView {
icon: 'file-generic',
name: I18n.t('Files'),
route: 'RoomFilesView',
params: { rid, t },
testID: 'room-actions-files'
},
{
icon: 'at',
name: I18n.t('Mentions'),
route: 'MentionedMessagesView',
params: { rid, t },
testID: 'room-actions-mentioned'
},
{
icon: 'star',
name: I18n.t('Starred'),
route: 'StarredMessagesView',
params: { rid, t },
testID: 'room-actions-starred'
},
{
@ -234,6 +250,7 @@ export default class RoomActionsView extends LoggedView {
icon: 'pin',
name: I18n.t('Pinned'),
route: 'PinnedMessagesView',
params: { rid, t },
testID: 'room-actions-pinned'
}
],
@ -389,8 +406,8 @@ export default class RoomActionsView extends LoggedView {
? <Text style={styles.roomTitle}>{room.fname}</Text>
: (
<View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} />
<Text style={styles.roomTitle}>{room.name}</Text>
<RoomTypeIcon type={room.prid ? 'discussion' : room.t} />
<Text style={styles.roomTitle}>{room.prid ? room.fname : room.name}</Text>
</View>
)
}

View File

@ -16,7 +16,6 @@ import StatusBar from '../../containers/StatusBar';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
room: state.room,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
@ -33,7 +32,7 @@ export default class RoomFilesView extends LoggedView {
user: PropTypes.object,
baseUrl: PropTypes.string,
customEmojis: PropTypes.object,
room: PropTypes.object
navigation: PropTypes.object
}
constructor(props) {
@ -42,6 +41,8 @@ export default class RoomFilesView extends LoggedView {
loading: false,
messages: []
};
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
}
componentDidMount() {
@ -70,8 +71,7 @@ export default class RoomFilesView extends LoggedView {
this.setState({ loading: true });
try {
const { room } = this.props;
const result = await RocketChat.getFiles(room.rid, room.t, messages.length);
const result = await RocketChat.getFiles(this.rid, this.t, messages.length);
if (result.success) {
this.setState(prevState => ({
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>
: (
<View style={styles.roomTitleRow}>
<RoomTypeIcon type={room.t} key='room-info-type' />
<Text testID='room-info-view-name' style={styles.roomTitle} key='room-info-name'>{room.name}</Text>
<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.prid ? room.fname : room.name}</Text>
</View>
)
);
@ -40,8 +40,7 @@ const getRoomTitle = room => (room.t === 'd'
},
activeUsers: state.activeUsers, // TODO: remove it
Message_TimeFormat: state.settings.Message_TimeFormat,
allRoles: state.roles,
room: state.room
allRoles: state.roles
}))
/** @extends React.Component */
export default class RoomInfoView extends LoggedView {
@ -75,12 +74,13 @@ export default class RoomInfoView extends LoggedView {
constructor(props) {
super('RoomInfoView', props);
const rid = props.navigation.getParam('rid');
const room = props.navigation.getParam('room');
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.sub = {
unsubscribe: () => {}
};
this.state = {
room: this.rooms[0] || {},
room: this.rooms[0] || room || {},
roomUser: {},
roles: []
};
@ -90,7 +90,7 @@ export default class RoomInfoView extends LoggedView {
safeAddListener(this.rooms, this.updateRoom);
const { room } = this.state;
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;
navigation.setParams({ showEdit: true });
}

View File

@ -23,7 +23,6 @@ import StatusBar from '../../containers/StatusBar';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
room: state.room,
user: {
id: state.login.user && state.login.user.id,
token: state.login.user && state.login.user.token
@ -191,10 +190,15 @@ export default class RoomMembersView extends LoggedView {
this.setState({ isLoading: true });
const { rid } = this.state;
const { navigation } = this.props;
try {
const membersResult = await RocketChat.getRoomMembers(rid, status);
const members = membersResult.records;
this.setState({ allUsers: status, members, isLoading: false });
navigation.setParams({ allUsers: status });
} catch (error) {
console.log('TCL: fetchMembers -> error', error);
this.setState({ isLoading: false });
}
}
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
},
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} />;
}
const icon = type === 'c' ? 'hashtag' : 'lock';
let icon;
if (type === 'discussion') {
icon = 'chat';
} else if (type === 'c') {
icon = 'hashtag';
} else {
icon = 'lock';
}
return (
<CustomIcon
name={icon}

View File

@ -1,57 +1,20 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, ScrollView
} from 'react-native';
import { connect } from 'react-redux';
import { responsive } from 'react-native-responsive-ui';
import equal from 'deep-equal';
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
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
}
});
import database from '../../../lib/realm';
import Header from './Header';
@responsive
@connect((state) => {
@connect((state, ownProps) => {
let status = '';
let title = '';
const roomType = state.room.t;
if (roomType === 'd') {
const { rid, type } = ownProps;
if (type === 'd') {
if (state.login.user && state.login.user.id) {
const { id: loggedUserId } = state.login.user;
const userId = state.room.rid.replace(loggedUserId, '').trim();
const userId = rid.replace(loggedUserId, '').trim();
if (userId === loggedUserId) {
status = state.login.user.status; // eslint-disable-line
} else {
@ -59,22 +22,9 @@ const styles = StyleSheet.create({
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 {
usersTyping: otherUsersTyping,
type: roomType,
title,
status
};
})
@ -82,14 +32,25 @@ export default class RoomHeaderView extends Component {
static propTypes = {
title: PropTypes.string,
type: PropTypes.string,
prid: PropTypes.string,
rid: PropTypes.string,
window: PropTypes.object,
usersTyping: PropTypes.array,
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 {
type, title, status, usersTyping, window
type, title, status, window
} = this.props;
if (nextProps.type !== type) {
return true;
@ -106,7 +67,7 @@ export default class RoomHeaderView extends Component {
if (nextProps.window.height !== window.height) {
return true;
}
if (!equal(nextProps.usersTyping, usersTyping)) {
if (!equal(nextState.usersTyping, usersTyping)) {
return true;
}
return false;
@ -121,53 +82,26 @@ export default class RoomHeaderView extends Component {
// }
// }
get typing() {
const { usersTyping } = this.props;
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>
);
updateState = () => {
this.setState({ usersTyping: this.usersTyping.slice() });
}
render() {
const { usersTyping } = this.state;
const {
window, title, usersTyping, type, status
window, title, type, status, prid
} = 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 (
<View style={styles.container}>
<View style={[styles.titleContainer, { width: widthScrollView }]}>
<ScrollView
showsHorizontalScrollIndicator={false}
horizontal
bounces={false}
contentContainerStyle={styles.scroll}
>
<Icon type={type} status={status} />
<Text style={[styles.title, { fontSize: TITLE_SIZE * scale }]} numberOfLines={1}>{title}</Text>
</ScrollView>
</View>
{this.typing}
</View>
<Header
prid={prid}
title={title}
type={type}
status={status}
width={window.width}
height={window.height}
usersTyping={usersTyping}
/>
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -164,7 +164,7 @@ export default class Sidebar extends Component {
<React.Fragment>
<SidebarItem
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')}
testID='sidebar-chats'
current={activeItemKey === 'ChatsStack'}

View File

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

View File

@ -36,10 +36,6 @@ describe('Room screen', () => {
// Render - Header
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() => {
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 */; };
7A32C247206D791D001C80E9 /* Crashlytics.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A32C245206D791D001C80E9 /* Crashlytics.framework */; };
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 */; };
7A9B5BCF221F32FA00478E23 /* custom.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 7A9B5BCE221F32F400478E23 /* custom.ttf */; };
7ACD4897222860DE00442C55 /* JavaScriptCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ACD4853222860DE00442C55 /* JavaScriptCore.framework */; };
7AFB806E205AE65700D004E7 /* libRCTToast.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AFB804C205AE63100D004E7 /* libRCTToast.a */; };
832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; };
@ -257,6 +257,13 @@
remoteGlobalIDString = 39DF4FE71E00394E00F5B4B2;
remoteInfo = RCTCustomInputController;
};
7A55F1B92236D50F005109A0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B1A58A7ACB0E4453A44AEC38 /* RNGestureHandler.xcodeproj */;
proxyType = 2;
remoteGlobalIDString = B5C32A36220C603B000FFB8D;
remoteInfo = "RNGestureHandler-tvOS";
};
7A770EC120BECDC7001AD51A /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
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>"; };
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>"; };
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>"; };
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; };
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>"; };
@ -847,7 +854,7 @@
AF5E16F0398347E6A80C8CBE /* Resources */ = {
isa = PBXGroup;
children = (
7A9B5BCE221F32F400478E23 /* custom.ttf */,
7A55F1C42236D541005109A0 /* custom.ttf */,
);
name = Resources;
sourceTree = "<group>";
@ -1297,6 +1304,13 @@
remoteRef = 7A430E1D20238C02008F55BC /* PBXContainerItemProxy */;
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 */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -1360,6 +1374,13 @@
remoteRef = 7AA7B71B2229AE520039764A /* PBXContainerItemProxy */;
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 */ = {
isa = PBXReferenceProxy;
fileType = archive.ar;
@ -1486,8 +1507,8 @@
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
7A9B5BCF221F32FA00478E23 /* custom.ttf in Resources */,
7A309C9C20724870000C6B13 /* Fabric.sh in Resources */,
7A55F1C52236D541005109A0 /* custom.ttf in Resources */,
13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;

Binary file not shown.

View File

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

View File

@ -1,5 +1,6 @@
import React from 'react';
import { ScrollView, StyleSheet } from 'react-native';
// import moment from 'moment';
import MessageComponent from '../../app/containers/message/Message';
import StoriesSeparator from './StoriesSeparator';
@ -359,6 +360,64 @@ export default (
<Separator title='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' />
<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 Avatar from './Avatar';
import Message from './Message';
// import RoomViewHeader from './RoomViewHeader';
const reducers = combineReducers({
settings: () => ({}),
@ -27,3 +28,6 @@ storiesOf('RoomItem', module)
.add('list', () => RoomItem);
storiesOf('Message', module)
.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"
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:
version "0.25.1"
resolved "https://registry.yarnpkg.com/react-native-device-info/-/react-native-device-info-0.25.1.tgz#bde3be9fe0e06d0c07ab5837e4fc1af90f66696b"