[NEW] Threads (#798)

This commit is contained in:
Diego Mello 2019-04-17 14:01:03 -03:00 committed by GitHub
parent 5aec0ec186
commit 9cf81bbab9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 4605 additions and 1616 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,25 +1,5 @@
import * as types from './actionsTypes';
export function messagesRequest(room) {
return {
type: types.MESSAGES.REQUEST,
room
};
}
export function messagesSuccess() {
return {
type: types.MESSAGES.SUCCESS
};
}
export function messagesFailure(err) {
return {
type: types.MESSAGES.FAILURE,
err
};
}
export function actionsShow(actionMessage) {
return {
type: types.MESSAGES.ACTIONS_SHOW,

View File

@ -1,16 +1,19 @@
import { SERVER } from './actionsTypes';
export function selectServerRequest(server) {
export function selectServerRequest(server, version, fetchVersion = true) {
return {
type: SERVER.SELECT_REQUEST,
server
server,
version,
fetchVersion
};
}
export function selectServerSuccess(server) {
export function selectServerSuccess(server, version) {
return {
type: SERVER.SELECT_SUCCESS,
server
server,
version
};
}

View File

@ -61,5 +61,8 @@ export default {
},
Assets_favicon_512: {
type: null
},
Threads_enabled: {
type: null
}
};

View File

@ -56,6 +56,7 @@ class MessageBox extends Component {
replyMessage: PropTypes.object,
replying: PropTypes.bool,
editing: PropTypes.bool,
threadsEnabled: PropTypes.bool,
user: PropTypes.shape({
id: PropTypes.string,
username: PropTypes.string,
@ -93,7 +94,7 @@ class MessageBox extends Component {
componentDidMount() {
const { rid } = this.props;
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room.draftMessage && room.draftMessage !== '') {
if (room && room.draftMessage) {
this.setInput(room.draftMessage);
this.setShowSend(true);
}
@ -571,18 +572,27 @@ class MessageBox extends Component {
if (message.trim() === '') {
return;
}
// if is editing a message
const {
editing, replying
} = this.props;
// Edit
if (editing) {
const { _id, rid } = editingMessage;
editRequest({ _id, msg: message, rid });
// Reply
} else if (replying) {
const {
user, replyMessage, roomType, closeReply
} = this.props;
const { replyMessage, closeReply, threadsEnabled } = this.props;
// Thread
if (threadsEnabled) {
onSubmit(message, replyMessage._id);
// Legacy reply
} else {
const { user, roomType } = this.props;
const permalink = await this.getPermalink(replyMessage);
let msg = `[ ](${ permalink }) `;
@ -593,9 +603,11 @@ class MessageBox extends Component {
msg = `${ msg } ${ message }`;
onSubmit(msg);
}
closeReply();
// Normal message
} else {
// if is submiting a new message
onSubmit(message);
}
this.clearInput();
@ -820,6 +832,7 @@ const mapStateToProps = state => ({
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
threadsEnabled: state.settings.Threads_enabled,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,

View File

@ -85,8 +85,6 @@ 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 '';
};
@ -99,6 +97,7 @@ export default class Message extends PureComponent {
baseUrl: PropTypes.string.isRequired,
customEmojis: PropTypes.object.isRequired,
timeFormat: PropTypes.string.isRequired,
customThreadTimeFormat: PropTypes.string,
msg: PropTypes.string,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
@ -137,6 +136,10 @@ export default class Message extends PureComponent {
useRealName: PropTypes.bool,
dcount: PropTypes.number,
dlm: PropTypes.instanceOf(Date),
tmid: PropTypes.string,
tcount: PropTypes.number,
tlm: PropTypes.instanceOf(Date),
tmsg: PropTypes.string,
// methods
closeReactions: PropTypes.func,
onErrorPress: PropTypes.func,
@ -144,8 +147,10 @@ export default class Message extends PureComponent {
onReactionLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
onDiscussionPress: PropTypes.func,
onThreadPress: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
}
static defaultProps = {
@ -169,6 +174,32 @@ export default class Message extends PureComponent {
onLongPress();
}
formatLastMessage = (lm) => {
const { customThreadTimeFormat } = this.props;
if (customThreadTimeFormat) {
return moment(lm).format(customThreadTimeFormat);
}
return lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null;
}
formatMessageCount = (count, type) => {
const discussion = type === 'discussion';
let text = discussion ? I18n.t('No_messages_yet') : null;
if (count === 1) {
text = `${ count } ${ discussion ? I18n.t('message') : I18n.t('reply') }`;
} else if (count > 1 && count < 1000) {
text = `${ count } ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
} else if (count > 999) {
text = `+999 ${ discussion ? I18n.t('messages') : I18n.t('replies') }`;
}
return text;
}
isInfoMessage = () => {
const { type } = this.props;
return SYSTEM_MESSAGES.includes(type);
@ -369,23 +400,11 @@ export default class Message extends PureComponent {
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';
}
const time = this.formatLastMessage(dlm);
const buttonText = this.formatMessageCount(dcount, 'discussion');
return (
<React.Fragment>
<Text style={styles.textInfo}>{I18n.t('Started_discussion')}</Text>
<Text style={styles.startedDiscussion}>{I18n.t('Started_discussion')}</Text>
<Text style={styles.text}>{msg}</Text>
<View style={styles.buttonContainer}>
<Touchable
@ -405,6 +424,56 @@ export default class Message extends PureComponent {
);
}
renderThread = () => {
const {
tcount, tlm, onThreadPress, msg
} = this.props;
if (!tlm) {
return null;
}
const time = this.formatLastMessage(tlm);
const buttonText = this.formatMessageCount(tcount, 'thread');
return (
<View style={styles.buttonContainer}>
<Touchable
onPress={onThreadPress}
background={Touchable.Ripple('#fff')}
style={[styles.button, styles.smallButton]}
hitSlop={BUTTON_HIT_SLOP}
testID={`message-thread-button-${ msg }`}
>
<React.Fragment>
<CustomIcon name='thread' size={20} style={styles.buttonIcon} />
<Text style={styles.buttonText}>{buttonText}</Text>
</React.Fragment>
</Touchable>
<Text style={styles.time}>{time}</Text>
</View>
);
}
renderRepliedThread = () => {
const {
tmid, tmsg, header, onThreadPress, fetchThreadName
} = this.props;
if (!tmid || !header || this.isTemp()) {
return null;
}
if (!tmsg) {
fetchThreadName(tmid);
return null;
}
return (
<Text style={styles.repliedThread} numberOfLines={3} testID={`message-thread-replied-on-${ tmsg }`}>
{I18n.t('Replied_on')} <Text style={styles.repliedThreadName} onPress={onThreadPress}>{tmsg}</Text>
</Text>
);
}
renderInner = () => {
const { type } = this.props;
if (type === 'discussion-created') {
@ -418,9 +487,11 @@ export default class Message extends PureComponent {
return (
<React.Fragment>
{this.renderUsername()}
{this.renderRepliedThread()}
{this.renderContent()}
{this.renderAttachment()}
{this.renderUrl()}
{this.renderThread()}
{this.renderReactions()}
{this.renderBroadcastReply()}
</React.Fragment>

View File

@ -11,6 +11,7 @@ import {
replyBroadcast as replyBroadcastAction
} from '../../actions/messages';
import { vibrate } from '../../utils/vibration';
import debounce from '../../utils/debounce';
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -27,15 +28,14 @@ import { vibrate } from '../../utils/vibration';
export default class MessageContainer extends React.Component {
static propTypes = {
item: PropTypes.object.isRequired,
reactions: PropTypes.any.isRequired,
user: PropTypes.shape({
id: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
token: PropTypes.string.isRequired
}),
customTimeFormat: PropTypes.string,
customThreadTimeFormat: PropTypes.string,
style: ViewPropTypes.style,
status: PropTypes.number,
archived: PropTypes.bool,
broadcast: PropTypes.bool,
previousItem: PropTypes.object,
@ -47,6 +47,8 @@ export default class MessageContainer extends React.Component {
Message_TimeFormat: PropTypes.string,
editingMessage: PropTypes.object,
useRealName: PropTypes.bool,
status: PropTypes.number,
navigation: PropTypes.object,
// methods - props
onLongPress: PropTypes.func,
onReactionPress: PropTypes.func,
@ -54,7 +56,8 @@ export default class MessageContainer extends React.Component {
// methods - redux
errorActionsShow: PropTypes.func,
replyBroadcast: PropTypes.func,
toggleReactionPicker: PropTypes.func
toggleReactionPicker: PropTypes.func,
fetchThreadName: PropTypes.func
}
static defaultProps = {
@ -73,7 +76,7 @@ export default class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) {
const { reactionsModal } = this.state;
const {
status, reactions, broadcast, _updatedAt, editingMessage, item
status, editingMessage, item, _updatedAt
} = this.props;
if (reactionsModal !== nextState.reactionsModal) {
@ -82,16 +85,10 @@ export default class MessageContainer extends React.Component {
if (status !== nextProps.status) {
return true;
}
// eslint-disable-next-line
if (!!_updatedAt ^ !!nextProps._updatedAt) {
return true;
}
if (!equal(reactions, nextProps.reactions)) {
return true;
}
if (broadcast !== nextProps.broadcast) {
if (item.tmsg !== nextProps.item.tmsg) {
return true;
}
if (!equal(editingMessage, nextProps.editingMessage)) {
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
return true;
@ -99,7 +96,7 @@ export default class MessageContainer extends React.Component {
return true;
}
}
return _updatedAt.toGMTString() !== nextProps._updatedAt.toGMTString();
return _updatedAt.toISOString() !== nextProps._updatedAt.toISOString();
}
onLongPress = () => {
@ -127,6 +124,20 @@ export default class MessageContainer extends React.Component {
onDiscussionPress(item);
}
onThreadPress = debounce(() => {
const { navigation, item } = this.props;
if (item.tmid) {
navigation.push('RoomView', {
rid: item.rid, tmid: item.tmid, name: item.tmsg, t: 'thread'
});
} else if (item.tlm) {
const title = item.msg || (item.attachments && item.attachments.length && item.attachments[0].title);
navigation.push('RoomView', {
rid: item.rid, tmid: item._id, name: title, t: 'thread'
});
}
}, 1000, true)
get timeFormat() {
const { customTimeFormat, Message_TimeFormat } = this.props;
return customTimeFormat || Message_TimeFormat;
@ -145,6 +156,7 @@ export default class MessageContainer extends React.Component {
&& (previousItem.u.username === item.u.username)
&& !(previousItem.groupable === false || item.groupable === false || broadcast === true)
&& (item.ts - previousItem.ts < Message_GroupingPeriod * 1000)
&& (previousItem.tmid === item.tmid)
)) {
return false;
}
@ -169,14 +181,15 @@ export default class MessageContainer extends React.Component {
render() {
const { reactionsModal } = this.state;
const {
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast
item, editingMessage, user, style, archived, baseUrl, customEmojis, useRealName, broadcast, fetchThreadName, customThreadTimeFormat
} = this.props;
const {
msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm
_id, msg, ts, attachments, urls, reactions, t, status, avatar, u, alias, editedBy, role, drid, dcount, dlm, tmid, tcount, tlm, tmsg
} = item;
const isEditing = editingMessage._id === item._id;
return (
<Message
id={_id}
msg={msg}
author={u}
ts={ts}
@ -192,6 +205,7 @@ export default class MessageContainer extends React.Component {
user={user}
edited={editedBy && !!editedBy.username}
timeFormat={this.timeFormat}
customThreadTimeFormat={customThreadTimeFormat}
style={style}
archived={archived}
broadcast={broadcast}
@ -203,6 +217,11 @@ export default class MessageContainer extends React.Component {
drid={drid}
dcount={dcount}
dlm={dlm}
tmid={tmid}
tcount={tcount}
tlm={tlm}
tmsg={tmsg}
fetchThreadName={fetchThreadName}
closeReactions={this.closeReactions}
onErrorPress={this.onErrorPress}
onLongPress={this.onLongPress}
@ -211,6 +230,7 @@ export default class MessageContainer extends React.Component {
replyBroadcast={this.replyBroadcast}
toggleReactionPicker={this.toggleReactionPicker}
onDiscussionPress={this.onDiscussionPress}
onThreadPress={this.onThreadPress}
/>
);
}

View File

@ -121,7 +121,7 @@ export default StyleSheet.create({
alignItems: 'center',
justifyContent: 'center',
backgroundColor: COLOR_PRIMARY,
borderRadius: 4
borderRadius: 2
},
smallButton: {
height: 30
@ -200,6 +200,13 @@ export default StyleSheet.create({
color: COLOR_PRIMARY,
...sharedStyles.textRegular
},
startedDiscussion: {
fontStyle: 'italic',
fontSize: 16,
marginBottom: 6,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
time: {
fontSize: 12,
paddingLeft: 10,
@ -207,5 +214,16 @@ export default StyleSheet.create({
...sharedStyles.textColorDescription,
...sharedStyles.textRegular,
fontWeight: '300'
},
repliedThread: {
fontSize: 16,
marginBottom: 6,
...sharedStyles.textColorDescription,
...sharedStyles.textRegular
},
repliedThreadName: {
fontSize: 16,
color: COLOR_PRIMARY,
...sharedStyles.textSemibold
}
});

View File

@ -124,6 +124,7 @@ export default {
Connect: 'Connect',
Connect_to_a_server: 'Connect to a server',
Connected: 'Connected',
connecting_server: 'connecting to server',
Connecting: 'Connecting...',
Continue_with: 'Continue with',
Copied_to_clipboard: 'Copied to clipboard!',
@ -198,6 +199,8 @@ export default {
Message_actions: 'Message actions',
Message_pinned: 'Message pinned',
Message_removed: 'Message removed',
message: 'message',
messages: 'messages',
Messages: 'Messages',
Microphone_Permission_Message: 'Rocket Chat needs access to your microphone so you can send audio message.',
Microphone_Permission: 'Microphone Permission',
@ -217,10 +220,12 @@ export default {
No_pinned_messages: 'No pinned messages',
No_results_found: 'No results found',
No_starred_messages: 'No starred messages',
No_thread_messages: 'No thread messages',
No_announcement_provided: 'No announcement provided.',
No_description_provided: 'No description provided.',
No_topic_provided: 'No topic provided.',
No_Message: 'No Message',
No_messages_yet: 'No messages yet',
No_Reactions: 'No Reactions',
Not_logged: 'Not logged',
Nothing_to_save: 'Nothing to save!',
@ -256,6 +261,9 @@ export default {
Read_Only: 'Read Only',
Register: 'Register',
Repeat_Password: 'Repeat Password',
Replied_on: 'Replied on:',
replies: 'replies',
reply: 'reply',
Reply: 'Reply',
Resend: 'Resend',
Reset_password: 'Reset password',
@ -311,7 +319,8 @@ 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}}"',
Thread: 'Thread',
Threads: 'Threads',
Timezone: 'Timezone',
Toggle_Drawer: 'Toggle_Drawer',
topic: 'topic',

View File

@ -131,6 +131,7 @@ export default {
Connect: 'Conectar',
Connect_to_a_server: 'Conectar a um servidor',
Connected: 'Conectado',
connecting_server: 'conectando no servidor',
Connecting: 'Conectando...',
Continue_with: 'Entrar com',
Copied_to_clipboard: 'Copiado para a área de transferência!',
@ -202,6 +203,8 @@ export default {
Message_actions: 'Ações',
Message_pinned: 'Fixou uma mensagem',
Message_removed: 'Mensagem removida',
message: 'mensagem',
messages: 'mensagens',
Messages: 'Mensagens',
Microphone_Permission_Message: 'Rocket Chat precisa de acesso ao seu microfone para enviar mensagens de áudio.',
Microphone_Permission: 'Acesso ao Microfone',
@ -220,10 +223,12 @@ export default {
No_pinned_messages: 'Não há mensagens fixadas',
No_results_found: 'Nenhum resultado encontrado',
No_starred_messages: 'Não há mensagens favoritas',
No_thread_messages: 'Não há tópicos',
No_announcement_provided: 'Sem anúncio.',
No_description_provided: 'Sem descrição.',
No_topic_provided: 'Sem tópico.',
No_Message: 'Não há mensagens',
No_messages_yet: 'Não há mensagens ainda',
No_Reactions: 'Sem reações',
Nothing_to_save: 'Nada para salvar!',
Notify_active_in_this_room: 'Notificar usuários ativos nesta sala',
@ -258,6 +263,9 @@ export default {
Read_Only: 'Somente Leitura',
Register: 'Registrar',
Repeat_Password: 'Repetir Senha',
Replied_on: 'Respondido em:',
replies: 'respostas',
reply: 'resposta',
Reply: 'Responder',
Resend: 'Reenviar',
Reset_password: 'Resetar senha',
@ -310,7 +318,8 @@ 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}}"',
Thread: 'Tópico',
Threads: 'Tópicos',
Timezone: 'Fuso horário',
topic: 'tópico',
Topic: 'Tópico',

View File

@ -29,6 +29,7 @@ import MentionedMessagesView from './views/MentionedMessagesView';
import StarredMessagesView from './views/StarredMessagesView';
import SearchMessagesView from './views/SearchMessagesView';
import PinnedMessagesView from './views/PinnedMessagesView';
import ThreadMessagesView from './views/ThreadMessagesView';
import SelectedUsersView from './views/SelectedUsersView';
import CreateChannelView from './views/CreateChannelView';
import LegalView from './views/LegalView';
@ -122,7 +123,8 @@ const ChatsStack = createStackNavigator({
StarredMessagesView,
SearchMessagesView,
PinnedMessagesView,
SelectedUsersView
SelectedUsersView,
ThreadMessagesView
}, {
defaultNavigationOptions: defaultHeader
});

View File

@ -39,8 +39,13 @@ export default function loadMessagesForRoom(...args) {
if (data && data.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => data.forEach((message) => {
message = buildMessage(message);
try {
database.create('messages', buildMessage(message), true);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
} catch (e) {
log('loadMessagesForRoom -> create messages', e);
}

View File

@ -31,11 +31,15 @@ export default function loadMissedMessages(...args) {
if (data) {
if (data.updated && data.updated.length) {
const { updated } = data;
updated.forEach(buildMessage);
InteractionManager.runAfterInteractions(() => {
database.write(() => updated.forEach((message) => {
try {
message = buildMessage(message);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
}
} catch (e) {
log('loadMissedMessages -> create messages', e);
}

View File

@ -0,0 +1,48 @@
import { InteractionManager } from 'react-native';
import EJSON from 'ejson';
import buildMessage from './helpers/buildMessage';
import database from '../realm';
import log from '../../utils/log';
async function load({ tmid, skip }) {
try {
// RC 1.0
const data = await this.sdk.methodCall('getThreadMessages', { tmid, limit: 50, skip });
if (!data || data.status === 'error') {
return [];
}
return data;
} catch (error) {
console.log(error);
return [];
}
}
export default function loadThreadMessages({ tmid, skip }) {
return new Promise(async(resolve, reject) => {
try {
const data = await load.call(this, { tmid, skip });
if (data && data.length) {
InteractionManager.runAfterInteractions(() => {
database.write(() => data.forEach((m) => {
try {
const message = buildMessage(EJSON.fromJSONValue(m));
message.rid = tmid;
database.create('threadMessages', message, true);
} catch (e) {
log('loadThreadMessages -> create messages', e);
}
}));
return resolve(data);
});
} else {
return resolve([]);
}
} catch (e) {
log('loadThreadMessages', e);
reject(e);
}
});
}

View File

@ -5,12 +5,13 @@ import reduxStore from '../createStore';
import log from '../../utils/log';
import random from '../../utils/random';
export const getMessage = (rid, msg = {}) => {
export const getMessage = (rid, msg = '', tmid) => {
const _id = random(17);
const message = {
_id,
rid,
msg,
tmid,
ts: new Date(),
_updatedAt: new Date(),
status: messagesStatus.TEMP,
@ -30,20 +31,28 @@ export const getMessage = (rid, msg = {}) => {
};
export async function sendMessageCall(message) {
const { _id, rid, msg } = message;
const {
_id, rid, msg, tmid
} = message;
// RC 0.60.0
const data = await this.sdk.post('chat.sendMessage', { message: { _id, rid, msg } });
const data = await this.sdk.post('chat.sendMessage', {
message: {
_id, rid, msg, tmid
}
});
return data;
}
export default async function(rid, msg) {
export default async function(rid, msg, tmid) {
try {
const message = getMessage(rid, msg);
const message = getMessage(rid, msg, tmid);
const [room] = database.objects('subscriptions').filtered('rid == $0', rid);
if (room) {
database.write(() => {
room.draftMessage = null;
});
}
try {
const ret = await sendMessageCall.call(this, message);

View File

@ -4,6 +4,7 @@ import log from '../../../utils/log';
import protectedFunction from '../helpers/protectedFunction';
import buildMessage from '../helpers/buildMessage';
import database from '../../realm';
import debounce from '../../../utils/debounce';
const unsubscribe = subscriptions => subscriptions.forEach(sub => sub.unsubscribe().catch(() => console.log('unsubscribeRoom')));
const removeListener = listener => listener.stop();
@ -107,27 +108,47 @@ export default function subscribeRoom({ rid }) {
const { _id } = ddpMessage.fields.args[0];
const message = database.objects('messages').filtered('_id = $0', _id);
database.delete(message);
const thread = database.objects('threads').filtered('_id = $0', _id);
database.delete(thread);
const threadMessage = database.objects('threadMessages').filtered('_id = $0', _id);
database.delete(threadMessage);
const cleanTmids = database.objects('messages').filtered('tmid = $0', _id).snapshot();
if (cleanTmids && cleanTmids.length) {
cleanTmids.forEach((m) => {
m.tmid = null;
});
}
}
});
}
});
const read = debounce(() => {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room._id) {
this.readMessages(rid);
}
}, 300);
const handleMessageReceived = protectedFunction((ddpMessage) => {
const message = buildMessage(ddpMessage.fields.args[0]);
const message = buildMessage(EJSON.fromJSONValue(ddpMessage.fields.args[0]));
if (rid !== message.rid) {
return;
}
requestAnimationFrame(() => {
try {
database.write(() => {
database.create('messages', EJSON.fromJSONValue(message), true);
database.create('messages', message, true);
// if it's a thread "header"
if (message.tlm) {
database.create('threads', message, true);
} else if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
});
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room._id) {
this.readMessages(rid);
}
read();
} catch (e) {
console.warn('handleMessageReceived', e);
}

View File

@ -11,7 +11,8 @@ const serversSchema = {
id: 'string',
name: { type: 'string', optional: true },
iconURL: { type: 'string', optional: true },
roomsUpdatedAt: { type: 'date', optional: true }
roomsUpdatedAt: { type: 'date', optional: true },
version: 'string?'
}
};
@ -206,8 +207,6 @@ const messagesSchema = {
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
// mentions: [],
// channels: [],
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
@ -223,7 +222,70 @@ const messagesSchema = {
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true }
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]'
}
};
const threadsSchema = {
name: 'threads',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true },
drid: { type: 'string', optional: true },
dcount: { type: 'int', optional: true },
dlm: { type: 'date', optional: true },
tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true },
replies: 'string[]'
}
};
const threadMessagesSchema = {
name: 'threadMessages',
primaryKey: '_id',
properties: {
_id: 'string',
msg: { type: 'string', optional: true },
t: { type: 'string', optional: true },
rid: { type: 'string', indexed: true },
ts: 'date',
u: 'users',
alias: { type: 'string', optional: true },
parseUrls: { type: 'bool', optional: true },
groupable: { type: 'bool', optional: true },
avatar: { type: 'string', optional: true },
attachments: { type: 'list', objectType: 'attachment' },
urls: { type: 'list', objectType: 'url', default: [] },
_updatedAt: { type: 'date', optional: true },
status: { type: 'int', optional: true },
pinned: { type: 'bool', optional: true },
starred: { type: 'bool', optional: true },
editedBy: 'messagesEditedBy',
reactions: { type: 'list', objectType: 'messagesReactions' },
role: { type: 'string', optional: true }
}
};
@ -296,6 +358,8 @@ const schema = [
subscriptionSchema,
subscriptionRolesSchema,
messagesSchema,
threadsSchema,
threadMessagesSchema,
usersSchema,
roomsSchema,
attachment,
@ -323,9 +387,9 @@ class DB {
schema: [
serversSchema
],
schemaVersion: 2,
schemaVersion: 4,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion === 1 && newRealm.schemaVersion === 2) {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 3) {
const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus
@ -363,6 +427,10 @@ class DB {
return this.database.objects(...args);
}
objectForPrimaryKey(...args) {
return this.database.objectForPrimaryKey(...args);
}
get database() {
return this.databases.activeDB;
}
@ -376,9 +444,9 @@ class DB {
return this.databases.activeDB = new Realm({
path: `${ path }.realm`,
schema,
schemaVersion: 4,
schemaVersion: 6,
migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion === 3 && newRealm.schemaVersion === 4) {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 6) {
const newSubs = newRealm.objects('subscriptions');
// eslint-disable-next-line no-plusplus

View File

@ -9,6 +9,7 @@ import messagesStatus from '../constants/messagesStatus';
import database, { safeAddListener } from './realm';
import log from '../utils/log';
import { isIOS, getBundleId } from '../utils/deviceInfo';
import EventEmitter from '../utils/events';
import {
setUser, setLoginServices, loginRequest, loginFailure, logout
@ -31,6 +32,7 @@ import canOpenRoom from './methods/canOpenRoom';
import loadMessagesForRoom from './methods/loadMessagesForRoom';
import loadMissedMessages from './methods/loadMissedMessages';
import loadThreadMessages from './methods/loadThreadMessages';
import sendMessage, { getMessage, sendMessageCall } from './methods/sendMessage';
import { sendFileMessage, cancelUpload, isUploadActive } from './methods/sendFileMessage';
@ -78,26 +80,24 @@ const RocketChat = {
console.warn(`AsyncStorage error: ${ error.message }`);
}
},
async testServer(server) {
async getServerInfo(server) {
try {
const result = await fetch(`${ server }/api/v1/info`).then(response => response.json());
if (result.success && result.info) {
if (semver.lt(result.info.version, MIN_ROCKETCHAT_VERSION)) {
const result = await fetch(`${ server }/api/info`).then(response => response.json());
if (result.success) {
if (semver.lt(result.version, MIN_ROCKETCHAT_VERSION)) {
return {
success: false,
message: 'Invalid_server_version',
messageOptions: {
currentVersion: result.info.version,
currentVersion: result.version,
minVersion: MIN_ROCKETCHAT_VERSION
}
};
}
return {
success: true
};
return result;
}
} catch (e) {
log('testServer', e);
log('getServerInfo', e);
}
return {
success: false,
@ -135,6 +135,7 @@ const RocketChat = {
}
},
async loginSuccess({ user }) {
EventEmitter.emit('connected');
reduxStore.dispatch(setUser(user));
reduxStore.dispatch(roomsRequest());
@ -370,6 +371,7 @@ const RocketChat = {
},
loadMissedMessages,
loadMessagesForRoom,
loadThreadMessages,
getMessage,
sendMessage,
getRooms,
@ -568,9 +570,9 @@ const RocketChat = {
// RC 0.64.0
return this.sdk.post('rooms.favorite', { roomId, favorite });
},
getRoomMembers(rid, allUsers) {
getRoomMembers(rid, allUsers, skip = 0, limit = 10) {
// RC 0.42.0
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers);
return this.sdk.methodCall('getUsersOfRoom', rid, allUsers, { skip, limit });
},
getUserRoles() {
// RC 0.27.0
@ -649,6 +651,10 @@ const RocketChat = {
// RC 0.51.0
return this.sdk.methodCall('addUsersToRoom', { rid, users });
},
getSingleMessage(msgId) {
// RC 0.57.0
return this.sdk.methodCall('getSingleMessage', msgId);
},
hasPermission(permissions, rid) {
let roles = [];
try {
@ -768,6 +774,17 @@ const RocketChat = {
roomId,
searchText
});
},
toggleFollowMessage(mid, follow) {
// RC 1.0
if (follow) {
return this.sdk.methodCall('followMessage', { mid });
}
return this.sdk.methodCall('unfollowMessage', { mid });
},
getThreadsList({ rid, limit, skip }) {
// RC 1.0
return this.sdk.methodCall('getThreadsList', { rid, limit, skip });
}
};

View File

@ -1,8 +1,6 @@
import * as types from '../actions/actionsTypes';
const initialState = {
isFetching: false,
failure: false,
message: {},
actionMessage: {},
replyMessage: {},
@ -14,23 +12,6 @@ const initialState = {
export default function messages(state = initialState, action) {
switch (action.type) {
case types.MESSAGES.REQUEST:
return {
...state,
isFetching: true
};
case types.MESSAGES.SUCCESS:
return {
...state,
isFetching: false
};
case types.LOGIN.FAILURE:
return {
...state,
isFetching: false,
failure: true,
errorMessage: action.err
};
case types.MESSAGES.ACTIONS_SHOW:
return {
...state,

View File

@ -5,6 +5,7 @@ const initialState = {
connected: false,
failure: false,
server: '',
version: null,
loading: true,
adding: false
};
@ -29,6 +30,7 @@ export default function server(state = initialState, action) {
return {
...state,
server: action.server,
version: action.version,
connecting: true,
connected: false,
loading: true
@ -37,6 +39,7 @@ export default function server(state = initialState, action) {
return {
...state,
server: action.server,
version: action.version,
connecting: false,
connected: true,
loading: false

View File

@ -69,7 +69,7 @@ const handleOpen = function* handleOpen({ params }) {
yield navigate({ params });
} else {
// if deep link is from a different server
const result = yield RocketChat.testServer(server);
const result = yield RocketChat.getServerInfo(server);
if (!result.success) {
return;
}

View File

@ -9,6 +9,7 @@ import { APP } from '../actions/actionsTypes';
import RocketChat from '../lib/rocketchat';
import log from '../utils/log';
import Navigation from '../lib/Navigation';
import database from '../lib/realm';
const restore = function* restore() {
try {
@ -27,7 +28,8 @@ const restore = function* restore() {
]);
yield put(actions.appStart('outside'));
} else if (server) {
yield put(selectServerRequest(server));
const serverObj = database.databases.serversDB.objectForPrimaryKey('servers', server);
yield put(selectServerRequest(server, serverObj && serverObj.version));
}
yield put(actions.appReady({}));

View File

@ -4,8 +4,6 @@ import { takeLatest, put, call } from 'redux-saga/effects';
import Navigation from '../lib/Navigation';
import { MESSAGES } from '../actions/actionsTypes';
import {
messagesSuccess,
messagesFailure,
deleteSuccess,
deleteFailure,
editSuccess,
@ -25,19 +23,6 @@ const editMessage = message => RocketChat.editMessage(message);
const toggleStarMessage = message => RocketChat.toggleStarMessage(message);
const togglePinMessage = message => RocketChat.togglePinMessage(message);
const get = function* get({ room }) {
try {
if (room.lastOpen) {
yield RocketChat.loadMissedMessages(room);
} else {
yield RocketChat.loadMessagesForRoom(room);
}
yield put(messagesSuccess());
} catch (err) {
yield put(messagesFailure(err));
}
};
const handleDeleteRequest = function* handleDeleteRequest({ message }) {
try {
yield call(deleteMessage, message);
@ -97,7 +82,6 @@ const handleReplyBroadcast = function* handleReplyBroadcast({ message }) {
};
const root = function* root() {
yield takeLatest(MESSAGES.REQUEST, get);
yield takeLatest(MESSAGES.DELETE_REQUEST, handleDeleteRequest);
yield takeLatest(MESSAGES.EDIT_REQUEST, handleEditRequest);
yield takeLatest(MESSAGES.TOGGLE_STAR_REQUEST, handleToggleStarRequest);

View File

@ -12,8 +12,31 @@ import database from '../lib/realm';
import log from '../utils/log';
import I18n from '../i18n';
const handleSelectServer = function* handleSelectServer({ server }) {
const getServerInfo = function* getServerInfo({ server }) {
try {
const serverInfo = yield RocketChat.getServerInfo(server);
if (!serverInfo.success) {
Alert.alert(I18n.t('Oops'), I18n.t(serverInfo.message, serverInfo.messageOptions));
yield put(serverFailure());
return;
}
database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server, version: serverInfo.version }, true);
});
return serverInfo;
} catch (e) {
log('getServerInfo', e);
}
};
const handleSelectServer = function* handleSelectServer({ server, version, fetchVersion }) {
try {
let serverInfo;
if (fetchVersion) {
serverInfo = yield getServerInfo({ server });
}
yield AsyncStorage.setItem('currentServer', server);
const userStringified = yield AsyncStorage.getItem(`${ RocketChat.TOKEN_KEY }-${ server }`);
@ -37,7 +60,7 @@ const handleSelectServer = function* handleSelectServer({ server }) {
return result;
}, {})));
yield put(selectServerSuccess(server));
yield put(selectServerSuccess(server, fetchVersion ? serverInfo && serverInfo.version : version));
} catch (e) {
log('handleSelectServer', e);
}
@ -45,13 +68,9 @@ const handleSelectServer = function* handleSelectServer({ server }) {
const handleServerRequest = function* handleServerRequest({ server }) {
try {
const result = yield RocketChat.testServer(server);
if (!result.success) {
Alert.alert(I18n.t('Oops'), I18n.t(result.message, result.messageOptions));
yield put(serverFailure());
return;
}
const serverInfo = yield getServerInfo({ server });
// TODO: cai aqui O.o
const loginServicesLength = yield RocketChat.getLoginServices(server);
if (loginServicesLength === 0) {
Navigation.navigate('LoginView');
@ -59,10 +78,7 @@ const handleServerRequest = function* handleServerRequest({ server }) {
Navigation.navigate('LoginSignupView');
}
database.databases.serversDB.write(() => {
database.databases.serversDB.create('servers', { id: server }, true);
});
yield put(selectServerRequest(server));
yield put(selectServerRequest(server, serverInfo.version, false));
} catch (e) {
yield put(serverFailure());
log('handleServerRequest', e);

View File

@ -21,6 +21,8 @@ import protectedFunction from '../../lib/methods/helpers/protectedFunction';
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import StatusBar from '../../containers/StatusBar';
const PAGE_SIZE = 25;
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
user: {
@ -62,19 +64,20 @@ export default class RoomMembersView extends LoggedView {
this.CANCEL_INDEX = 0;
this.MUTE_INDEX = 1;
this.actionSheetOptions = [''];
const { rid, members } = props.navigation.state.params;
const { rid } = props.navigation.state.params;
this.rooms = database.objects('subscriptions').filtered('rid = $0', rid);
this.permissions = RocketChat.hasPermission(['mute-user'], rid);
this.state = {
isLoading: true,
isLoading: false,
allUsers: false,
filtering: false,
rid,
members,
members: [],
membersFiltered: [],
userLongPressed: {},
room: this.rooms[0] || {},
options: []
options: [],
end: false
};
}
@ -170,7 +173,9 @@ export default class RoomMembersView extends LoggedView {
toggleStatus = () => {
try {
const { allUsers } = this.state;
this.fetchMembers(!allUsers);
this.setState({ members: [], allUsers: !allUsers, end: false }, () => {
this.fetchMembers();
});
} catch (e) {
log('RoomMembers.toggleStatus', e);
}
@ -186,15 +191,26 @@ export default class RoomMembersView extends LoggedView {
});
}
fetchMembers = async(status) => {
this.setState({ isLoading: true });
const { rid } = this.state;
// eslint-disable-next-line react/sort-comp
fetchMembers = async() => {
const {
rid, members, isLoading, allUsers, end
} = this.state;
const { navigation } = this.props;
if (isLoading || end) {
return;
}
this.setState({ isLoading: true });
try {
const membersResult = await RocketChat.getRoomMembers(rid, status);
const members = membersResult.records;
this.setState({ allUsers: status, members, isLoading: false });
navigation.setParams({ allUsers: status });
const membersResult = await RocketChat.getRoomMembers(rid, allUsers, members.length, PAGE_SIZE);
const newMembers = membersResult.records;
this.setState({
members: members.concat(newMembers || []),
isLoading: false,
end: newMembers.length < PAGE_SIZE
});
navigation.setParams({ allUsers });
} catch (error) {
console.log('TCL: fetchMembers -> error', error);
this.setState({ isLoading: false });
@ -260,9 +276,9 @@ export default class RoomMembersView extends LoggedView {
const {
filtering, members, membersFiltered, isLoading
} = this.state;
if (isLoading) {
return <ActivityIndicator style={styles.loading} />;
}
// if (isLoading) {
// return <ActivityIndicator style={styles.loading} />;
// }
return (
<SafeAreaView style={styles.list} testID='room-members-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
@ -273,6 +289,16 @@ export default class RoomMembersView extends LoggedView {
keyExtractor={item => item._id}
ItemSeparatorComponent={this.renderSeparator}
ListHeaderComponent={this.renderSearchBar}
ListFooterComponent={() => {
if (isLoading) {
return <ActivityIndicator style={styles.loading} />;
}
return null;
}}
onEndReachedThreshold={0.1}
onEndReached={this.fetchMembers}
maxToRenderPerBatch={5}
windowSize={10}
{...scrollPersistTaps}
/>
</SafeAreaView>

View File

@ -3,23 +3,26 @@ import PropTypes from 'prop-types';
import {
View, Text, StyleSheet, ScrollView
} from 'react-native';
import { emojify } from 'react-emojione';
import I18n from '../../../i18n';
import sharedStyles from '../../Styles';
import { isIOS } from '../../../utils/deviceInfo';
import { isIOS, isAndroid } 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'
},
threadContainer: {
marginRight: isAndroid ? 20 : undefined
},
title: {
...sharedStyles.textSemibold,
color: HEADER_TITLE,
@ -62,7 +65,7 @@ Typing.propTypes = {
};
const Header = React.memo(({
prid, title, type, status, usersTyping, width, height
title, type, status, usersTyping, width, height, prid, tmid, widthOffset
}) => {
const portrait = height > width;
let scale = 1;
@ -72,9 +75,13 @@ const Header = React.memo(({
scale = 0.8;
}
}
if (title) {
title = emojify(title, { output: 'unicode' });
}
return (
<View style={styles.container}>
<View style={styles.titleContainer}>
<View style={[styles.container, { width: width - widthOffset }]}>
<View style={[styles.titleContainer, tmid && styles.threadContainer]}>
<ScrollView
showsHorizontalScrollIndicator={false}
horizontal
@ -82,10 +89,10 @@ const Header = React.memo(({
contentContainerStyle={styles.scroll}
>
<Icon type={prid ? 'discussion' : type} status={status} />
<Text style={[styles.title, { fontSize: TITLE_SIZE * scale }]} numberOfLines={1}>{title}</Text>
<Text style={[styles.title, { fontSize: TITLE_SIZE * scale }]} numberOfLines={1} testID={`room-view-title-${ title }`}>{title}</Text>
</ScrollView>
</View>
<Typing usersTyping={usersTyping} />
{type === 'thread' ? null : <Typing usersTyping={usersTyping} />}
</View>
);
});
@ -96,8 +103,10 @@ Header.propTypes = {
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
prid: PropTypes.string,
tmid: PropTypes.string,
status: PropTypes.string,
usersTyping: PropTypes.array
usersTyping: PropTypes.array,
widthOffset: PropTypes.number
};
Header.defaultProps = {

View File

@ -30,6 +30,8 @@ const Icon = React.memo(({ type, status }) => {
let icon;
if (type === 'discussion') {
icon = 'chat';
} else if (type === 'thread') {
icon = 'thread';
} else if (type === 'c') {
icon = 'hashtag';
} else {

View File

@ -0,0 +1,114 @@
import React from 'react';
import { StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { CustomHeaderButtons, Item } from '../../../containers/HeaderButton';
import database, { safeAddListener } from '../../../lib/realm';
import RocketChat from '../../../lib/rocketchat';
import log from '../../../utils/log';
const styles = StyleSheet.create({
more: {
marginHorizontal: 0, marginLeft: 0, marginRight: 5
},
thread: {
marginHorizontal: 0, marginLeft: 0, marginRight: 10
}
});
@connect(state => ({
userId: state.login.user && state.login.user.id,
threadsEnabled: state.settings.Threads_enabled
}))
class RightButtonsContainer extends React.PureComponent {
static propTypes = {
userId: PropTypes.string,
threadsEnabled: PropTypes.bool,
rid: PropTypes.string,
t: PropTypes.string,
tmid: PropTypes.string,
navigation: PropTypes.object
};
constructor(props) {
super(props);
if (props.tmid) {
this.thread = database.objectForPrimaryKey('messages', props.tmid);
safeAddListener(this.thread, this.updateThread);
}
this.state = {
isFollowingThread: true
};
}
updateThread = () => {
const { userId } = this.props;
this.setState({
isFollowingThread: this.thread.replies && !!this.thread.replies.find(t => t === userId)
});
}
goThreadsView = () => {
const { rid, t, navigation } = this.props;
navigation.navigate('ThreadMessagesView', { rid, t });
}
goRoomActionsView = () => {
const { rid, t, navigation } = this.props;
navigation.navigate('RoomActionsView', { rid, t });
}
toggleFollowThread = async() => {
const { isFollowingThread } = this.state;
const { tmid } = this.props;
try {
await RocketChat.toggleFollowMessage(tmid, !isFollowingThread);
} catch (e) {
console.log('TCL: RightButtonsContainer -> toggleFollowThread -> e', e);
log('toggleFollowThread', e);
}
}
render() {
const { isFollowingThread } = this.state;
const { t, tmid, threadsEnabled } = this.props;
if (t === 'l') {
return null;
}
if (tmid) {
return (
<CustomHeaderButtons>
<Item
title='bell'
iconName={isFollowingThread ? 'Bell-off' : 'bell'}
onPress={this.toggleFollowThread}
testID={isFollowingThread ? 'room-view-header-unfollow' : 'room-view-header-follow'}
/>
</CustomHeaderButtons>
);
}
return (
<CustomHeaderButtons>
{threadsEnabled ? (
<Item
title='thread'
iconName='thread'
onPress={this.goThreadsView}
testID='room-view-header-threads'
buttonStyle={styles.thread}
/>
) : null}
<Item
title='more'
iconName='menu'
onPress={this.goRoomActionsView}
testID='room-view-header-actions'
buttonStyle={styles.more}
/>
</CustomHeaderButtons>
);
}
}
export default RightButtonsContainer;

View File

@ -6,6 +6,7 @@ import equal from 'deep-equal';
import database from '../../../lib/realm';
import Header from './Header';
import RightButtons from './RightButtons';
@responsive
@connect((state, ownProps) => {
@ -33,9 +34,11 @@ export default class RoomHeaderView extends Component {
title: PropTypes.string,
type: PropTypes.string,
prid: PropTypes.string,
tmid: PropTypes.string,
rid: PropTypes.string,
window: PropTypes.object,
status: PropTypes.string
status: PropTypes.string,
widthOffset: PropTypes.number
};
constructor(props) {
@ -89,19 +92,23 @@ export default class RoomHeaderView extends Component {
render() {
const { usersTyping } = this.state;
const {
window, title, type, status, prid
window, title, type, status, prid, tmid, widthOffset
} = this.props;
return (
<Header
prid={prid}
tmid={tmid}
title={title}
type={type}
status={status}
width={window.width}
height={window.height}
usersTyping={usersTyping}
widthOffset={widthOffset}
/>
);
}
}
export { RightButtons };

View File

@ -1,125 +1,137 @@
import React from 'react';
import { ActivityIndicator, FlatList, InteractionManager } from 'react-native';
import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import debounce from 'lodash/debounce';
import styles from './styles';
import database, { safeAddListener } from '../../lib/realm';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import debounce from '../../utils/debounce';
import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
import EmptyRoom from './EmptyRoom';
// import ScrollBottomButton from './ScrollBottomButton';
export class List extends React.Component {
export class List extends React.PureComponent {
static propTypes = {
onEndReached: PropTypes.func,
renderFooter: PropTypes.func,
renderRow: PropTypes.func,
rid: PropTypes.string,
t: PropTypes.string,
window: PropTypes.object
tmid: PropTypes.string
};
constructor(props) {
super(props);
console.time(`${ this.constructor.name } init`);
console.time(`${ this.constructor.name } mount`);
if (props.tmid) {
this.data = database
.objects('threadMessages')
.filtered('rid = $0', props.tmid)
.sorted('ts', true);
this.threads = [];
} else {
this.data = database
.objects('messages')
.filtered('rid = $0', props.rid)
.sorted('ts', true);
this.threads = database.objects('threads').filtered('rid = $0', props.rid);
}
this.state = {
loading: true,
loadingMore: false,
end: false,
messages: this.data.slice()
// showScollToBottomButton: false
messages: this.data.slice(),
threads: this.threads.slice()
};
safeAddListener(this.data, this.updateState);
console.timeEnd(`${ this.constructor.name } init`);
}
// shouldComponentUpdate(nextProps, nextState) {
// const {
// loadingMore, loading, end, showScollToBottomButton, messages
// } = this.state;
// const { window } = this.props;
// return end !== nextState.end
// || loadingMore !== nextState.loadingMore
// || loading !== nextState.loading
// || showScollToBottomButton !== nextState.showScollToBottomButton
// // || messages.length !== nextState.messages.length
// || !equal(messages, nextState.messages)
// || window.width !== nextProps.window.width;
// }
componentDidMount() {
console.timeEnd(`${ this.constructor.name } mount`);
}
componentWillUnmount() {
this.data.removeAllListeners();
this.threads.removeAllListeners();
if (this.updateState && this.updateState.stop) {
this.updateState.stop();
}
if (this.interactionManager && this.interactionManager.cancel) {
this.interactionManager.cancel();
if (this.updateThreads && this.updateThreads.stop) {
this.updateThreads.stop();
}
if (this.interactionManagerState && this.interactionManagerState.cancel) {
this.interactionManagerState.cancel();
}
if (this.interactionManagerThreads && this.interactionManagerThreads.cancel) {
this.interactionManagerThreads.cancel();
}
console.countReset(`${ this.constructor.name }.render calls`);
}
// eslint-disable-next-line react/sort-comp
updateState = debounce(() => {
this.interactionManager = InteractionManager.runAfterInteractions(() => {
this.setState({ messages: this.data.slice(), loading: false, loadingMore: false });
this.interactionManagerState = InteractionManager.runAfterInteractions(() => {
this.setState({
messages: this.data.slice(),
threads: this.threads.slice(),
loading: false
});
}, 300);
});
}, 300, { leading: true });
onEndReached = async() => {
const {
loadingMore, loading, end, messages
loading, end, messages
} = this.state;
if (loadingMore || loading || end || messages.length < 50) {
if (loading || end || messages.length < 50) {
return;
}
this.setState({ loadingMore: true });
const { rid, t } = this.props;
this.setState({ loading: true });
const { rid, t, tmid } = this.props;
try {
const result = await RocketChat.loadMessagesForRoom({ rid, t, latest: this.data[this.data.length - 1].ts });
let result;
if (tmid) {
result = await RocketChat.loadThreadMessages({ tmid, skip: messages.length });
} else {
result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts });
}
this.setState({ end: result.length < 50 });
} catch (e) {
this.setState({ loadingMore: false });
this.setState({ loading: false });
log('ListView.onEndReached', e);
}
}
// 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 });
// }
// }
renderFooter = () => {
const { loadingMore, loading } = this.state;
if (loadingMore || loading) {
return <ActivityIndicator style={styles.loadingMore} />;
const { loading } = this.state;
if (loading) {
return <ActivityIndicator style={styles.loading} />;
}
return null;
}
renderItem = ({ item, index }) => {
const { messages, threads } = this.state;
const { renderRow } = this.props;
if (item.tmid) {
const thread = threads.find(t => t._id === item.tmid);
if (thread) {
let tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
tmsg = emojify(tmsg, { output: 'unicode' });
item = { ...item, tmsg };
}
}
return renderRow(item, messages[index + 1]);
}
render() {
console.count(`${ this.constructor.name }.render calls`);
const { renderRow } = this.props;
const { messages } = this.state;
return (
<React.Fragment>
@ -130,10 +142,9 @@ export class List extends React.Component {
keyExtractor={item => item._id}
data={messages}
extraData={this.state}
renderItem={({ item, index }) => renderRow(item, messages[index + 1])}
renderItem={this.renderItem}
contentContainerStyle={styles.contentContainer}
style={styles.list}
// onScroll={this.handleScroll}
inverted
removeClippedSubviews
initialNumToRender={1}
@ -144,11 +155,6 @@ export class List extends React.Component {
ListFooterComponent={this.renderFooter}
{...scrollPersistTaps}
/>
{/* <ScrollBottomButton
show={showScollToBottomButton}
onPress={this.scrollToBottom}
landscape={window.width > window.height}
/> */}
</React.Fragment>
);
}

View File

@ -1,60 +0,0 @@
import React from 'react';
import { TouchableOpacity, StyleSheet } from 'react-native';
import PropTypes from 'prop-types';
import { isNotch } from '../../utils/deviceInfo';
import { CustomIcon } from '../../lib/Icons';
import { COLOR_BUTTON_PRIMARY } from '../../constants/colors';
const styles = StyleSheet.create({
button: {
position: 'absolute',
width: 42,
height: 42,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#EAF2FE',
borderRadius: 21,
shadowColor: '#000',
shadowOffset: {
width: 0,
height: 1
},
shadowOpacity: 0.20,
shadowRadius: 1.41,
elevation: 2
}
});
let right;
let bottom = 80;
if (isNotch) {
bottom = 120;
}
const ScrollBottomButton = React.memo(({ show, onPress, landscape }) => {
if (show) {
if (landscape) {
right = 45;
} else {
right = 30;
}
return (
<TouchableOpacity
activeOpacity={0.8}
style={[styles.button, { right, bottom }]}
onPress={onPress}
>
<CustomIcon name='arrow-down' color={COLOR_BUTTON_PRIMARY} size={30} />
</TouchableOpacity>
);
}
return null;
});
ScrollBottomButton.propTypes = {
show: PropTypes.bool.isRequired,
onPress: PropTypes.func.isRequired,
landscape: PropTypes.bool
};
export default ScrollBottomButton;

View File

@ -9,11 +9,11 @@ import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import moment from 'moment';
import 'react-native-console-time-polyfill';
import EJSON from 'ejson';
import {
toggleReactionPicker as toggleReactionPickerAction,
actionsShow as actionsShowAction,
messagesRequest as messagesRequestAction,
editCancel as editCancelAction,
replyCancel as replyCancelAction
} from '../../actions/messages';
@ -30,14 +30,15 @@ import UploadProgress from './UploadProgress';
import styles from './styles';
import log from '../../utils/log';
import { isIOS } from '../../utils/deviceInfo';
import EventEmitter from '../../utils/events';
import I18n from '../../i18n';
import ConnectionBadge from '../../containers/ConnectionBadge';
import { CustomHeaderButtons, Item } from '../../containers/HeaderButton';
import RoomHeaderView from './Header';
import RoomHeaderView, { RightButtons } from './Header';
import StatusBar from '../../containers/StatusBar';
import Separator from './Separator';
import { COLOR_WHITE } from '../../constants/colors';
import debounce from '../../utils/debounce';
import buildMessage from '../../lib/methods/helpers/buildMessage';
@connect(state => ({
user: {
@ -49,13 +50,13 @@ import debounce from '../../utils/debounce';
showActions: state.messages.showActions,
showErrorActions: state.messages.showErrorActions,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
useRealName: state.settings.UI_Use_Real_Name
useRealName: state.settings.UI_Use_Real_Name,
isAuthenticated: state.login.isAuthenticated
}), dispatch => ({
editCancel: () => dispatch(editCancelAction()),
replyCancel: () => dispatch(replyCancelAction()),
toggleReactionPicker: message => dispatch(toggleReactionPickerAction(message)),
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage)),
messagesRequest: room => dispatch(messagesRequestAction(room))
actionsShow: actionMessage => dispatch(actionsShowAction(actionMessage))
}))
/** @extends React.Component */
export default class RoomView extends LoggedView {
@ -64,15 +65,13 @@ export default class RoomView extends LoggedView {
const prid = navigation.getParam('prid');
const title = navigation.getParam('name');
const t = navigation.getParam('t');
const tmid = navigation.getParam('tmid');
return {
headerTitle: <RoomHeaderView rid={rid} prid={prid} title={title} type={t} />,
headerRight: t === 'l'
? null
: (
<CustomHeaderButtons>
<Item title='more' iconName='menu' onPress={() => navigation.navigate('RoomActionsView', { rid, t })} testID='room-view-header-actions' />
</CustomHeaderButtons>
)
headerTitleContainerStyle: styles.headerTitleContainerStyle,
headerTitle: (
<RoomHeaderView rid={rid} prid={prid} tmid={tmid} title={title} type={t} widthOffset={tmid ? 95 : 130} />
),
headerRight: <RightButtons rid={rid} tmid={tmid} t={t} navigation={navigation} />
};
}
@ -88,9 +87,9 @@ export default class RoomView extends LoggedView {
actionMessage: PropTypes.object,
appState: PropTypes.string,
useRealName: PropTypes.bool,
isAuthenticated: PropTypes.bool,
toggleReactionPicker: PropTypes.func.isRequired,
actionsShow: PropTypes.func,
messagesRequest: PropTypes.func,
editCancel: PropTypes.func,
replyCancel: PropTypes.func
};
@ -101,6 +100,7 @@ export default class RoomView extends LoggedView {
console.time(`${ this.constructor.name } mount`);
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.tmid = props.navigation.getParam('tmid');
this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.state = {
joined: this.rooms.length > 0,
@ -110,38 +110,37 @@ export default class RoomView extends LoggedView {
this.beginAnimating = false;
this.beginAnimatingTimeout = setTimeout(() => this.beginAnimating = true, 300);
this.messagebox = React.createRef();
safeAddListener(this.rooms, this.updateRoom);
console.timeEnd(`${ this.constructor.name } init`);
}
componentDidMount() {
this.didMountInteraction = InteractionManager.runAfterInteractions(async() => {
this.didMountInteraction = InteractionManager.runAfterInteractions(() => {
const { room } = this.state;
const { messagesRequest, navigation } = this.props;
messagesRequest(room);
const { navigation, isAuthenticated } = this.props;
// if room is joined
if (room._id) {
if (room._id && !this.tmid) {
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);
}
if (isAuthenticated) {
this.init();
} else {
this.setLastOpen(null);
EventEmitter.addEventListener('connected', this.handleConnected);
}
}
safeAddListener(this.rooms, this.updateRoom);
});
console.timeEnd(`${ this.constructor.name } mount`);
}
shouldComponentUpdate(nextProps, nextState) {
const {
room, joined
room, joined, lastOpen
} = this.state;
const { showActions, showErrorActions, appState } = this.props;
if (room.ro !== nextState.room.ro) {
if (lastOpen !== nextState.lastOpen) {
return true;
} else if (room.ro !== nextState.room.ro) {
return true;
} else if (room.f !== nextState.room.f) {
return true;
@ -180,11 +179,13 @@ export default class RoomView extends LoggedView {
componentWillUnmount() {
if (this.messagebox && this.messagebox.current && this.messagebox.current.text) {
const { text } = this.messagebox.current;
database.write(() => {
const [room] = this.rooms;
if (room) {
database.write(() => {
room.draftMessage = text;
});
}
}
this.rooms.removeAllListeners();
if (this.sub && this.sub.stop) {
this.sub.stop();
@ -204,9 +205,41 @@ export default class RoomView extends LoggedView {
if (this.updateStateInteraction && this.updateStateInteraction.cancel) {
this.updateStateInteraction.cancel();
}
if (this.initInteraction && this.initInteraction.cancel) {
this.initInteraction.cancel();
}
EventEmitter.removeListener('connected', this.handleConnected);
console.countReset(`${ this.constructor.name }.render calls`);
}
// eslint-disable-next-line react/sort-comp
init = () => {
try {
this.initInteraction = InteractionManager.runAfterInteractions(async() => {
const { room } = this.state;
if (this.tmid) {
RocketChat.loadThreadMessages({ tmid: this.tmid, t: this.t });
} else {
await this.getMessages(room);
// if room is joined
if (room._id) {
if (room.alert || room.unread || room.userMentions) {
this.setLastOpen(room.ls);
} else {
this.setLastOpen(null);
}
RocketChat.readMessages(room.rid).catch(e => console.log(e));
this.sub = await RocketChat.subscribeRoom(room);
}
}
});
} catch (e) {
console.log('TCL: init -> e', e);
log('RoomView.init', e);
}
}
onMessageLongPress = (message) => {
const { actionsShow } = this.props;
actionsShow(message);
@ -232,6 +265,11 @@ export default class RoomView extends LoggedView {
});
}, 1000, true)
handleConnected = () => {
this.init();
EventEmitter.removeListener('connected', this.handleConnected);
}
internalSetState = (...args) => {
if (isIOS && this.beginAnimating) {
LayoutAnimation.easeInEaseOut();
@ -241,14 +279,16 @@ export default class RoomView extends LoggedView {
updateRoom = () => {
this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
if (this.rooms[0]) {
const room = JSON.parse(JSON.stringify(this.rooms[0] || {}));
this.internalSetState({ room });
}
});
}
sendMessage = (message) => {
sendMessage = (message, tmid) => {
LayoutAnimation.easeInEaseOut();
RocketChat.sendMessage(this.rid, message).then(() => {
RocketChat.sendMessage(this.rid, message, this.tmid || tmid).then(() => {
this.setLastOpen(null);
});
};
@ -258,6 +298,20 @@ export default class RoomView extends LoggedView {
return ((room.prid || useRealName) && room.fname) || room.name;
}
getMessages = () => {
const { room } = this.state;
try {
if (room.lastOpen) {
return RocketChat.loadMissedMessages(room);
} else {
return RocketChat.loadMessagesForRoom(room);
}
} catch (e) {
console.log('TCL: getMessages -> e', e);
log('getMessages', e);
}
}
setLastOpen = lastOpen => this.setState({ lastOpen });
joinRoom = async() => {
@ -301,9 +355,22 @@ export default class RoomView extends LoggedView {
return false;
}
// eslint-disable-next-line react/sort-comp
fetchThreadName = async(tmid) => {
try {
// TODO: we should build a tmid queue here in order to search for a single tmid only once
const thread = await RocketChat.getSingleMessage(tmid);
database.write(() => {
database.create('threads', buildMessage(EJSON.fromJSONValue(thread)), true);
});
} catch (error) {
console.log('TCL: fetchThreadName -> error', error);
}
}
renderItem = (item, previousItem) => {
const { room, lastOpen } = this.state;
const { user } = this.props;
const { user, navigation } = this.props;
let dateSeparator = null;
let showUnreadSeparator = false;
@ -319,23 +386,28 @@ export default class RoomView extends LoggedView {
}
}
if (showUnreadSeparator || dateSeparator) {
return (
<React.Fragment>
const message = (
<Message
key={item._id}
item={item}
status={item.status}
reactions={JSON.parse(JSON.stringify(item.reactions))}
user={user}
archived={room.archived}
broadcast={room.broadcast}
previousItem={previousItem}
status={item.status}
_updatedAt={item._updatedAt}
previousItem={previousItem}
navigation={navigation}
fetchThreadName={this.fetchThreadName}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
onDiscussionPress={this.onDiscussionPress}
/>
);
if (showUnreadSeparator || dateSeparator) {
return (
<React.Fragment>
{message}
<Separator
ts={dateSeparator}
unread={showUnreadSeparator}
@ -344,28 +416,13 @@ export default class RoomView extends LoggedView {
);
}
return (
<Message
key={item._id}
item={item}
status={item.status}
reactions={JSON.parse(JSON.stringify(item.reactions))}
user={user}
archived={room.archived}
broadcast={room.broadcast}
previousItem={previousItem}
_updatedAt={item._updatedAt}
onReactionPress={this.onReactionPress}
onLongPress={this.onMessageLongPress}
onDiscussionPress={this.onDiscussionPress}
/>
);
return message;
}
renderFooter = () => {
const { joined, room } = this.state;
if (!joined) {
if (!joined && !this.tmid) {
return (
<View style={styles.joinRoomContainer} key='room-view-join' testID='room-view-join'>
<Text style={styles.previewMode}>{I18n.t('You_are_in_preview_mode')}</Text>
@ -397,13 +454,21 @@ export default class RoomView extends LoggedView {
return <MessageBox ref={this.messagebox} onSubmit={this.sendMessage} rid={this.rid} roomType={room.t} />;
};
renderList = () => {
renderActions = () => {
const { room } = this.state;
const { rid, t } = room;
const {
user, showActions, showErrorActions, navigation
} = this.props;
if (!navigation.isFocused()) {
return null;
}
return (
<React.Fragment>
<List rid={rid} t={t} renderRow={this.renderItem} />
{this.renderFooter()}
{room._id && showActions
? <MessageActions room={room} user={user} />
: null
}
{showErrorActions ? <MessageErrorActions /> : null}
</React.Fragment>
);
}
@ -411,17 +476,14 @@ export default class RoomView extends LoggedView {
render() {
console.count(`${ this.constructor.name }.render calls`);
const { room } = this.state;
const { user, showActions, showErrorActions } = this.props;
const { rid, t } = room;
return (
<SafeAreaView style={styles.container} testID='room-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
{this.renderList()}
{room._id && showActions
? <MessageActions room={room} user={user} />
: null
}
{showErrorActions ? <MessageErrorActions /> : null}
<List rid={rid} t={t} tmid={this.tmid} renderRow={this.renderItem} />
{this.renderFooter()}
{this.renderActions()}
<ReactionPicker onEmojiSelected={this.onReactionPress} />
<UploadProgress rid={this.rid} />
<ConnectionBadge />

View File

@ -1,8 +1,9 @@
import { StyleSheet } from 'react-native';
import {
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT_DESCRIPTION
} from '../../constants/colors';
import { isIOS } from '../../utils/deviceInfo';
import sharedStyles from '../Styles';
export default StyleSheet.create({
@ -23,8 +24,8 @@ export default StyleSheet.create({
height: 1,
backgroundColor: COLOR_SEPARATOR
},
loadingMore: {
textAlign: 'center',
loading: {
flex: 1,
padding: 15,
color: COLOR_TEXT_DESCRIPTION
},
@ -40,9 +41,6 @@ export default StyleSheet.create({
borderRadius: 4,
flexDirection: 'column'
},
loading: {
flex: 1
},
joinRoomContainer: {
justifyContent: 'flex-end',
alignItems: 'center',
@ -67,5 +65,9 @@ export default StyleSheet.create({
fontSize: 16,
...sharedStyles.textMedium,
...sharedStyles.textColorNormal
},
headerTitleContainerStyle: {
justifyContent: 'flex-start',
left: isIOS ? 40 : 50
}
});

View File

@ -264,10 +264,11 @@ export default class RoomsListView extends LoggedView {
} = this.props;
if (server && this.hasActiveDB()) {
this.data = database.objects('subscriptions').filtered('archived != true && open == true && t != $0', 'l');
if (sortBy === 'alphabetical') {
this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('name', false);
this.data = this.data.sorted('name', false);
} else {
this.data = database.objects('subscriptions').filtered('archived != true && open == true').sorted('roomUpdatedAt', true);
this.data = this.data.sorted('roomUpdatedAt', true);
}
let chats = [];
@ -281,7 +282,7 @@ export default class RoomsListView extends LoggedView {
// unread
if (showUnread) {
this.unread = this.data.filtered('archived != true && open == true').filtered('(unread > 0 || alert == true)');
this.unread = this.data.filtered('(unread > 0 || alert == true)');
unread = this.removeRealmInstance(this.unread);
safeAddListener(this.unread, debounce(() => this.internalSetState({ unread: this.removeRealmInstance(this.unread) }), 300));
} else {

View File

@ -0,0 +1,180 @@
import React from 'react';
import PropTypes from 'prop-types';
import {
FlatList, View, Text, InteractionManager
} from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import EJSON from 'ejson';
import moment from 'moment';
import LoggedView from '../View';
import styles from './styles';
import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import database, { safeAddListener } from '../../lib/realm';
import StatusBar from '../../containers/StatusBar';
import buildMessage from '../../lib/methods/helpers/buildMessage';
import log from '../../utils/log';
import debounce from '../../utils/debounce';
const Separator = React.memo(() => <View style={styles.separator} />);
@connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
customEmojis: state.customEmojis,
user: {
id: state.login.user && state.login.user.id,
username: state.login.user && state.login.user.username,
token: state.login.user && state.login.user.token
}
}))
/** @extends React.Component */
export default class ThreadMessagesView extends LoggedView {
static navigationOptions = {
title: I18n.t('Threads')
}
static propTypes = {
user: PropTypes.object,
navigation: PropTypes.object
}
constructor(props) {
super('ThreadMessagesView', props);
this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t');
this.messages = database.objects('threads').filtered('rid = $0', this.rid);
safeAddListener(this.messages, this.updateMessages);
this.state = {
loading: false,
messages: this.messages.slice(),
end: false,
total: 0
};
}
componentDidMount() {
this.load();
}
shouldComponentUpdate(nextProps, nextState) {
const { loading, messages, end } = this.state;
if (nextState.loading !== loading) {
return true;
}
if (!equal(nextState.messages, messages)) {
return true;
}
if (!equal(nextState.end, end)) {
return true;
}
return false;
}
updateMessages = () => {
this.setState({ messages: this.messages.slice() });
}
// eslint-disable-next-line react/sort-comp
load = debounce(async() => {
const {
loading, end, total
} = this.state;
if (end || loading) {
return;
}
this.setState({ loading: true });
try {
const result = await RocketChat.getThreadsList({ rid: this.rid, limit: 50, skip: total });
database.write(() => result.forEach((message) => {
try {
database.create('threads', buildMessage(EJSON.fromJSONValue(message)), true);
} catch (e) {
log('ThreadMessagesView -> load -> create', e);
}
}));
InteractionManager.runAfterInteractions(() => {
this.setState(prevState => ({
loading: false,
end: result.length < 50,
total: prevState.total + result.length
}));
});
} catch (error) {
console.log('ThreadMessagesView -> catch -> error', error);
this.setState({ loading: false, end: true });
}
}, 300, true)
formatMessage = lm => (
lm ? moment(lm).calendar(null, {
lastDay: `[${ I18n.t('Yesterday') }]`,
sameDay: 'h:mm A',
lastWeek: 'dddd',
sameElse: 'MMM D'
}) : null
)
renderSeparator = () => <Separator />
renderEmpty = () => (
<View style={styles.listEmptyContainer} testID='thread-messages-view'>
<Text style={styles.noDataFound}>{I18n.t('No_thread_messages')}</Text>
</View>
)
renderItem = ({ item }) => {
const { user, navigation } = this.props;
return (
<Message
key={item._id}
item={item}
user={user}
archived={false}
broadcast={false}
status={item.status}
_updatedAt={item._updatedAt}
navigation={navigation}
customTimeFormat='MMM D'
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
fetchThreadName={this.fetchThreadName}
onDiscussionPress={this.onDiscussionPress}
/>
);
}
render() {
const { messages, loading } = this.state;
if (!loading && messages.length === 0) {
return this.renderEmpty();
}
return (
<SafeAreaView style={styles.list} testID='thread-messages-view' forceInset={{ bottom: 'never' }}>
<StatusBar />
<FlatList
data={messages}
renderItem={this.renderItem}
style={styles.list}
contentContainerStyle={styles.contentContainer}
keyExtractor={item => item._id}
onEndReached={this.load}
onEndReachedThreshold={0.5}
maxToRenderPerBatch={5}
initialNumToRender={1}
ItemSeparatorComponent={this.renderSeparator}
ListFooterComponent={loading ? <RCActivityIndicator /> : null}
/>
</SafeAreaView>
);
}
}

View File

@ -0,0 +1,32 @@
import { StyleSheet } from 'react-native';
import sharedStyles from '../Styles';
import { COLOR_WHITE, COLOR_SEPARATOR } from '../../constants/colors';
export default StyleSheet.create({
list: {
flex: 1,
backgroundColor: COLOR_WHITE
},
listEmptyContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: COLOR_WHITE
},
noDataFound: {
fontSize: 14,
...sharedStyles.textRegular,
...sharedStyles.textColorNormal
},
contentContainer: {
paddingBottom: 30
},
separator: {
height: StyleSheet.hairlineWidth,
width: '100%',
marginLeft: 60,
marginTop: 10,
backgroundColor: COLOR_SEPARATOR
}
});

View File

@ -21,6 +21,8 @@ async function navigateToRoom() {
}
describe('Room screen', () => {
const mainRoom = `private${ data.random }`;
before(async() => {
await navigateToRoom();
});
@ -28,6 +30,8 @@ describe('Room screen', () => {
describe('Render', async() => {
it('should have room screen', async() => {
await expect(element(by.id('room-view'))).toBeVisible();
await waitFor(element(by.id(`room-view-title-${ mainRoom }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ mainRoom }`))).toBeVisible();
});
it('should have messages list', async() => {
@ -228,17 +232,6 @@ describe('Room screen', () => {
await expect(element(by.id('message-reaction-:grinning:'))).toBeNotVisible();
});
it('should reply message', async() => {
await mockMessage('reply');
await element(by.text(`${ data.random }reply`)).longPress();
await waitFor(element(by.text('Message actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Message actions'))).toBeVisible();
await element(by.text('Reply')).tap();
await element(by.id('messagebox-input')).typeText('replied');
await element(by.id('messagebox-send-message')).tap();
// TODO: test if reply was sent
});
it('should edit message', async() => {
await mockMessage('edit');
await element(by.text(`${ data.random }edit`)).longPress();
@ -281,6 +274,67 @@ describe('Room screen', () => {
// TODO: delete message - swipe on action sheet missing
});
describe('Thread', async() => {
const thread = `${ data.random }thread`;
it('should create thread', async() => {
await mockMessage('thread');
await element(by.text(thread)).longPress();
await waitFor(element(by.text('Message actions'))).toBeVisible().withTimeout(5000);
await expect(element(by.text('Message actions'))).toBeVisible();
await element(by.text('Reply')).tap();
await element(by.id('messagebox-input')).typeText('replied');
await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.id(`message-thread-button-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`message-thread-button-${ thread }`))).toBeVisible();
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toBeVisible();
});
it('should navigate to thread from button', async() => {
await element(by.id(`message-thread-button-${ thread }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toBeVisible();
await tapBack();
});
it('should toggle follow thread', async() => {
await element(by.id(`message-thread-button-${ thread }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toBeVisible();
await element(by.id('room-view-header-unfollow')).tap();
await waitFor(element(by.id('room-view-header-follow'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view-header-follow'))).toBeVisible();
await element(by.id('room-view-header-follow')).tap();
await waitFor(element(by.id('room-view-header-unfollow'))).toBeVisible().withTimeout(60000);
await expect(element(by.id('room-view-header-unfollow'))).toBeVisible();
await tapBack();
});
it('should navigate to thread from thread name', async() => {
await element(by.id(`message-thread-replied-on-${ thread }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toBeVisible();
await tapBack();
});
it('should navigate to thread from threads view', async() => {
await element(by.id('room-view-header-threads')).tap();
await waitFor(element(by.id('thread-messages-view'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('thread-messages-view'))).toBeVisible();
await element(by.id(`message-thread-button-${ thread }`)).tap();
await waitFor(element(by.id('room-view'))).toBeVisible().withTimeout(5000);
await waitFor(element(by.id(`room-view-title-${ thread }`))).toBeVisible().withTimeout(5000);
await expect(element(by.id(`room-view-title-${ thread }`))).toBeVisible();
await tapBack();
await waitFor(element(by.id('thread-messages-view'))).toBeVisible().withTimeout(5000);
await expect(element(by.id('thread-messages-view'))).toBeVisible();
await tapBack();
});
});
afterEach(async() => {
takeScreenshot();
});

View File

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

View File

@ -67,7 +67,7 @@
"react-navigation-header-buttons": "^2.1.2",
"react-redux": "^6.0.0",
"reactotron-react-native": "2.2",
"realm": "2.24",
"realm": "2.26.1",
"redux": "^4.0.1",
"redux-enhancer-react-native-appstate": "^0.3.1",
"redux-immutable-state-invariant": "^2.1.0",

View File

@ -26,6 +26,7 @@ const author = {
const baseUrl = 'https://open.rocket.chat';
const customEmojis = { react_rocket: 'png', nyan_rocket: 'png', marioparty: 'gif' };
const date = new Date(2017, 10, 10, 10);
const longText = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
const Message = props => (
<MessageComponent
@ -50,7 +51,7 @@ export default (
<Message msg='Message' />
<Separator title='Long message' />
<Message msg='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum' />
<Message msg={longText} />
<Separator title='Grouped messages' />
<Message msg='...' />
@ -58,7 +59,7 @@ export default (
msg='Different user'
author={{
...author,
username: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
username: longText
}}
/>
<Message msg='This is the third message' header={false} />
@ -74,7 +75,7 @@ export default (
msg='Message'
author={{
...author,
username: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum'
username: longText
}}
alias='Diego Mello'
/>
@ -262,6 +263,7 @@ export default (
header={false}
/>
{/* Legacy thread */}
<Separator title='Message with reply' />
<Message
msg="I'm fine!"
@ -282,6 +284,127 @@ export default (
}]}
/>
<Separator title='Message with thread' />
<Message
msg='How are you?'
tcount={1}
tlm={date}
/>
<Message
msg="I'm fine!"
tmid='1'
tmsg='How are you?'
/>
<Message
msg="I'm fine!"
tmid='1'
tmsg='Thread with emoji :) :joy:'
/>
<Message
msg="I'm fine!"
tmid='1'
tmsg={longText}
/>
<Message
msg={longText}
tmid='1'
tmsg='How are you?'
/>
<Message
msg={longText}
tmid='1'
tmsg={longText}
/>
<Message
msg='How are you?'
tcount={0}
tlm={date}
/>
<Message
msg='How are you?'
tcount={9999}
tlm={date}
/>
{/* <Message
msg='How are you?'
tcount={9999}
tlm={moment().subtract(1, 'hour')}
/>
<Message
msg='How are you?'
tcount={9999}
tlm={moment().subtract(1, 'day')}
/>
<Message
msg='How are you?'
tcount={9999}
tlm={moment().subtract(5, 'day')}
/>
<Message
msg='How are you?'
tcount={9999}
tlm={moment().subtract(30, 'day')}
/> */}
<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={longText}
/>
<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='URL' />
<Message
urls={[{
@ -360,64 +483,6 @@ export default (
<Separator title='Broadcast' />
<Message msg='Broadcasted message' broadcast replyBroadcast={() => alert('broadcast!')} />
<Separator title='Discussion' />
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={null}
dlm={null}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1}
dlm={date}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={10}
dlm={date}
msg='Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={date}
msg='This is a discussion'
/>
{/* <Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(1, 'hour')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(1, 'day')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(5, 'day')}
msg='This is a discussion'
/>
<Message
type='discussion-created'
drid='aisduhasidhs'
dcount={1000}
dlm={moment().subtract(30, 'day')}
msg='This is a discussion'
/> */}
<Separator title='Archived' />
<Message msg='This message is inside an archived room' archived />

View File

@ -31,6 +31,9 @@ const Header = props => (
height={480}
{...props}
/>
<CustomHeaderButtons>
<Item title='thread' iconName='thread' />
</CustomHeaderButtons>
<CustomHeaderButtons>
<Item title='more' iconName='menu' />
</CustomHeaderButtons>
@ -47,6 +50,7 @@ export default (
<Header type='c' />
<Header type='p' />
<Header type='discussion' />
<Header type='thread' />
<StoriesSeparator title='Typing' />
<Header usersTyping={[{ username: 'diego.mello' }]} />

260
yarn.lock
View File

@ -1878,6 +1878,20 @@ arr-union@^3.1.0:
resolved "https://registry.yarnpkg.com/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4"
integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=
array-back@^1.0.3, array-back@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/array-back/-/array-back-1.0.4.tgz#644ba7f095f7ffcf7c43b5f0dc39d3c1f03c063b"
integrity sha1-ZEun8JX3/898Q7Xw3DnTwfA8Bjs=
dependencies:
typical "^2.6.0"
array-back@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/array-back/-/array-back-2.0.0.tgz#6877471d51ecc9c9bfa6136fb6c7d5fe69748022"
integrity sha512-eJv4pLLufP3g5kcZry0j6WXpIbzYw9GUB4mVJZno9wfwiBxbizTnHCw3VJb07cBihbFX48Y7oSrW9y+gt4glyw==
dependencies:
typical "^2.6.1"
array-equal@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
@ -3080,7 +3094,7 @@ binstring@^0.2.1:
resolved "https://registry.yarnpkg.com/binstring/-/binstring-0.2.1.tgz#8a174d301f6d54efda550dd98bb4cb524eacd75d"
integrity sha1-ihdNMB9tVO/aVQ3Zi7TLUk6s110=
bl@^1.2.1:
bl@^1.0.0, bl@^1.2.1:
version "1.2.2"
resolved "https://registry.yarnpkg.com/bl/-/bl-1.2.2.tgz#a160911717103c07410cef63ef51b397c025af9c"
integrity sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==
@ -3306,11 +3320,29 @@ bser@^2.0.0:
dependencies:
node-int64 "^0.4.0"
buffer-crc32@^0.2.13:
buffer-alloc-unsafe@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz#bd7dc26ae2972d0eda253be061dba992349c19f0"
integrity sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==
buffer-alloc@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/buffer-alloc/-/buffer-alloc-1.2.0.tgz#890dd90d923a873e08e10e5fd51a57e5b7cce0ec"
integrity sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==
dependencies:
buffer-alloc-unsafe "^1.1.0"
buffer-fill "^1.0.0"
buffer-crc32@^0.2.13, buffer-crc32@~0.2.3:
version "0.2.13"
resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242"
integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=
buffer-fill@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/buffer-fill/-/buffer-fill-1.0.0.tgz#f8f78b76789888ef39f205cd637f68e702122b2c"
integrity sha1-+PeLdniYiO858gXNY39o5wISKyw=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@ -3330,6 +3362,14 @@ buffer@^4.3.0:
ieee754 "^1.1.4"
isarray "^1.0.0"
buffer@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.2.1.tgz#dd57fa0f109ac59c602479044dca7b8b3d0b71d6"
integrity sha512-c+Ko0loDaFfuPWiL02ls9Xd3GO3cPVmUobQ6t3rXNUk304u6hGq+8N/kFi+QEIKhzK3uwolVhLzszmfLmMLnqg==
dependencies:
base64-js "^1.0.2"
ieee754 "^1.1.4"
builtin-modules@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-1.1.1.tgz#270f076c5a72c02f5b65a47df94c5fe3a278892f"
@ -3723,6 +3763,15 @@ combined-stream@^1.0.6, combined-stream@~1.0.6:
dependencies:
delayed-stream "~1.0.0"
command-line-args@^4.0.6:
version "4.0.7"
resolved "https://registry.yarnpkg.com/command-line-args/-/command-line-args-4.0.7.tgz#f8d1916ecb90e9e121eda6428e41300bfb64cc46"
integrity sha512-aUdPvQRAyBvQd2n7jXcsMDz68ckBJELXNzBybCHOibUWEg0mWTnaYCSRU8h9R+aNRSvDihJtssSRCiDRpLaezA==
dependencies:
array-back "^2.0.0"
find-replace "^1.0.3"
typical "^2.6.1"
commander@2.15.1:
version "2.15.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
@ -3743,6 +3792,13 @@ commander@~2.13.0:
resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c"
integrity sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==
commander@~2.8.1:
version "2.8.1"
resolved "https://registry.yarnpkg.com/commander/-/commander-2.8.1.tgz#06be367febfda0c330aa1e2a072d3dc9762425d4"
integrity sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=
dependencies:
graceful-readlink ">= 1.0.0"
commist@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/commist/-/commist-1.0.0.tgz#c0c352501cf6f52e9124e3ef89c9806e2022ebef"
@ -4201,6 +4257,59 @@ decode-uri-component@^0.2.0:
resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"
integrity sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=
decompress-tar@^4.0.0, decompress-tar@^4.1.0, decompress-tar@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-tar/-/decompress-tar-4.1.1.tgz#718cbd3fcb16209716e70a26b84e7ba4592e5af1"
integrity sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==
dependencies:
file-type "^5.2.0"
is-stream "^1.1.0"
tar-stream "^1.5.2"
decompress-tarbz2@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz#3082a5b880ea4043816349f378b56c516be1a39b"
integrity sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==
dependencies:
decompress-tar "^4.1.0"
file-type "^6.1.0"
is-stream "^1.1.0"
seek-bzip "^1.0.5"
unbzip2-stream "^1.0.9"
decompress-targz@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/decompress-targz/-/decompress-targz-4.1.1.tgz#c09bc35c4d11f3de09f2d2da53e9de23e7ce1eee"
integrity sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==
dependencies:
decompress-tar "^4.1.1"
file-type "^5.2.0"
is-stream "^1.1.0"
decompress-unzip@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/decompress-unzip/-/decompress-unzip-4.0.1.tgz#deaaccdfd14aeaf85578f733ae8210f9b4848f69"
integrity sha1-3qrM39FK6vhVePczroIQ+bSEj2k=
dependencies:
file-type "^3.8.0"
get-stream "^2.2.0"
pify "^2.3.0"
yauzl "^2.4.2"
decompress@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/decompress/-/decompress-4.2.0.tgz#7aedd85427e5a92dacfe55674a7c505e96d01f9d"
integrity sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=
dependencies:
decompress-tar "^4.0.0"
decompress-tarbz2 "^4.0.0"
decompress-targz "^4.0.0"
decompress-unzip "^4.0.1"
graceful-fs "^4.1.10"
make-dir "^1.0.0"
pify "^2.3.0"
strip-dirs "^2.0.0"
deep-equal@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5"
@ -5334,6 +5443,13 @@ fbjs@^1.0.0:
setimmediate "^1.0.5"
ua-parser-js "^0.7.18"
fd-slicer@~1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.1.0.tgz#25c7c89cb1f9077f8891bbe61d8f390eae256f1e"
integrity sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=
dependencies:
pend "~1.2.0"
figgy-pudding@^3.5.1:
version "3.5.1"
resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
@ -5379,6 +5495,21 @@ file-system-cache@^1.0.5:
fs-extra "^0.30.0"
ramda "^0.21.0"
file-type@^3.8.0:
version "3.9.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-3.9.0.tgz#257a078384d1db8087bc449d107d52a52672b9e9"
integrity sha1-JXoHg4TR24CHvESdEH1SpSZyuek=
file-type@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-5.2.0.tgz#2ddbea7c73ffe36368dfae49dc338c058c2b8ad6"
integrity sha1-LdvqfHP/42No365J3DOMBYwritY=
file-type@^6.1.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/file-type/-/file-type-6.2.0.tgz#e50cd75d356ffed4e306dc4f5bcf52a79903a919"
integrity sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==
file-uri-to-path@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd"
@ -5467,6 +5598,14 @@ find-cache-dir@^2.0.0:
make-dir "^1.0.0"
pkg-dir "^3.0.0"
find-replace@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/find-replace/-/find-replace-1.0.3.tgz#b88e7364d2d9c959559f388c66670d6130441fa0"
integrity sha1-uI5zZNLZyVlVnziMZmcNYTBEH6A=
dependencies:
array-back "^1.0.4"
test-value "^2.1.0"
find-up@3.0.0, find-up@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73"
@ -5577,6 +5716,11 @@ from2@^2.1.0:
inherits "^2.0.1"
readable-stream "^2.0.0"
fs-constants@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad"
integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==
fs-extra@^0.30.0:
version "0.30.0"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.30.0.tgz#f233ffcc08d4da7d432daa449776989db1df93f0"
@ -5597,7 +5741,7 @@ fs-extra@^1.0.0:
jsonfile "^2.1.0"
klaw "^1.0.0"
fs-extra@^4.0.2:
fs-extra@^4.0.2, fs-extra@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.3.tgz#0d852122e5bc5beb453fb028e9c0c9bf36340c94"
integrity sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==
@ -5714,6 +5858,14 @@ get-port@^2.1.0:
dependencies:
pinkie-promise "^2.0.0"
get-stream@^2.2.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-2.3.1.tgz#5f38f93f346009666ee0150a054167f91bdd95de"
integrity sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=
dependencies:
object-assign "^4.0.1"
pinkie-promise "^2.0.0"
get-stream@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14"
@ -5928,11 +6080,16 @@ got@^6.7.1:
unzip-response "^2.0.1"
url-parse-lax "^1.0.0"
graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
graceful-fs@^4.1.10, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.1.9:
version "4.1.15"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.15.tgz#ffb703e1066e8a0eeaa4c8b80ba9253eeefbfb00"
integrity sha512-6uHUhOPEBgQ24HM+r6b/QwWfZq+yiFcipKFrOFiBEnWdy5sdzYoi+pJeQaPI5qOLRFqWmAXUPQNsielzdLoecA==
"graceful-readlink@>= 1.0.0":
version "1.0.1"
resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725"
integrity sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=
graphlib@^2.1.1, graphlib@^2.1.5:
version "2.1.7"
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.7.tgz#b6a69f9f44bd9de3963ce6804a2fc9e73d86aecc"
@ -6423,7 +6580,7 @@ inherits@2.0.1:
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
integrity sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=
ini@^1.3.0, ini@^1.3.4, ini@~1.3.0:
ini@^1.3.0, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0:
version "1.3.5"
resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927"
integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==
@ -6720,6 +6877,11 @@ is-installed-globally@^0.1.0:
global-dirs "^0.1.0"
is-path-inside "^1.0.0"
is-natural-number@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/is-natural-number/-/is-natural-number-4.0.1.tgz#ab9d76e1db4ced51e35de0c72ebecf09f734cde8"
integrity sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=
is-negated-glob@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-negated-glob/-/is-negated-glob-1.0.0.tgz#6910bca5da8c95e784b5751b976cf5a10fee36d2"
@ -9425,12 +9587,17 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
integrity sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=
pify@^2.0.0:
pify@^2.0.0, pify@^2.3.0:
version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw=
@ -9680,7 +9847,7 @@ process@~0.5.1:
resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf"
integrity sha1-FjjYqONML0QKkduVq5rrZ3/Bhc8=
progress@^2.0.0:
progress@^2.0.0, progress@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
@ -10646,7 +10813,7 @@ read-pkg@^3.0.0:
normalize-package-data "^2.3.2"
path-type "^3.0.0"
"readable-stream@1 || 2", readable-stream@2, "readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
"readable-stream@1 || 2", readable-stream@2, "readable-stream@> 1.0.0 < 3.0.0", readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.4, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.0, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
version "2.3.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf"
integrity sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==
@ -10709,16 +10876,22 @@ readdirp@^2.0.0:
micromatch "^3.1.10"
readable-stream "^2.0.2"
realm@2.24:
version "2.24.0"
resolved "https://registry.yarnpkg.com/realm/-/realm-2.24.0.tgz#4c804bed23360b7d4f23964e708d142608f7a335"
integrity sha512-pIeZNoUfqrfo9WRdP3PtVVjh2aEde9l6T5tReN4as9MLn9sC/17tppatrl5S3gfakxvNQH3uJ9FdYI7lS6EspQ==
realm@2.26.1:
version "2.26.1"
resolved "https://registry.yarnpkg.com/realm/-/realm-2.26.1.tgz#9d890c85c4d0946bef0a3ece736551c6a8a5dc49"
integrity sha512-kkDOMV5vgaPOYgTELHFPws9suEF0LI/kSb8SIZ615STKHLHLiRxioxgBcu5beO5HVkjxe5jYx7duSB3NASr+AA==
dependencies:
command-line-args "^4.0.6"
decompress "^4.2.0"
deepmerge "2.1.0"
fs-extra "^4.0.3"
https-proxy-agent "^2.2.1"
ini "^1.3.5"
nan "^2.12.1"
node-fetch "^1.7.3"
node-machine-id "^1.1.10"
node-pre-gyp "^0.11.0"
progress "^2.0.3"
prop-types "^15.6.2"
request "^2.88.0"
stream-counter "^1.0.0"
@ -11288,6 +11461,13 @@ secure-keys@^1.0.0:
resolved "https://registry.yarnpkg.com/secure-keys/-/secure-keys-1.0.0.tgz#f0c82d98a3b139a8776a8808050b824431087fca"
integrity sha1-8MgtmKOxOah3aogIBQuCRDEIf8o=
seek-bzip@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/seek-bzip/-/seek-bzip-1.0.5.tgz#cfe917cb3d274bcffac792758af53173eb1fabdc"
integrity sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=
dependencies:
commander "~2.8.1"
semver-diff@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36"
@ -12087,6 +12267,13 @@ strip-bom@^2.0.0:
dependencies:
is-utf8 "^0.2.0"
strip-dirs@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/strip-dirs/-/strip-dirs-2.1.0.tgz#4987736264fc344cf20f6c34aca9d13d1d4ed6c5"
integrity sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==
dependencies:
is-natural-number "^4.0.1"
strip-eof@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf"
@ -12192,6 +12379,19 @@ tapable@^1.0.0, tapable@^1.1.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.1.tgz#4d297923c5a72a42360de2ab52dadfaaec00018e"
integrity sha512-9I2ydhj8Z9veORCw5PRm4u9uebCn0mcCa6scWoNcbZ6dAtoo2618u9UUzxgmsCOreJpqDDuv61LvwofW7hLcBA==
tar-stream@^1.5.2:
version "1.6.2"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-1.6.2.tgz#8ea55dab37972253d9a9af90fdcd559ae435c555"
integrity sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==
dependencies:
bl "^1.0.0"
buffer-alloc "^1.2.0"
end-of-stream "^1.0.0"
fs-constants "^1.0.0"
readable-stream "^2.3.0"
to-buffer "^1.1.1"
xtend "^4.0.0"
tar@^4:
version "4.4.8"
resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.8.tgz#b19eec3fde2a96e64666df9fdb40c5ca1bc3747d"
@ -12274,6 +12474,14 @@ test-exclude@^4.2.1:
read-pkg-up "^1.0.1"
require-main-filename "^1.0.1"
test-value@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/test-value/-/test-value-2.1.0.tgz#11da6ff670f3471a73b625ca4f3fdcf7bb748291"
integrity sha1-Edpv9nDzRxpztiXKTz/c97t0gpE=
dependencies:
array-back "^1.0.3"
typical "^2.6.0"
text-table@0.2.0, text-table@^0.2.0:
version "0.2.0"
resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4"
@ -12324,7 +12532,7 @@ through2@^2.0.0, through2@^2.0.1, through2@^2.0.2, through2@~2.0.0:
readable-stream "~2.3.6"
xtend "~4.0.1"
through@^2.3.6:
through@^2.3.6, through@^2.3.8:
version "2.3.8"
resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5"
integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=
@ -12381,6 +12589,11 @@ to-arraybuffer@^1.0.0:
resolved "https://registry.yarnpkg.com/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz#7d229b1fcc637e466ca081180836a7aabff83f43"
integrity sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=
to-buffer@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/to-buffer/-/to-buffer-1.1.1.tgz#493bd48f62d7c43fcded313a03dcadb2e1213a80"
integrity sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==
to-fast-properties@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47"
@ -12514,6 +12727,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
typical@^2.6.0, typical@^2.6.1:
version "2.6.1"
resolved "https://registry.yarnpkg.com/typical/-/typical-2.6.1.tgz#5c080e5d661cbbe38259d2e70a3c7253e873881d"
integrity sha1-XAgOXWYcu+OCWdLnCjxyU+hziB0=
ua-parser-js@^0.7.18:
version "0.7.19"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
@ -12557,6 +12775,14 @@ ultron@~1.1.0:
resolved "https://registry.yarnpkg.com/ultron/-/ultron-1.1.1.tgz#9fe1536a10a664a65266a1e3ccf85fd36302bc9c"
integrity sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==
unbzip2-stream@^1.0.9:
version "1.3.3"
resolved "https://registry.yarnpkg.com/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz#d156d205e670d8d8c393e1c02ebd506422873f6a"
integrity sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==
dependencies:
buffer "^5.2.1"
through "^2.3.8"
unc-path-regex@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/unc-path-regex/-/unc-path-regex-0.1.2.tgz#e73dd3d7b0d7c5ed86fbac6b0ae7d8c6a69d50fa"
@ -13345,3 +13571,11 @@ yargs@^9.0.0:
which-module "^2.0.0"
y18n "^3.2.1"
yargs-parser "^7.0.0"
yauzl@^2.4.2:
version "2.10.0"
resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9"
integrity sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=
dependencies:
buffer-crc32 "~0.2.3"
fd-slicer "~1.1.0"