[NEW] Discussions (#696)
This commit is contained in:
parent
7a80550d61
commit
1d9acdb700
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 }]} />;
|
||||
}
|
||||
|
|
|
@ -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')}
|
||||
>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</RectButton>,
|
||||
<React.Fragment>
|
||||
<FastImage
|
||||
style={[styles.image, isPressed && { opacity: 0.5 }]}
|
||||
source={{ uri: encodeURI(img) }}
|
||||
resizeMode={FastImage.resizeMode.cover}
|
||||
/>
|
||||
{this.getDescription()}
|
||||
</React.Fragment>
|
||||
</Touchable>,
|
||||
<PhotoModal
|
||||
key='modal'
|
||||
title={file.title}
|
||||
|
|
|
@ -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={[
|
||||
|
|
|
@ -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,31 +295,27 @@ 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
|
||||
<Touchable
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
onLongPress={onReactionLongPress}
|
||||
key={reaction.emoji}
|
||||
onHandlerStateChange={({ nativeEvent }) => nativeEvent.state === State.ACTIVE && onReactionLongPress()}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && styles.reactionButtonReacted]}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<RectButton
|
||||
onPress={() => onReactionPress(reaction.emoji)}
|
||||
testID={`message-reaction-${ reaction.emoji }`}
|
||||
style={[styles.reactionButton, reacted && { backgroundColor: '#e8f2ff' }]}
|
||||
activeOpacity={0.8}
|
||||
underlayColor={underlayColor}
|
||||
>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</RectButton>
|
||||
</LongPressGestureHandler>
|
||||
<View style={[styles.reactionContainer, reacted && styles.reactedContainer]}>
|
||||
<Emoji
|
||||
content={reaction.emoji}
|
||||
customEmojis={customEmojis}
|
||||
standardEmojiStyle={styles.reactionEmoji}
|
||||
customEmojiStyle={styles.reactionCustomEmoji}
|
||||
baseUrl={baseUrl}
|
||||
/>
|
||||
<Text style={styles.reactionCount}>{ reaction.usernames.length }</Text>
|
||||
</View>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
onPress={replyBroadcast}
|
||||
style={styles.broadcastButton}
|
||||
activeOpacity={0.5}
|
||||
underlayColor={COLOR_WHITE}
|
||||
>
|
||||
<CustomIcon name='back' size={20} style={styles.broadcastButtonIcon} />
|
||||
<Text style={styles.broadcastButtonText}>{I18n.t('Reply')}</Text>
|
||||
</RectButton>
|
||||
<View style={styles.buttonContainer}>
|
||||
<Touchable
|
||||
onPress={replyBroadcast}
|
||||
background={Touchable.Ripple('#fff')}
|
||||
style={styles.button}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
>
|
||||
<React.Fragment>
|
||||
<CustomIcon name='back' size={20} style={styles.buttonIcon} />
|
||||
<Text style={styles.buttonText}>{I18n.t('Reply')}</Text>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderDiscussion = () => {
|
||||
const {
|
||||
msg, dcount, dlm, onDiscussionPress
|
||||
} = this.props;
|
||||
const time = 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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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')}
|
||||
>
|
||||
{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>
|
||||
{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>
|
||||
</React.Fragment>
|
||||
</Touchable>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
@ -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, {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 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 watchUserTyping = function* watchUserTyping({ rid, 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);
|
||||
};
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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],
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
const membersResult = await RocketChat.getRoomMembers(rid, status);
|
||||
const members = membersResult.records;
|
||||
this.setState({ allUsers: status, members, isLoading: false });
|
||||
navigation.setParams({ allUsers: status });
|
||||
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 = () => {
|
||||
|
|
|
@ -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;
|
|
@ -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}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
this.updateState.stop();
|
||||
if (this.updateState && this.updateState.stop) {
|
||||
this.updateState.stop();
|
||||
}
|
||||
if (this.interactionManager && this.interactionManager.cancel) {
|
||||
this.interactionManager.cancel();
|
||||
}
|
||||
console.countReset(`${ this.constructor.name }.render calls`);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
updateState = debounce(() => {
|
||||
this.setState({ messages: this.data.slice(), loading: false, loadingMore: false });
|
||||
this.interactionManager = InteractionManager.runAfterInteractions(() => {
|
||||
this.setState({ messages: this.data.slice(), loading: false, loadingMore: false });
|
||||
});
|
||||
}, 300);
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
);
|
||||
}
|
||||
safeAddListener(this.rooms, this.updateRoom);
|
||||
this.internalSetState({ loaded: true });
|
||||
// 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);
|
||||
});
|
||||
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) {
|
||||
RocketChat.loadMissedMessages(room).catch(e => console.log(e));
|
||||
RocketChat.readMessages(room.rid).catch(e => console.log(e));
|
||||
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;
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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 || [],
|
||||
|
|
|
@ -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'}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
BIN
ios/custom.ttf
BIN
ios/custom.ttf
Binary file not shown.
|
@ -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",
|
||||
|
|
|
@ -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 />
|
||||
|
||||
|
|
|
@ -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>
|
||||
);
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue