[FIX] Threads (#838)

Closes #826
Closes #827
Closes #828
Closes #829
Closes #830
Closes #831
Closes #832
Closes #833
This commit is contained in:
Diego Mello 2019-04-24 15:36:29 -03:00 committed by GitHub
parent 0266cc2e01
commit 5744114d7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 2220 additions and 1665 deletions

File diff suppressed because it is too large Load Diff

View File

@ -11,6 +11,7 @@ export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
export const COLOR_SEPARATOR = '#A7A7AA'; export const COLOR_SEPARATOR = '#A7A7AA';
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5'; export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
export const COLOR_BORDER = '#e1e5e8'; export const COLOR_BORDER = '#e1e5e8';
export const COLOR_UNREAD = '#e1e5e8';
export const STATUS_COLORS = { export const STATUS_COLORS = {
online: '#2de0a5', online: '#2de0a5',
busy: COLOR_DANGER, busy: COLOR_DANGER,

View File

@ -57,12 +57,14 @@ class MessageBox extends Component {
replying: PropTypes.bool, replying: PropTypes.bool,
editing: PropTypes.bool, editing: PropTypes.bool,
threadsEnabled: PropTypes.bool, threadsEnabled: PropTypes.bool,
isFocused: PropTypes.bool,
user: PropTypes.shape({ user: PropTypes.shape({
id: PropTypes.string, id: PropTypes.string,
username: PropTypes.string, username: PropTypes.string,
token: PropTypes.string token: PropTypes.string
}), }),
roomType: PropTypes.string, roomType: PropTypes.string,
tmid: PropTypes.string,
editCancel: PropTypes.func.isRequired, editCancel: PropTypes.func.isRequired,
editRequest: PropTypes.func.isRequired, editRequest: PropTypes.func.isRequired,
onSubmit: PropTypes.func.isRequired, onSubmit: PropTypes.func.isRequired,
@ -92,23 +94,37 @@ class MessageBox extends Component {
} }
componentDidMount() { componentDidMount() {
const { rid } = this.props; const { rid, tmid } = this.props;
const [room] = database.objects('subscriptions').filtered('rid = $0', rid); let msg;
if (room && room.draftMessage) { if (tmid) {
this.setInput(room.draftMessage); const thread = database.objectForPrimaryKey('threads', tmid);
if (thread) {
msg = thread.draftMessage;
}
} else {
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
if (room) {
msg = room.draftMessage;
}
}
if (msg) {
this.setInput(msg);
this.setShowSend(true); this.setShowSend(true);
} }
} }
componentWillReceiveProps(nextProps) { componentWillReceiveProps(nextProps) {
const { message, replyMessage } = this.props; const { message, replyMessage, isFocused } = this.props;
if (message !== nextProps.message && nextProps.message.msg) { if (!isFocused) {
return;
}
if (!equal(message, nextProps.message) && nextProps.message.msg) {
this.setInput(nextProps.message.msg); this.setInput(nextProps.message.msg);
if (this.text) { if (this.text) {
this.setShowSend(true); this.setShowSend(true);
} }
this.focus(); this.focus();
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) { } else if (!equal(replyMessage, nextProps.replyMessage)) {
this.focus(); this.focus();
} else if (!nextProps.message) { } else if (!nextProps.message) {
this.clearInput(); this.clearInput();
@ -120,8 +136,11 @@ class MessageBox extends Component {
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
} = this.state; } = this.state;
const { const {
roomType, replying, editing roomType, replying, editing, isFocused
} = this.props; } = this.props;
if (!isFocused) {
return false;
}
if (nextProps.roomType !== roomType) { if (nextProps.roomType !== roomType) {
return true; return true;
} }
@ -481,7 +500,7 @@ class MessageBox extends Component {
} }
sendImageMessage = async(file) => { sendImageMessage = async(file) => {
const { rid } = this.props; const { rid, tmid } = this.props;
this.setState({ file: { isVisible: false } }); this.setState({ file: { isVisible: false } });
const fileInfo = { const fileInfo = {
@ -493,7 +512,7 @@ class MessageBox extends Component {
path: file.path path: file.path
}; };
try { try {
await RocketChat.sendFileMessage(rid, fileInfo); await RocketChat.sendFileMessage(rid, fileInfo, tmid);
} catch (e) { } catch (e) {
log('sendImageMessage', e); log('sendImageMessage', e);
} }
@ -539,14 +558,14 @@ class MessageBox extends Component {
} }
finishAudioMessage = async(fileInfo) => { finishAudioMessage = async(fileInfo) => {
const { rid } = this.props; const { rid, tmid } = this.props;
this.setState({ this.setState({
recording: false recording: false
}); });
if (fileInfo) { if (fileInfo) {
try { try {
await RocketChat.sendFileMessage(rid, fileInfo); await RocketChat.sendFileMessage(rid, fileInfo, tmid);
} catch (e) { } catch (e) {
if (e && e.error === 'error-file-too-large') { if (e && e.error === 'error-file-too-large') {
return Alert.alert(I18n.t(e.error)); return Alert.alert(I18n.t(e.error));
@ -830,7 +849,7 @@ class MessageBox extends Component {
const mapStateToProps = state => ({ const mapStateToProps = state => ({
message: state.messages.message, message: state.messages.message,
replyMessage: state.messages.replyMessage, replyMessage: state.messages.replyMessage,
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg, replying: state.messages.replying,
editing: state.messages.editing, editing: state.messages.editing,
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
threadsEnabled: state.settings.Threads_enabled, threadsEnabled: state.settings.Threads_enabled,

View File

@ -6,8 +6,8 @@ import {
import Video from 'react-native-video'; import Video from 'react-native-video';
import Slider from 'react-native-slider'; import Slider from 'react-native-slider';
import moment from 'moment'; import moment from 'moment';
import { BorderlessButton } from 'react-native-gesture-handler';
import equal from 'deep-equal'; import equal from 'deep-equal';
import Touchable from 'react-native-platform-touchable';
import Markdown from './Markdown'; import Markdown from './Markdown';
import { CustomIcon } from '../../lib/Icons'; import { CustomIcon } from '../../lib/Icons';
@ -27,7 +27,7 @@ const styles = StyleSheet.create({
marginBottom: 6 marginBottom: 6
}, },
playPauseButton: { playPauseButton: {
width: 56, marginHorizontal: 10,
alignItems: 'center', alignItems: 'center',
backgroundColor: 'transparent' backgroundColor: 'transparent'
}, },
@ -35,11 +35,10 @@ const styles = StyleSheet.create({
color: COLOR_PRIMARY color: COLOR_PRIMARY
}, },
slider: { slider: {
flex: 1, flex: 1
marginRight: 10
}, },
duration: { duration: {
marginRight: 16, marginHorizontal: 12,
fontSize: 14, fontSize: 14,
...sharedStyles.textColorNormal, ...sharedStyles.textColorNormal,
...sharedStyles.textRegular ...sharedStyles.textRegular
@ -47,10 +46,16 @@ const styles = StyleSheet.create({
thumbStyle: { thumbStyle: {
width: 12, width: 12,
height: 12 height: 12
},
trackStyle: {
height: 2
} }
}); });
const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss'); const formatTime = seconds => moment.utc(seconds * 1000).format('mm:ss');
const BUTTON_HIT_SLOP = {
top: 12, right: 12, bottom: 12, left: 12
};
export default class Audio extends React.Component { export default class Audio extends React.Component {
static propTypes = { static propTypes = {
@ -97,30 +102,30 @@ export default class Audio extends React.Component {
return false; return false;
} }
onLoad(data) { onLoad = (data) => {
this.setState({ duration: data.duration > 0 ? data.duration : 0 }); this.setState({ duration: data.duration > 0 ? data.duration : 0 });
} }
onProgress(data) { onProgress = (data) => {
const { duration } = this.state; const { duration } = this.state;
if (data.currentTime <= duration) { if (data.currentTime <= duration) {
this.setState({ currentTime: data.currentTime }); this.setState({ currentTime: data.currentTime });
} }
} }
onEnd() { onEnd = () => {
this.setState({ paused: true, currentTime: 0 }); this.setState({ paused: true, currentTime: 0 });
requestAnimationFrame(() => { requestAnimationFrame(() => {
this.player.seek(0); this.player.seek(0);
}); });
} }
getDuration() { getDuration = () => {
const { duration } = this.state; const { duration } = this.state;
return formatTime(duration); return formatTime(duration);
} }
togglePlayPause() { togglePlayPause = () => {
const { paused } = this.state; const { paused } = this.state;
this.setState({ paused: !paused }); this.setState({ paused: !paused });
} }
@ -152,16 +157,18 @@ export default class Audio extends React.Component {
paused={paused} paused={paused}
repeat={false} repeat={false}
/> />
<BorderlessButton <Touchable
style={styles.playPauseButton} style={styles.playPauseButton}
onPress={() => this.togglePlayPause()} onPress={this.togglePlayPause}
hitSlop={BUTTON_HIT_SLOP}
background={Touchable.SelectableBackgroundBorderless()}
> >
{ {
paused paused
? <CustomIcon name='play' size={30} style={styles.playPauseImage} /> ? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
: <CustomIcon name='pause' size={30} style={styles.playPauseImage} /> : <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
} }
</BorderlessButton> </Touchable>
<Slider <Slider
style={styles.slider} style={styles.slider}
value={currentTime} value={currentTime}
@ -177,6 +184,7 @@ export default class Audio extends React.Component {
minimumTrackTintColor={COLOR_PRIMARY} minimumTrackTintColor={COLOR_PRIMARY}
onValueChange={value => this.setState({ currentTime: value })} onValueChange={value => this.setState({ currentTime: value })}
thumbStyle={styles.thumbStyle} thumbStyle={styles.thumbStyle}
trackStyle={styles.trackStyle}
/> />
<Text style={styles.duration}>{this.getDuration()}</Text> <Text style={styles.duration}>{this.getDuration()}</Text>
</View>, </View>,

View File

@ -22,7 +22,7 @@ export default class Markdown extends React.Component {
render() { render() {
const { const {
msg, customEmojis, style, rules, baseUrl, username, edited msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
} = this.props; } = this.props;
if (!msg) { if (!msg) {
return null; return null;
@ -32,12 +32,15 @@ export default class Markdown extends React.Component {
m = emojify(m, { output: 'unicode' }); m = emojify(m, { output: 'unicode' });
} }
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim(); m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
if (numberOfLines > 0) {
m = m.replace(/[\n]+/g, '\n').trim();
}
return ( return (
<MarkdownRenderer <MarkdownRenderer
rules={{ rules={{
paragraph: (node, children) => ( paragraph: (node, children) => (
// eslint-disable-next-line // eslint-disable-next-line
<Text key={node.key} style={styles.paragraph}> <Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
{children} {children}
{edited ? <Text style={styles.edited}> (edited)</Text> : null} {edited ? <Text style={styles.edited}> (edited)</Text> : null}
</Text> </Text>
@ -111,5 +114,6 @@ Markdown.propTypes = {
customEmojis: PropTypes.object.isRequired, customEmojis: PropTypes.object.isRequired,
style: PropTypes.any, style: PropTypes.any,
rules: PropTypes.object, rules: PropTypes.object,
edited: PropTypes.bool edited: PropTypes.bool,
numberOfLines: PropTypes.number
}; };

View File

@ -5,10 +5,8 @@ import {
} from 'react-native'; } from 'react-native';
import moment from 'moment'; import moment from 'moment';
import { KeyboardUtils } from 'react-native-keyboard-input'; import { KeyboardUtils } from 'react-native-keyboard-input';
import {
BorderlessButton
} from 'react-native-gesture-handler';
import Touchable from 'react-native-platform-touchable'; import Touchable from 'react-native-platform-touchable';
import { emojify } from 'react-emojione';
import Image from './Image'; import Image from './Image';
import User from './User'; import User from './User';
@ -164,6 +162,11 @@ export default class Message extends PureComponent {
onPress = () => { onPress = () => {
KeyboardUtils.dismiss(); KeyboardUtils.dismiss();
const { onThreadPress, tlm, tmid } = this.props;
if ((tlm || tmid) && onThreadPress) {
onThreadPress();
}
} }
onLongPress = () => { onLongPress = () => {
@ -269,10 +272,25 @@ export default class Message extends PureComponent {
if (this.isInfoMessage()) { if (this.isInfoMessage()) {
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>; return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
} }
const { const {
customEmojis, msg, baseUrl, user, edited customEmojis, msg, baseUrl, user, edited, tmid
} = this.props; } = this.props;
return <Markdown msg={msg} customEmojis={customEmojis} baseUrl={baseUrl} username={user.username} edited={edited} />;
if (tmid && !msg) {
return <Text style={styles.text}>{I18n.t('Sent_an_attachment')}</Text>;
}
return (
<Markdown
msg={msg}
customEmojis={customEmojis}
baseUrl={baseUrl}
username={user.username}
edited={edited}
numberOfLines={tmid ? 1 : 0}
/>
);
} }
renderAttachment() { renderAttachment() {
@ -316,9 +334,9 @@ export default class Message extends PureComponent {
} }
const { onErrorPress } = this.props; const { onErrorPress } = this.props;
return ( return (
<BorderlessButton onPress={onErrorPress} style={styles.errorButton}> <Touchable onPress={onErrorPress} style={styles.errorButton}>
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} /> <CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
</BorderlessButton> </Touchable>
); );
} }
@ -457,7 +475,7 @@ export default class Message extends PureComponent {
renderRepliedThread = () => { renderRepliedThread = () => {
const { const {
tmid, tmsg, header, onThreadPress, fetchThreadName tmid, tmsg, header, fetchThreadName
} = this.props; } = this.props;
if (!tmid || !header || this.isTemp()) { if (!tmid || !header || this.isTemp()) {
return null; return null;
@ -468,15 +486,18 @@ export default class Message extends PureComponent {
return null; return null;
} }
const msg = emojify(tmsg, { output: 'unicode' });
return ( return (
<Text style={styles.repliedThread} numberOfLines={3} testID={`message-thread-replied-on-${ tmsg }`}> <View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
{I18n.t('Replied_on')} <Text style={styles.repliedThreadName} onPress={onThreadPress}>{tmsg}</Text> <CustomIcon name='thread' size={20} style={[styles.buttonIcon, styles.repliedThreadIcon]} />
</Text> <Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
</View>
); );
} }
renderInner = () => { renderInner = () => {
const { type } = this.props; const { type, tmid } = this.props;
if (type === 'discussion-created') { if (type === 'discussion-created') {
return ( return (
<React.Fragment> <React.Fragment>
@ -485,10 +506,18 @@ export default class Message extends PureComponent {
</React.Fragment> </React.Fragment>
); );
} }
if (tmid) {
return (
<React.Fragment>
{this.renderUsername()}
{this.renderRepliedThread()}
{this.renderContent()}
</React.Fragment>
);
}
return ( return (
<React.Fragment> <React.Fragment>
{this.renderUsername()} {this.renderUsername()}
{this.renderRepliedThread()}
{this.renderContent()} {this.renderContent()}
{this.renderAttachment()} {this.renderAttachment()}
{this.renderUrl()} {this.renderUrl()}

View File

@ -76,7 +76,7 @@ export default class MessageContainer extends React.Component {
shouldComponentUpdate(nextProps, nextState) { shouldComponentUpdate(nextProps, nextState) {
const { reactionsModal } = this.state; const { reactionsModal } = this.state;
const { const {
status, editingMessage, item, _updatedAt status, editingMessage, item, _updatedAt, navigation
} = this.props; } = this.props;
if (reactionsModal !== nextState.reactionsModal) { if (reactionsModal !== nextState.reactionsModal) {
@ -89,7 +89,7 @@ export default class MessageContainer extends React.Component {
return true; return true;
} }
if (!equal(editingMessage, nextProps.editingMessage)) { if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) { if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
return true; return true;
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) { } else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {

View File

@ -216,13 +216,16 @@ export default StyleSheet.create({
fontWeight: '300' fontWeight: '300'
}, },
repliedThread: { repliedThread: {
fontSize: 16, flexDirection: 'row',
marginBottom: 6, flex: 1
...sharedStyles.textColorDescription, },
...sharedStyles.textRegular repliedThreadIcon: {
color: COLOR_PRIMARY
}, },
repliedThreadName: { repliedThreadName: {
fontSize: 16, fontSize: 16,
fontStyle: 'normal',
flex: 1,
color: COLOR_PRIMARY, color: COLOR_PRIMARY,
...sharedStyles.textSemibold ...sharedStyles.textSemibold
} }

View File

@ -293,6 +293,7 @@ export default {
Send: 'Send', Send: 'Send',
Send_audio_message: 'Send audio message', Send_audio_message: 'Send audio message',
Send_message: 'Send message', Send_message: 'Send message',
Sent_an_attachment: 'Sent an attachment',
Server: 'Server', Server: 'Server',
Servers: 'Servers', Servers: 'Servers',
Set_username_subtitle: 'The username is used to allow others to mention you in messages', Set_username_subtitle: 'The username is used to allow others to mention you in messages',

View File

@ -295,6 +295,7 @@ export default {
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',
Sent_an_attachment: 'Enviou um anexo',
Server: 'Servidor', Server: 'Servidor',
Set_username_subtitle: 'O usuário é utilizado para permitir que você seja mencionado em mensagens', Set_username_subtitle: 'O usuário é utilizado para permitir que você seja mencionado em mensagens',
Settings: 'Configurações', Settings: 'Configurações',

View File

@ -284,6 +284,7 @@ export default {
Send: 'Enviar', Send: 'Enviar',
Send_audio_message: 'Enviar mensagem de áudio', Send_audio_message: 'Enviar mensagem de áudio',
Send_message: 'Enviar mensagem', Send_message: 'Enviar mensagem',
Sent_an_attachment: 'Enviou um ficheiro',
Server: 'Servidor', Server: 'Servidor',
Servers: 'Servidores', Servers: 'Servidores',
Set_username_subtitle: 'O nome de utilizador é usado para permitir que outros mencionem você em mensagens', Set_username_subtitle: 'O nome de utilizador é usado para permitir que outros mencionem você em mensagens',

View File

@ -145,7 +145,7 @@ const ProfileStack = createStackNavigator({
defaultNavigationOptions: defaultHeader defaultNavigationOptions: defaultHeader
}); });
ProfileView.navigationOptions = ({ navigation }) => { ProfileStack.navigationOptions = ({ navigation }) => {
let drawerLockMode = 'unlocked'; let drawerLockMode = 'unlocked';
if (navigation.state.index > 0) { if (navigation.state.index > 0) {
drawerLockMode = 'locked-closed'; drawerLockMode = 'locked-closed';

View File

@ -46,6 +46,11 @@ export default function loadMessagesForRoom(...args) {
if (message.tlm) { if (message.tlm) {
database.create('threads', message, true); database.create('threads', message, true);
} }
// if it belongs to a thread
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) { } catch (e) {
log('loadMessagesForRoom -> create messages', e); log('loadMessagesForRoom -> create messages', e);
} }

View File

@ -40,11 +40,14 @@ export default function loadMissedMessages(...args) {
if (message.tlm) { if (message.tlm) {
database.create('threads', message, true); database.create('threads', message, true);
} }
if (message.tmid) {
message.rid = message.tmid;
database.create('threadMessages', message, true);
}
} catch (e) { } catch (e) {
log('loadMissedMessages -> create messages', e); log('loadMissedMessages -> create messages', e);
} }
})); }));
resolve(updated);
}); });
} }
if (data.deleted && data.deleted.length) { if (data.deleted && data.deleted.length) {
@ -55,6 +58,10 @@ export default function loadMissedMessages(...args) {
deleted.forEach((m) => { deleted.forEach((m) => {
const message = database.objects('messages').filtered('_id = $0', m._id); const message = database.objects('messages').filtered('_id = $0', m._id);
database.delete(message); database.delete(message);
const thread = database.objects('threads').filtered('_id = $0', m._id);
database.delete(thread);
const threadMessage = database.objects('threadMessages').filtered('_id = $0', m._id);
database.delete(threadMessage);
}); });
}); });
} catch (e) { } catch (e) {
@ -63,7 +70,7 @@ export default function loadMissedMessages(...args) {
}); });
} }
} }
resolve([]); resolve();
} catch (e) { } catch (e) {
log('loadMissedMessages', e); log('loadMissedMessages', e);
reject(e); reject(e);

View File

@ -5,24 +5,26 @@ import buildMessage from './helpers/buildMessage';
import database from '../realm'; import database from '../realm';
import log from '../../utils/log'; import log from '../../utils/log';
async function load({ tmid, skip }) { async function load({ tmid, offset }) {
try { try {
// RC 1.0 // RC 1.0
const data = await this.sdk.methodCall('getThreadMessages', { tmid, limit: 50, skip }); const result = await this.sdk.get('chat.getThreadMessages', {
if (!data || data.status === 'error') { tmid, count: 50, offset, sort: { ts: -1 }
});
if (!result || !result.success) {
return []; return [];
} }
return data; return result.messages;
} catch (error) { } catch (error) {
console.log(error); console.log(error);
return []; return [];
} }
} }
export default function loadThreadMessages({ tmid, skip }) { export default function loadThreadMessages({ tmid, offset = 0 }) {
return new Promise(async(resolve, reject) => { return new Promise(async(resolve, reject) => {
try { try {
const data = await load.call(this, { tmid, skip }); const data = await load.call(this, { tmid, offset });
if (data && data.length) { if (data && data.length) {
InteractionManager.runAfterInteractions(() => { InteractionManager.runAfterInteractions(() => {

View File

@ -29,7 +29,7 @@ export async function cancelUpload(path) {
} }
} }
export async function sendFileMessage(rid, fileInfo) { export async function sendFileMessage(rid, fileInfo, tmid) {
try { try {
const data = await RNFetchBlob.wrap(fileInfo.path); const data = await RNFetchBlob.wrap(fileInfo.path);
if (!fileInfo.size) { if (!fileInfo.size) {
@ -86,6 +86,8 @@ export async function sendFileMessage(rid, fileInfo) {
name: completeResult.name, name: completeResult.name,
description: completeResult.description, description: completeResult.description,
url: completeResult.path url: completeResult.path
}, {
tmid
}); });
database.write(() => { database.write(() => {

View File

@ -104,7 +104,8 @@ const subscriptionSchema = {
muted: { type: 'list', objectType: 'usersMuted' }, muted: { type: 'list', objectType: 'usersMuted' },
broadcast: { type: 'bool', optional: true }, broadcast: { type: 'bool', optional: true },
prid: { type: 'string', optional: true }, prid: { type: 'string', optional: true },
draftMessage: { type: 'string', optional: true } draftMessage: { type: 'string', optional: true },
lastThreadSync: 'date?'
} }
}; };
@ -259,7 +260,8 @@ const threadsSchema = {
tmid: { type: 'string', optional: true }, tmid: { type: 'string', optional: true },
tcount: { type: 'int', optional: true }, tcount: { type: 'int', optional: true },
tlm: { type: 'date', optional: true }, tlm: { type: 'date', optional: true },
replies: 'string[]' replies: 'string[]',
draftMessage: 'string?'
} }
}; };
@ -387,9 +389,9 @@ class DB {
schema: [ schema: [
serversSchema serversSchema
], ],
schemaVersion: 4, schemaVersion: 5,
migration: (oldRealm, newRealm) => { migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 3) { if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 5) {
const newServers = newRealm.objects('servers'); const newServers = newRealm.objects('servers');
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
@ -441,15 +443,12 @@ class DB {
setActiveDB(database = '') { setActiveDB(database = '') {
const path = database.replace(/(^\w+:|^)\/\//, ''); const path = database.replace(/(^\w+:|^)\/\//, '');
if (this.database) {
this.database.close();
}
return this.databases.activeDB = new Realm({ return this.databases.activeDB = new Realm({
path: `${ path }.realm`, path: `${ path }.realm`,
schema, schema,
schemaVersion: 6, schemaVersion: 8,
migration: (oldRealm, newRealm) => { migration: (oldRealm, newRealm) => {
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 6) { if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
const newSubs = newRealm.objects('subscriptions'); const newSubs = newRealm.objects('subscriptions');
// eslint-disable-next-line no-plusplus // eslint-disable-next-line no-plusplus
@ -459,6 +458,10 @@ class DB {
} }
const newMessages = newRealm.objects('messages'); const newMessages = newRealm.objects('messages');
newRealm.delete(newMessages); newRealm.delete(newMessages);
const newThreads = newRealm.objects('threads');
newRealm.delete(newThreads);
const newThreadMessages = newRealm.objects('threadMessages');
newRealm.delete(newThreadMessages);
} }
} }
}); });

View File

@ -416,8 +416,6 @@ const RocketChat = {
data = data.filtered('t != $0', 'd'); data = data.filtered('t != $0', 'd');
} }
data = data.slice(0, 7); data = data.slice(0, 7);
const array = Array.from(data);
data = JSON.parse(JSON.stringify(array));
const usernames = data.map(sub => sub.name); const usernames = data.map(sub => sub.name);
try { try {
@ -782,9 +780,17 @@ const RocketChat = {
} }
return this.sdk.methodCall('unfollowMessage', { mid }); return this.sdk.methodCall('unfollowMessage', { mid });
}, },
getThreadsList({ rid, limit, skip }) { getThreadsList({ rid, count, offset }) {
// RC 1.0 // RC 1.0
return this.sdk.methodCall('getThreadsList', { rid, limit, skip }); return this.sdk.get('chat.getThreadsList', {
rid, count, offset, sort: { ts: -1 }
});
},
getSyncThreadsList({ rid, updatedSince }) {
// RC 1.0
return this.sdk.get('chat.syncThreadsList', {
rid, updatedSince
});
} }
}; };

View File

@ -41,7 +41,7 @@ const formatMsg = ({
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps); const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
const LastMessage = React.memo(({ const LastMessage = React.memo(({
lastMessage, type, showLastMessage, username lastMessage, type, showLastMessage, username, alert
}) => ( }) => (
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}> <Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
{formatMsg({ {formatMsg({
@ -54,7 +54,8 @@ LastMessage.propTypes = {
lastMessage: PropTypes.object, lastMessage: PropTypes.object,
type: PropTypes.string, type: PropTypes.string,
showLastMessage: PropTypes.bool, showLastMessage: PropTypes.bool,
username: PropTypes.string username: PropTypes.string,
alert: PropTypes.bool
}; };
export default LastMessage; export default LastMessage;

View File

@ -14,8 +14,8 @@ const UnreadBadge = React.memo(({ unread, userMentions, type }) => {
const mentioned = userMentions > 0 && type !== 'd'; const mentioned = userMentions > 0 && type !== 'd';
return ( return (
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentioned]}> <View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentionedContainer]}>
<Text style={styles.unreadNumberText}>{ unread }</Text> <Text style={[styles.unreadText, mentioned && styles.unreadMentionedText]}>{ unread }</Text>
</View> </View>
); );
}); });

View File

@ -109,7 +109,7 @@ export default class RoomItem extends React.Component {
{_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null} {_updatedAt ? <Text style={[styles.date, alert && styles.updateAlert]} ellipsizeMode='tail' numberOfLines={1}>{ date }</Text> : null}
</View> </View>
<View style={styles.row}> <View style={styles.row}>
<LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} /> <LastMessage lastMessage={lastMessage} type={type} showLastMessage={showLastMessage} username={username} alert={alert} />
<UnreadBadge unread={unread} userMentions={userMentions} type={type} /> <UnreadBadge unread={unread} userMentions={userMentions} type={type} />
</View> </View>
</View> </View>

View File

@ -2,7 +2,7 @@ import { StyleSheet, PixelRatio } from 'react-native';
import sharedStyles from '../../views/Styles'; import sharedStyles from '../../views/Styles';
import { import {
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_UNREAD, COLOR_TEXT
} from '../../constants/colors'; } from '../../constants/colors';
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale(); export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
@ -53,27 +53,30 @@ export default StyleSheet.create({
...sharedStyles.textSemibold ...sharedStyles.textSemibold
}, },
unreadNumberContainer: { unreadNumberContainer: {
minWidth: 22, minWidth: 21,
height: 22, height: 21,
paddingVertical: 3, paddingVertical: 3,
paddingHorizontal: 5, paddingHorizontal: 5,
borderRadius: 14, borderRadius: 10.5,
backgroundColor: COLOR_TEXT, backgroundColor: COLOR_UNREAD,
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
marginLeft: 10 marginLeft: 10
}, },
unreadMentioned: { unreadMentionedContainer: {
backgroundColor: COLOR_PRIMARY backgroundColor: COLOR_PRIMARY
}, },
unreadNumberText: { unreadText: {
color: COLOR_WHITE, color: COLOR_TEXT,
overflow: 'hidden', overflow: 'hidden',
fontSize: 13, fontSize: 13,
...sharedStyles.textRegular, ...sharedStyles.textMedium,
letterSpacing: 0.56, letterSpacing: 0.56,
textAlign: 'center' textAlign: 'center'
}, },
unreadMentionedText: {
color: COLOR_WHITE
},
status: { status: {
marginRight: 7, marginRight: 7,
marginTop: 3 marginTop: 3

View File

@ -4,6 +4,7 @@ const initialState = {
message: {}, message: {},
actionMessage: {}, actionMessage: {},
replyMessage: {}, replyMessage: {},
replying: false,
editing: false, editing: false,
showActions: false, showActions: false,
showErrorActions: false, showErrorActions: false,
@ -64,12 +65,14 @@ export default function messages(state = initialState, action) {
replyMessage: { replyMessage: {
...action.message, ...action.message,
mention: action.mention mention: action.mention
} },
replying: true
}; };
case types.MESSAGES.REPLY_CANCEL: case types.MESSAGES.REPLY_CANCEL:
return { return {
...state, ...state,
replyMessage: {} replyMessage: {},
replying: false
}; };
case types.MESSAGES.SET_INPUT: case types.MESSAGES.SET_INPUT:
return { return {

View File

@ -10,10 +10,14 @@ import log from '../../../utils/log';
const styles = StyleSheet.create({ const styles = StyleSheet.create({
more: { more: {
marginHorizontal: 0, marginLeft: 0, marginRight: 5 marginHorizontal: 0,
marginLeft: 0,
marginRight: 5
}, },
thread: { thread: {
marginHorizontal: 0, marginLeft: 0, marginRight: 10 marginHorizontal: 0,
marginLeft: 0,
marginRight: 15
} }
}); });
@ -34,6 +38,7 @@ class RightButtonsContainer extends React.PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
if (props.tmid) { if (props.tmid) {
// FIXME: it may be empty if the thread header isn't fetched yet
this.thread = database.objectForPrimaryKey('messages', props.tmid); this.thread = database.objectForPrimaryKey('messages', props.tmid);
safeAddListener(this.thread, this.updateThread); safeAddListener(this.thread, this.updateThread);
} }

View File

@ -1,7 +1,6 @@
import React from 'react'; import React from 'react';
import { ActivityIndicator, FlatList, InteractionManager } from 'react-native'; import { ActivityIndicator, FlatList, InteractionManager } from 'react-native';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { emojify } from 'react-emojione';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import styles from './styles'; import styles from './styles';
@ -30,7 +29,7 @@ export class List extends React.PureComponent {
.objects('threadMessages') .objects('threadMessages')
.filtered('rid = $0', props.tmid) .filtered('rid = $0', props.tmid)
.sorted('ts', true); .sorted('ts', true);
this.threads = []; this.threads = database.objects('threads').filtered('_id = $0', props.tmid);
} else { } else {
this.data = database this.data = database
.objects('messages') .objects('messages')
@ -83,7 +82,7 @@ export class List extends React.PureComponent {
}); });
}, 300, { leading: true }); }, 300, { leading: true });
onEndReached = async() => { onEndReached = debounce(async() => {
const { const {
loading, end, messages loading, end, messages
} = this.state; } = this.state;
@ -96,17 +95,17 @@ export class List extends React.PureComponent {
try { try {
let result; let result;
if (tmid) { if (tmid) {
result = await RocketChat.loadThreadMessages({ tmid, skip: messages.length }); result = await RocketChat.loadThreadMessages({ tmid, offset: messages.length });
} else { } else {
result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts }); result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts });
} }
this.setState({ end: result.length < 50 }); this.setState({ end: result.length < 50, loading: false });
} catch (e) { } catch (e) {
this.setState({ loading: false }); this.setState({ loading: false });
log('ListView.onEndReached', e); log('ListView.onEndReached', e);
} }
} }, 300)
renderFooter = () => { renderFooter = () => {
const { loading } = this.state; const { loading } = this.state;
@ -122,10 +121,7 @@ export class List extends React.PureComponent {
if (item.tmid) { if (item.tmid) {
const thread = threads.find(t => t._id === item.tmid); const thread = threads.find(t => t._id === item.tmid);
if (thread) { if (thread) {
let tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title); const tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
if (tmsg) {
tmsg = emojify(tmsg, { output: 'unicode' });
}
item = { ...item, tmsg }; item = { ...item, tmsg };
} }
} }
@ -134,15 +130,24 @@ export class List extends React.PureComponent {
render() { render() {
console.count(`${ this.constructor.name }.render calls`); console.count(`${ this.constructor.name }.render calls`);
const { messages } = this.state; const { messages, threads } = this.state;
const { tmid } = this.props;
let data = [];
if (tmid) {
const thread = { ...threads[0] };
thread.tlm = null;
data = [...messages, thread];
} else {
data = messages;
}
return ( return (
<React.Fragment> <React.Fragment>
<EmptyRoom length={messages.length} /> <EmptyRoom length={data.length} />
<FlatList <FlatList
testID='room-view-messages' testID='room-view-messages'
ref={ref => this.list = ref} ref={ref => this.list = ref}
keyExtractor={item => item._id} keyExtractor={item => item._id}
data={messages} data={data}
extraData={this.state} extraData={this.state}
renderItem={this.renderItem} renderItem={this.renderItem}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}

View File

@ -46,6 +46,8 @@ import buildMessage from '../../lib/methods/helpers/buildMessage';
token: state.login.user && state.login.user.token token: state.login.user && state.login.user.token
}, },
actionMessage: state.messages.actionMessage, actionMessage: state.messages.actionMessage,
editing: state.messages.editing,
replying: state.messages.replying,
showActions: state.messages.showActions, showActions: state.messages.showActions,
showErrorActions: state.messages.showErrorActions, showErrorActions: state.messages.showErrorActions,
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background', appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
@ -87,6 +89,8 @@ export default class RoomView extends LoggedView {
appState: PropTypes.string, appState: PropTypes.string,
useRealName: PropTypes.bool, useRealName: PropTypes.bool,
isAuthenticated: PropTypes.bool, isAuthenticated: PropTypes.bool,
editing: PropTypes.bool,
replying: PropTypes.bool,
toggleReactionPicker: PropTypes.func.isRequired, toggleReactionPicker: PropTypes.func.isRequired,
actionsShow: PropTypes.func, actionsShow: PropTypes.func,
editCancel: PropTypes.func, editCancel: PropTypes.func,
@ -176,12 +180,18 @@ export default class RoomView extends LoggedView {
} }
componentWillUnmount() { componentWillUnmount() {
if (this.messagebox && this.messagebox.current && this.messagebox.current.text) { const { editing, replying } = this.props;
if (!editing && this.messagebox && this.messagebox.current && this.messagebox.current.text) {
const { text } = this.messagebox.current; const { text } = this.messagebox.current;
const [room] = this.rooms; let obj;
if (room) { if (this.tmid) {
obj = database.objectForPrimaryKey('threads', this.tmid);
} else {
[obj] = this.rooms;
}
if (obj) {
database.write(() => { database.write(() => {
room.draftMessage = text; obj.draftMessage = text;
}); });
} }
} }
@ -192,9 +202,14 @@ export default class RoomView extends LoggedView {
if (this.beginAnimatingTimeout) { if (this.beginAnimatingTimeout) {
clearTimeout(this.beginAnimatingTimeout); clearTimeout(this.beginAnimatingTimeout);
} }
const { editCancel, replyCancel } = this.props; if (editing) {
editCancel(); const { editCancel } = this.props;
replyCancel(); editCancel();
}
if (replying) {
const { replyCancel } = this.props;
replyCancel();
}
if (this.didMountInteraction && this.didMountInteraction.cancel) { if (this.didMountInteraction && this.didMountInteraction.cancel) {
this.didMountInteraction.cancel(); this.didMountInteraction.cancel();
} }
@ -217,7 +232,7 @@ export default class RoomView extends LoggedView {
this.initInteraction = InteractionManager.runAfterInteractions(async() => { this.initInteraction = InteractionManager.runAfterInteractions(async() => {
const { room } = this.state; const { room } = this.state;
if (this.tmid) { if (this.tmid) {
RocketChat.loadThreadMessages({ tmid: this.tmid, t: this.t }); await this.getThreadMessages();
} else { } else {
await this.getMessages(room); await this.getMessages(room);
@ -241,7 +256,7 @@ export default class RoomView extends LoggedView {
onMessageLongPress = (message) => { onMessageLongPress = (message) => {
const { actionsShow } = this.props; const { actionsShow } = this.props;
actionsShow(message); actionsShow({ ...message, rid: this.rid });
} }
onReactionPress = (shortname, messageId) => { onReactionPress = (shortname, messageId) => {
@ -311,6 +326,15 @@ export default class RoomView extends LoggedView {
} }
} }
getThreadMessages = () => {
try {
return RocketChat.loadThreadMessages({ tmid: this.tmid });
} catch (e) {
console.log('TCL: getThreadMessages -> e', e);
log('getThreadMessages', e);
}
}
setLastOpen = lastOpen => this.setState({ lastOpen }); setLastOpen = lastOpen => this.setState({ lastOpen });
joinRoom = async() => { joinRoom = async() => {
@ -420,6 +444,7 @@ export default class RoomView extends LoggedView {
renderFooter = () => { renderFooter = () => {
const { joined, room } = this.state; const { joined, room } = this.state;
const { navigation } = this.props;
if (!joined && !this.tmid) { if (!joined && !this.tmid) {
return ( return (
@ -450,7 +475,16 @@ export default class RoomView extends LoggedView {
</View> </View>
); );
} }
return <MessageBox ref={this.messagebox} onSubmit={this.sendMessage} rid={this.rid} roomType={room.t} />; return (
<MessageBox
ref={this.messagebox}
onSubmit={this.sendMessage}
rid={this.rid}
tmid={this.tmid}
roomType={room.t}
isFocused={navigation.isFocused()}
/>
);
}; };
renderActions = () => { renderActions = () => {

View File

@ -175,45 +175,6 @@ export default class RoomsListView extends LoggedView {
return true; return true;
} }
const { showUnread, showFavorites, groupByType } = this.props;
if (showUnread) {
const { unread } = this.state;
if (!isEqual(nextState.unread, unread)) {
return true;
}
}
if (showFavorites) {
const { favorites } = this.state;
if (!isEqual(nextState.favorites, favorites)) {
return true;
}
}
if (groupByType) {
const {
dicussions, channels, privateGroup, direct, livechat
} = this.state;
if (!isEqual(nextState.dicussions, dicussions)) {
return true;
}
if (!isEqual(nextState.channels, channels)) {
return true;
}
if (!isEqual(nextState.privateGroup, privateGroup)) {
return true;
}
if (!isEqual(nextState.direct, direct)) {
return true;
}
if (!isEqual(nextState.livechat, livechat)) {
return true;
}
} else {
const { chats } = this.state;
if (!isEqual(nextState.chats, chats)) {
return true;
}
}
const { search } = this.state; const { search } = this.state;
if (!isEqual(nextState.search, search)) { if (!isEqual(nextState.search, search)) {
return true; return true;
@ -311,27 +272,20 @@ export default class RoomsListView extends LoggedView {
updateState = debounce(() => { updateState = debounce(() => {
this.updateStateInteraction = InteractionManager.runAfterInteractions(() => { this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
this.internalSetState({ this.internalSetState({
chats: this.getSnapshot(this.chats), chats: this.chats,
unread: this.getSnapshot(this.unread), unread: this.unread,
favorites: this.getSnapshot(this.favorites), favorites: this.favorites,
discussions: this.getSnapshot(this.discussions), discussions: this.discussions,
channels: this.getSnapshot(this.channels), channels: this.channels,
privateGroup: this.getSnapshot(this.privateGroup), privateGroup: this.privateGroup,
direct: this.getSnapshot(this.direct), direct: this.direct,
livechat: this.getSnapshot(this.livechat), livechat: this.livechat,
loading: false loading: false
}); });
this.forceUpdate();
}); });
}, 300); }, 300);
getSnapshot = (data) => {
if (data && data.length) {
const array = Array.from(data);
return JSON.parse(JSON.stringify(array));
}
return [];
}
initSearchingAndroid = () => { initSearchingAndroid = () => {
const { openSearchHeader, navigation } = this.props; const { openSearchHeader, navigation } = this.props;
this.setState({ searching: true }); this.setState({ searching: true });
@ -441,26 +395,29 @@ export default class RoomsListView extends LoggedView {
} = this.props; } = this.props;
const id = item.rid.replace(userId, '').trim(); const id = item.rid.replace(userId, '').trim();
return ( if (item.search || (item.isValid && item.isValid())) {
<RoomItem return (
alert={item.alert} <RoomItem
unread={item.unread} alert={item.alert}
userMentions={item.userMentions} unread={item.unread}
favorite={item.f} userMentions={item.userMentions}
lastMessage={item.lastMessage} favorite={item.f}
name={this.getRoomTitle(item)} lastMessage={JSON.parse(JSON.stringify(item.lastMessage))}
_updatedAt={item.roomUpdatedAt} name={this.getRoomTitle(item)}
key={item._id} _updatedAt={item.roomUpdatedAt}
id={id} key={item._id}
type={item.t} id={id}
baseUrl={baseUrl} type={item.t}
prid={item.prid} baseUrl={baseUrl}
showLastMessage={StoreLastMessage} prid={item.prid}
onPress={() => this._onPressItem(item)} showLastMessage={StoreLastMessage}
testID={`rooms-list-view-item-${ item.name }`} onPress={() => this._onPressItem(item)}
height={ROW_HEIGHT} testID={`rooms-list-view-item-${ item.name }`}
/> height={ROW_HEIGHT}
); />
);
}
return null;
} }
renderSectionHeader = header => ( renderSectionHeader = header => (
@ -481,11 +438,10 @@ export default class RoomsListView extends LoggedView {
} else if (header === 'Chats' && groupByType) { } else if (header === 'Chats' && groupByType) {
return null; return null;
} }
if (data.length > 0) { if (data && data.length > 0) {
return ( return (
<FlatList <FlatList
data={data} data={data}
extraData={data}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
style={styles.list} style={styles.list}
renderItem={this.renderItem} renderItem={this.renderItem}
@ -553,7 +509,6 @@ export default class RoomsListView extends LoggedView {
<FlatList <FlatList
ref={this.getScrollRef} ref={this.getScrollRef}
data={search.length ? search : chats} data={search.length ? search : chats}
extraData={search.length ? search : chats}
contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}} contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}}
keyExtractor={keyExtractor} keyExtractor={keyExtractor}
style={styles.list} style={styles.list}

View File

@ -5,8 +5,6 @@ import {
} from 'react-native'; } from 'react-native';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation'; import { SafeAreaView } from 'react-navigation';
import equal from 'deep-equal';
import EJSON from 'ejson';
import moment from 'moment'; import moment from 'moment';
import LoggedView from '../View'; import LoggedView from '../View';
@ -22,6 +20,7 @@ import log from '../../utils/log';
import debounce from '../../utils/debounce'; import debounce from '../../utils/debounce';
const Separator = React.memo(() => <View style={styles.separator} />); const Separator = React.memo(() => <View style={styles.separator} />);
const API_FETCH_COUNT = 50;
@connect(state => ({ @connect(state => ({
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '', baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
@ -47,72 +46,136 @@ export default class ThreadMessagesView extends LoggedView {
super('ThreadMessagesView', props); super('ThreadMessagesView', props);
this.rid = props.navigation.getParam('rid'); this.rid = props.navigation.getParam('rid');
this.t = props.navigation.getParam('t'); this.t = props.navigation.getParam('t');
this.messages = database.objects('threads').filtered('rid = $0', this.rid); this.rooms = database.objects('subscriptions').filtered('rid = $0', this.rid);
this.messages = database.objects('threads').filtered('rid = $0', this.rid).sorted('ts', true);
safeAddListener(this.messages, this.updateMessages); safeAddListener(this.messages, this.updateMessages);
this.state = { this.state = {
loading: false, loading: false,
messages: this.messages.slice(),
end: false, end: false,
total: 0 messages: this.messages
}; };
this.mounted = false;
} }
componentDidMount() { componentDidMount() {
this.load(); this.mountInteraction = InteractionManager.runAfterInteractions(() => {
this.init();
this.mounted = true;
});
} }
shouldComponentUpdate(nextProps, nextState) { componentWillUnmount() {
const { loading, messages, end } = this.state; this.messages.removeAllListeners();
if (nextState.loading !== loading) { if (this.mountInteraction && this.mountInteraction.cancel) {
return true; this.mountInteraction.cancel();
} }
if (!equal(nextState.messages, messages)) { if (this.loadInteraction && this.loadInteraction.cancel) {
return true; this.loadInteraction.cancel();
} }
if (!equal(nextState.end, end)) { if (this.syncInteraction && this.syncInteraction.cancel) {
return true; this.syncInteraction.cancel();
} }
return false;
} }
updateMessages = () => { // eslint-disable-next-line react/sort-comp
this.setState({ messages: this.messages.slice() }); updateMessages = debounce(() => {
this.setState({ messages: this.messages });
}, 300)
init = () => {
const [room] = this.rooms;
const lastThreadSync = new Date();
if (room.lastThreadSync) {
this.sync(room.lastThreadSync);
} else {
this.load();
}
database.write(() => {
room.lastThreadSync = lastThreadSync;
});
} }
// eslint-disable-next-line react/sort-comp // eslint-disable-next-line react/sort-comp
load = debounce(async() => { load = debounce(async() => {
const { const { loading, end } = this.state;
loading, end, total if (end || loading || !this.mounted) {
} = this.state;
if (end || loading) {
return; return;
} }
this.setState({ loading: true }); this.setState({ loading: true });
try { try {
const result = await RocketChat.getThreadsList({ rid: this.rid, limit: 50, skip: total }); const result = await RocketChat.getThreadsList({
rid: this.rid, count: API_FETCH_COUNT, offset: this.messages.length
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
}));
}); });
if (result.success) {
this.loadInteraction = InteractionManager.runAfterInteractions(() => {
database.write(() => result.threads.forEach((message) => {
try {
database.create('threads', buildMessage(message), true);
} catch (e) {
log('ThreadMessagesView -> load -> create', e);
}
}));
this.setState({
loading: false,
end: result.count < API_FETCH_COUNT
});
});
}
} catch (error) { } catch (error) {
console.log('ThreadMessagesView -> catch -> error', error); console.log('ThreadMessagesView -> load -> error', error);
this.setState({ loading: false, end: true }); this.setState({ loading: false, end: true });
} }
}, 300, true) }, 300)
// eslint-disable-next-line react/sort-comp
sync = async(updatedSince) => {
this.setState({ loading: true });
try {
const result = await RocketChat.getSyncThreadsList({
rid: this.rid, updatedSince: updatedSince.toISOString()
});
if (result.success && result.threads) {
this.syncInteraction = InteractionManager.runAfterInteractions(() => {
const { update, remove } = result.threads;
database.write(() => {
if (update && update.length) {
update.forEach((message) => {
try {
database.create('threads', buildMessage(message), true);
} catch (e) {
log('ThreadMessagesView -> sync -> update', e);
}
});
}
if (remove && remove.length) {
remove.forEach((message) => {
const oldMessage = database.objectForPrimaryKey('threads', message._id);
if (oldMessage) {
try {
database.delete(oldMessage);
} catch (e) {
log('ThreadMessagesView -> sync -> delete', e);
}
}
});
}
});
this.setState({
loading: false
});
});
}
} catch (error) {
console.log('ThreadMessagesView -> sync -> error', error);
this.setState({ loading: false });
}
}
formatMessage = lm => ( formatMessage = lm => (
lm ? moment(lm).calendar(null, { lm ? moment(lm).calendar(null, {
@ -133,28 +196,31 @@ export default class ThreadMessagesView extends LoggedView {
renderItem = ({ item }) => { renderItem = ({ item }) => {
const { user, navigation } = this.props; const { user, navigation } = this.props;
return ( if (item.isValid && item.isValid()) {
<Message return (
key={item._id} <Message
item={item} key={item._id}
user={user} item={item}
archived={false} user={user}
broadcast={false} archived={false}
status={item.status} broadcast={false}
_updatedAt={item._updatedAt} status={item.status}
navigation={navigation} _updatedAt={item._updatedAt}
customTimeFormat='MMM D' navigation={navigation}
customThreadTimeFormat='MMM Do YYYY, h:mm:ss a' customTimeFormat='MMM D'
fetchThreadName={this.fetchThreadName} customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
onDiscussionPress={this.onDiscussionPress} fetchThreadName={this.fetchThreadName}
/> onDiscussionPress={this.onDiscussionPress}
); />
);
}
return null;
} }
render() { render() {
const { messages, loading } = this.state; const { loading, messages } = this.state;
if (!loading && messages.length === 0) { if (!loading && this.messages.length === 0) {
return this.renderEmpty(); return this.renderEmpty();
} }
@ -163,6 +229,7 @@ export default class ThreadMessagesView extends LoggedView {
<StatusBar /> <StatusBar />
<FlatList <FlatList
data={messages} data={messages}
extraData={this.state}
renderItem={this.renderItem} renderItem={this.renderItem}
style={styles.list} style={styles.list}
contentContainerStyle={styles.contentContainer} contentContainerStyle={styles.contentContainer}

View File

@ -284,10 +284,10 @@ describe('Room screen', () => {
await element(by.text('Reply')).tap(); await element(by.text('Reply')).tap();
await element(by.id('messagebox-input')).typeText('replied'); await element(by.id('messagebox-input')).typeText('replied');
await element(by.id('messagebox-send-message')).tap(); await element(by.id('messagebox-send-message')).tap();
await waitFor(element(by.id(`message-thread-button-${ thread }`))).toBeVisible().withTimeout(5000); await waitFor(element(by.id(`message-thread-button-${ thread }`))).toExist().withTimeout(5000);
await expect(element(by.id(`message-thread-button-${ thread }`))).toBeVisible(); await expect(element(by.id(`message-thread-button-${ thread }`))).toExist();
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toBeVisible().withTimeout(5000); await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toBeVisible(); await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toExist();
}); });
it('should navigate to thread from button', async() => { it('should navigate to thread from button', async() => {

View File

@ -263,7 +263,6 @@ export default (
header={false} header={false}
/> />
{/* Legacy thread */}
<Separator title='Message with reply' /> <Separator title='Message with reply' />
<Message <Message
msg="I'm fine!" msg="I'm fine!"
@ -290,6 +289,11 @@ export default (
tcount={1} tcount={1}
tlm={date} tlm={date}
/> />
<Message
msg='How are you?'
tcount={9999}
tlm={date}
/>
<Message <Message
msg="I'm fine!" msg="I'm fine!"
tmid='1' tmid='1'
@ -316,15 +320,15 @@ export default (
tmsg={longText} tmsg={longText}
/> />
<Message <Message
msg='How are you?' tmid='1'
tcount={0} tmsg='Thread with attachment'
tlm={date} attachments={[{
/> title: 'This is a title',
<Message description: 'This is a description',
msg='How are you?' audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
tcount={9999} }]}
tlm={date}
/> />
{/* <Message {/* <Message
msg='How are you?' msg='How are you?'
tcount={9999} tcount={9999}