[FIX] Threads (#838)
Closes #826 Closes #827 Closes #828 Closes #829 Closes #830 Closes #831 Closes #832 Closes #833
This commit is contained in:
parent
0266cc2e01
commit
5744114d7d
File diff suppressed because it is too large
Load Diff
|
@ -11,6 +11,7 @@ export const COLOR_TEXT_DESCRIPTION = '#9ca2a8';
|
|||
export const COLOR_SEPARATOR = '#A7A7AA';
|
||||
export const COLOR_BACKGROUND_CONTAINER = '#f3f4f5';
|
||||
export const COLOR_BORDER = '#e1e5e8';
|
||||
export const COLOR_UNREAD = '#e1e5e8';
|
||||
export const STATUS_COLORS = {
|
||||
online: '#2de0a5',
|
||||
busy: COLOR_DANGER,
|
||||
|
|
|
@ -57,12 +57,14 @@ class MessageBox extends Component {
|
|||
replying: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
threadsEnabled: PropTypes.bool,
|
||||
isFocused: PropTypes.bool,
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
username: PropTypes.string,
|
||||
token: PropTypes.string
|
||||
}),
|
||||
roomType: PropTypes.string,
|
||||
tmid: PropTypes.string,
|
||||
editCancel: PropTypes.func.isRequired,
|
||||
editRequest: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
|
@ -92,23 +94,37 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { rid } = this.props;
|
||||
const [room] = database.objects('subscriptions').filtered('rid = $0', rid);
|
||||
if (room && room.draftMessage) {
|
||||
this.setInput(room.draftMessage);
|
||||
const { rid, tmid } = this.props;
|
||||
let msg;
|
||||
if (tmid) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const { message, replyMessage } = this.props;
|
||||
if (message !== nextProps.message && nextProps.message.msg) {
|
||||
const { message, replyMessage, isFocused } = this.props;
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
if (!equal(message, nextProps.message) && nextProps.message.msg) {
|
||||
this.setInput(nextProps.message.msg);
|
||||
if (this.text) {
|
||||
this.setShowSend(true);
|
||||
}
|
||||
this.focus();
|
||||
} else if (replyMessage !== nextProps.replyMessage && nextProps.replyMessage.msg) {
|
||||
} else if (!equal(replyMessage, nextProps.replyMessage)) {
|
||||
this.focus();
|
||||
} else if (!nextProps.message) {
|
||||
this.clearInput();
|
||||
|
@ -120,8 +136,11 @@ class MessageBox extends Component {
|
|||
showEmojiKeyboard, showFilesAction, showSend, recording, mentions, file
|
||||
} = this.state;
|
||||
const {
|
||||
roomType, replying, editing
|
||||
roomType, replying, editing, isFocused
|
||||
} = this.props;
|
||||
if (!isFocused) {
|
||||
return false;
|
||||
}
|
||||
if (nextProps.roomType !== roomType) {
|
||||
return true;
|
||||
}
|
||||
|
@ -481,7 +500,7 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
sendImageMessage = async(file) => {
|
||||
const { rid } = this.props;
|
||||
const { rid, tmid } = this.props;
|
||||
|
||||
this.setState({ file: { isVisible: false } });
|
||||
const fileInfo = {
|
||||
|
@ -493,7 +512,7 @@ class MessageBox extends Component {
|
|||
path: file.path
|
||||
};
|
||||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo);
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
|
||||
} catch (e) {
|
||||
log('sendImageMessage', e);
|
||||
}
|
||||
|
@ -539,14 +558,14 @@ class MessageBox extends Component {
|
|||
}
|
||||
|
||||
finishAudioMessage = async(fileInfo) => {
|
||||
const { rid } = this.props;
|
||||
const { rid, tmid } = this.props;
|
||||
|
||||
this.setState({
|
||||
recording: false
|
||||
});
|
||||
if (fileInfo) {
|
||||
try {
|
||||
await RocketChat.sendFileMessage(rid, fileInfo);
|
||||
await RocketChat.sendFileMessage(rid, fileInfo, tmid);
|
||||
} catch (e) {
|
||||
if (e && e.error === 'error-file-too-large') {
|
||||
return Alert.alert(I18n.t(e.error));
|
||||
|
@ -830,7 +849,7 @@ class MessageBox extends Component {
|
|||
const mapStateToProps = state => ({
|
||||
message: state.messages.message,
|
||||
replyMessage: state.messages.replyMessage,
|
||||
replying: state.messages.replyMessage && !!state.messages.replyMessage.msg,
|
||||
replying: state.messages.replying,
|
||||
editing: state.messages.editing,
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
threadsEnabled: state.settings.Threads_enabled,
|
||||
|
|
|
@ -6,8 +6,8 @@ import {
|
|||
import Video from 'react-native-video';
|
||||
import Slider from 'react-native-slider';
|
||||
import moment from 'moment';
|
||||
import { BorderlessButton } from 'react-native-gesture-handler';
|
||||
import equal from 'deep-equal';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
|
||||
import Markdown from './Markdown';
|
||||
import { CustomIcon } from '../../lib/Icons';
|
||||
|
@ -27,7 +27,7 @@ const styles = StyleSheet.create({
|
|||
marginBottom: 6
|
||||
},
|
||||
playPauseButton: {
|
||||
width: 56,
|
||||
marginHorizontal: 10,
|
||||
alignItems: 'center',
|
||||
backgroundColor: 'transparent'
|
||||
},
|
||||
|
@ -35,11 +35,10 @@ const styles = StyleSheet.create({
|
|||
color: COLOR_PRIMARY
|
||||
},
|
||||
slider: {
|
||||
flex: 1,
|
||||
marginRight: 10
|
||||
flex: 1
|
||||
},
|
||||
duration: {
|
||||
marginRight: 16,
|
||||
marginHorizontal: 12,
|
||||
fontSize: 14,
|
||||
...sharedStyles.textColorNormal,
|
||||
...sharedStyles.textRegular
|
||||
|
@ -47,10 +46,16 @@ const styles = StyleSheet.create({
|
|||
thumbStyle: {
|
||||
width: 12,
|
||||
height: 12
|
||||
},
|
||||
trackStyle: {
|
||||
height: 2
|
||||
}
|
||||
});
|
||||
|
||||
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 {
|
||||
static propTypes = {
|
||||
|
@ -97,30 +102,30 @@ export default class Audio extends React.Component {
|
|||
return false;
|
||||
}
|
||||
|
||||
onLoad(data) {
|
||||
onLoad = (data) => {
|
||||
this.setState({ duration: data.duration > 0 ? data.duration : 0 });
|
||||
}
|
||||
|
||||
onProgress(data) {
|
||||
onProgress = (data) => {
|
||||
const { duration } = this.state;
|
||||
if (data.currentTime <= duration) {
|
||||
this.setState({ currentTime: data.currentTime });
|
||||
}
|
||||
}
|
||||
|
||||
onEnd() {
|
||||
onEnd = () => {
|
||||
this.setState({ paused: true, currentTime: 0 });
|
||||
requestAnimationFrame(() => {
|
||||
this.player.seek(0);
|
||||
});
|
||||
}
|
||||
|
||||
getDuration() {
|
||||
getDuration = () => {
|
||||
const { duration } = this.state;
|
||||
return formatTime(duration);
|
||||
}
|
||||
|
||||
togglePlayPause() {
|
||||
togglePlayPause = () => {
|
||||
const { paused } = this.state;
|
||||
this.setState({ paused: !paused });
|
||||
}
|
||||
|
@ -152,16 +157,18 @@ export default class Audio extends React.Component {
|
|||
paused={paused}
|
||||
repeat={false}
|
||||
/>
|
||||
<BorderlessButton
|
||||
<Touchable
|
||||
style={styles.playPauseButton}
|
||||
onPress={() => this.togglePlayPause()}
|
||||
onPress={this.togglePlayPause}
|
||||
hitSlop={BUTTON_HIT_SLOP}
|
||||
background={Touchable.SelectableBackgroundBorderless()}
|
||||
>
|
||||
{
|
||||
paused
|
||||
? <CustomIcon name='play' size={30} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={30} style={styles.playPauseImage} />
|
||||
? <CustomIcon name='play' size={36} style={styles.playPauseImage} />
|
||||
: <CustomIcon name='pause' size={36} style={styles.playPauseImage} />
|
||||
}
|
||||
</BorderlessButton>
|
||||
</Touchable>
|
||||
<Slider
|
||||
style={styles.slider}
|
||||
value={currentTime}
|
||||
|
@ -177,6 +184,7 @@ export default class Audio extends React.Component {
|
|||
minimumTrackTintColor={COLOR_PRIMARY}
|
||||
onValueChange={value => this.setState({ currentTime: value })}
|
||||
thumbStyle={styles.thumbStyle}
|
||||
trackStyle={styles.trackStyle}
|
||||
/>
|
||||
<Text style={styles.duration}>{this.getDuration()}</Text>
|
||||
</View>,
|
||||
|
|
|
@ -22,7 +22,7 @@ export default class Markdown extends React.Component {
|
|||
|
||||
render() {
|
||||
const {
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited
|
||||
msg, customEmojis, style, rules, baseUrl, username, edited, numberOfLines
|
||||
} = this.props;
|
||||
if (!msg) {
|
||||
return null;
|
||||
|
@ -32,12 +32,15 @@ export default class Markdown extends React.Component {
|
|||
m = emojify(m, { output: 'unicode' });
|
||||
}
|
||||
m = m.replace(/^\[([^\]]*)\]\(([^)]*)\)/, '').trim();
|
||||
if (numberOfLines > 0) {
|
||||
m = m.replace(/[\n]+/g, '\n').trim();
|
||||
}
|
||||
return (
|
||||
<MarkdownRenderer
|
||||
rules={{
|
||||
paragraph: (node, children) => (
|
||||
// eslint-disable-next-line
|
||||
<Text key={node.key} style={styles.paragraph}>
|
||||
<Text key={node.key} style={styles.paragraph} numberOfLines={numberOfLines}>
|
||||
{children}
|
||||
{edited ? <Text style={styles.edited}> (edited)</Text> : null}
|
||||
</Text>
|
||||
|
@ -111,5 +114,6 @@ Markdown.propTypes = {
|
|||
customEmojis: PropTypes.object.isRequired,
|
||||
style: PropTypes.any,
|
||||
rules: PropTypes.object,
|
||||
edited: PropTypes.bool
|
||||
edited: PropTypes.bool,
|
||||
numberOfLines: PropTypes.number
|
||||
};
|
||||
|
|
|
@ -5,10 +5,8 @@ import {
|
|||
} from 'react-native';
|
||||
import moment from 'moment';
|
||||
import { KeyboardUtils } from 'react-native-keyboard-input';
|
||||
import {
|
||||
BorderlessButton
|
||||
} from 'react-native-gesture-handler';
|
||||
import Touchable from 'react-native-platform-touchable';
|
||||
import { emojify } from 'react-emojione';
|
||||
|
||||
import Image from './Image';
|
||||
import User from './User';
|
||||
|
@ -164,6 +162,11 @@ export default class Message extends PureComponent {
|
|||
|
||||
onPress = () => {
|
||||
KeyboardUtils.dismiss();
|
||||
|
||||
const { onThreadPress, tlm, tmid } = this.props;
|
||||
if ((tlm || tmid) && onThreadPress) {
|
||||
onThreadPress();
|
||||
}
|
||||
}
|
||||
|
||||
onLongPress = () => {
|
||||
|
@ -269,10 +272,25 @@ export default class Message extends PureComponent {
|
|||
if (this.isInfoMessage()) {
|
||||
return <Text style={styles.textInfo}>{getInfoMessage({ ...this.props })}</Text>;
|
||||
}
|
||||
|
||||
const {
|
||||
customEmojis, msg, baseUrl, user, edited
|
||||
customEmojis, msg, baseUrl, user, edited, tmid
|
||||
} = 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() {
|
||||
|
@ -316,9 +334,9 @@ export default class Message extends PureComponent {
|
|||
}
|
||||
const { onErrorPress } = this.props;
|
||||
return (
|
||||
<BorderlessButton onPress={onErrorPress} style={styles.errorButton}>
|
||||
<Touchable onPress={onErrorPress} style={styles.errorButton}>
|
||||
<CustomIcon name='circle-cross' color={COLOR_DANGER} size={20} />
|
||||
</BorderlessButton>
|
||||
</Touchable>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -457,7 +475,7 @@ export default class Message extends PureComponent {
|
|||
|
||||
renderRepliedThread = () => {
|
||||
const {
|
||||
tmid, tmsg, header, onThreadPress, fetchThreadName
|
||||
tmid, tmsg, header, fetchThreadName
|
||||
} = this.props;
|
||||
if (!tmid || !header || this.isTemp()) {
|
||||
return null;
|
||||
|
@ -468,15 +486,18 @@ export default class Message extends PureComponent {
|
|||
return null;
|
||||
}
|
||||
|
||||
const msg = emojify(tmsg, { output: 'unicode' });
|
||||
|
||||
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>
|
||||
<View style={styles.repliedThread} testID={`message-thread-replied-on-${ msg }`}>
|
||||
<CustomIcon name='thread' size={20} style={[styles.buttonIcon, styles.repliedThreadIcon]} />
|
||||
<Text style={styles.repliedThreadName} numberOfLines={1}>{msg}</Text>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
renderInner = () => {
|
||||
const { type } = this.props;
|
||||
const { type, tmid } = this.props;
|
||||
if (type === 'discussion-created') {
|
||||
return (
|
||||
<React.Fragment>
|
||||
|
@ -485,10 +506,18 @@ export default class Message extends PureComponent {
|
|||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
if (tmid) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderRepliedThread()}
|
||||
{this.renderContent()}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<React.Fragment>
|
||||
{this.renderUsername()}
|
||||
{this.renderRepliedThread()}
|
||||
{this.renderContent()}
|
||||
{this.renderAttachment()}
|
||||
{this.renderUrl()}
|
||||
|
|
|
@ -76,7 +76,7 @@ export default class MessageContainer extends React.Component {
|
|||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { reactionsModal } = this.state;
|
||||
const {
|
||||
status, editingMessage, item, _updatedAt
|
||||
status, editingMessage, item, _updatedAt, navigation
|
||||
} = this.props;
|
||||
|
||||
if (reactionsModal !== nextState.reactionsModal) {
|
||||
|
@ -89,7 +89,7 @@ export default class MessageContainer extends React.Component {
|
|||
return true;
|
||||
}
|
||||
|
||||
if (!equal(editingMessage, nextProps.editingMessage)) {
|
||||
if (navigation.isFocused() && !equal(editingMessage, nextProps.editingMessage)) {
|
||||
if (nextProps.editingMessage && nextProps.editingMessage._id === item._id) {
|
||||
return true;
|
||||
} else if (!nextProps.editingMessage._id !== item._id && editingMessage._id === item._id) {
|
||||
|
|
|
@ -216,13 +216,16 @@ export default StyleSheet.create({
|
|||
fontWeight: '300'
|
||||
},
|
||||
repliedThread: {
|
||||
fontSize: 16,
|
||||
marginBottom: 6,
|
||||
...sharedStyles.textColorDescription,
|
||||
...sharedStyles.textRegular
|
||||
flexDirection: 'row',
|
||||
flex: 1
|
||||
},
|
||||
repliedThreadIcon: {
|
||||
color: COLOR_PRIMARY
|
||||
},
|
||||
repliedThreadName: {
|
||||
fontSize: 16,
|
||||
fontStyle: 'normal',
|
||||
flex: 1,
|
||||
color: COLOR_PRIMARY,
|
||||
...sharedStyles.textSemibold
|
||||
}
|
||||
|
|
|
@ -293,6 +293,7 @@ export default {
|
|||
Send: 'Send',
|
||||
Send_audio_message: 'Send audio message',
|
||||
Send_message: 'Send message',
|
||||
Sent_an_attachment: 'Sent an attachment',
|
||||
Server: 'Server',
|
||||
Servers: 'Servers',
|
||||
Set_username_subtitle: 'The username is used to allow others to mention you in messages',
|
||||
|
|
|
@ -295,6 +295,7 @@ export default {
|
|||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
Sent_an_attachment: 'Enviou um anexo',
|
||||
Server: 'Servidor',
|
||||
Set_username_subtitle: 'O usuário é utilizado para permitir que você seja mencionado em mensagens',
|
||||
Settings: 'Configurações',
|
||||
|
|
|
@ -284,6 +284,7 @@ export default {
|
|||
Send: 'Enviar',
|
||||
Send_audio_message: 'Enviar mensagem de áudio',
|
||||
Send_message: 'Enviar mensagem',
|
||||
Sent_an_attachment: 'Enviou um ficheiro',
|
||||
Server: 'Servidor',
|
||||
Servers: 'Servidores',
|
||||
Set_username_subtitle: 'O nome de utilizador é usado para permitir que outros mencionem você em mensagens',
|
||||
|
|
|
@ -145,7 +145,7 @@ const ProfileStack = createStackNavigator({
|
|||
defaultNavigationOptions: defaultHeader
|
||||
});
|
||||
|
||||
ProfileView.navigationOptions = ({ navigation }) => {
|
||||
ProfileStack.navigationOptions = ({ navigation }) => {
|
||||
let drawerLockMode = 'unlocked';
|
||||
if (navigation.state.index > 0) {
|
||||
drawerLockMode = 'locked-closed';
|
||||
|
|
|
@ -46,6 +46,11 @@ export default function loadMessagesForRoom(...args) {
|
|||
if (message.tlm) {
|
||||
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) {
|
||||
log('loadMessagesForRoom -> create messages', e);
|
||||
}
|
||||
|
|
|
@ -40,11 +40,14 @@ export default function loadMissedMessages(...args) {
|
|||
if (message.tlm) {
|
||||
database.create('threads', message, true);
|
||||
}
|
||||
if (message.tmid) {
|
||||
message.rid = message.tmid;
|
||||
database.create('threadMessages', message, true);
|
||||
}
|
||||
} catch (e) {
|
||||
log('loadMissedMessages -> create messages', e);
|
||||
}
|
||||
}));
|
||||
resolve(updated);
|
||||
});
|
||||
}
|
||||
if (data.deleted && data.deleted.length) {
|
||||
|
@ -55,6 +58,10 @@ export default function loadMissedMessages(...args) {
|
|||
deleted.forEach((m) => {
|
||||
const message = database.objects('messages').filtered('_id = $0', m._id);
|
||||
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) {
|
||||
|
@ -63,7 +70,7 @@ export default function loadMissedMessages(...args) {
|
|||
});
|
||||
}
|
||||
}
|
||||
resolve([]);
|
||||
resolve();
|
||||
} catch (e) {
|
||||
log('loadMissedMessages', e);
|
||||
reject(e);
|
||||
|
|
|
@ -5,24 +5,26 @@ import buildMessage from './helpers/buildMessage';
|
|||
import database from '../realm';
|
||||
import log from '../../utils/log';
|
||||
|
||||
async function load({ tmid, skip }) {
|
||||
async function load({ tmid, offset }) {
|
||||
try {
|
||||
// RC 1.0
|
||||
const data = await this.sdk.methodCall('getThreadMessages', { tmid, limit: 50, skip });
|
||||
if (!data || data.status === 'error') {
|
||||
const result = await this.sdk.get('chat.getThreadMessages', {
|
||||
tmid, count: 50, offset, sort: { ts: -1 }
|
||||
});
|
||||
if (!result || !result.success) {
|
||||
return [];
|
||||
}
|
||||
return data;
|
||||
return result.messages;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default function loadThreadMessages({ tmid, skip }) {
|
||||
export default function loadThreadMessages({ tmid, offset = 0 }) {
|
||||
return new Promise(async(resolve, reject) => {
|
||||
try {
|
||||
const data = await load.call(this, { tmid, skip });
|
||||
const data = await load.call(this, { tmid, offset });
|
||||
|
||||
if (data && data.length) {
|
||||
InteractionManager.runAfterInteractions(() => {
|
||||
|
|
|
@ -29,7 +29,7 @@ export async function cancelUpload(path) {
|
|||
}
|
||||
}
|
||||
|
||||
export async function sendFileMessage(rid, fileInfo) {
|
||||
export async function sendFileMessage(rid, fileInfo, tmid) {
|
||||
try {
|
||||
const data = await RNFetchBlob.wrap(fileInfo.path);
|
||||
if (!fileInfo.size) {
|
||||
|
@ -86,6 +86,8 @@ export async function sendFileMessage(rid, fileInfo) {
|
|||
name: completeResult.name,
|
||||
description: completeResult.description,
|
||||
url: completeResult.path
|
||||
}, {
|
||||
tmid
|
||||
});
|
||||
|
||||
database.write(() => {
|
||||
|
|
|
@ -104,7 +104,8 @@ const subscriptionSchema = {
|
|||
muted: { type: 'list', objectType: 'usersMuted' },
|
||||
broadcast: { type: 'bool', 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 },
|
||||
tcount: { type: 'int', optional: true },
|
||||
tlm: { type: 'date', optional: true },
|
||||
replies: 'string[]'
|
||||
replies: 'string[]',
|
||||
draftMessage: 'string?'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -387,9 +389,9 @@ class DB {
|
|||
schema: [
|
||||
serversSchema
|
||||
],
|
||||
schemaVersion: 4,
|
||||
schemaVersion: 5,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 3) {
|
||||
if (oldRealm.schemaVersion >= 1 && newRealm.schemaVersion <= 5) {
|
||||
const newServers = newRealm.objects('servers');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
@ -441,15 +443,12 @@ class DB {
|
|||
|
||||
setActiveDB(database = '') {
|
||||
const path = database.replace(/(^\w+:|^)\/\//, '');
|
||||
if (this.database) {
|
||||
this.database.close();
|
||||
}
|
||||
return this.databases.activeDB = new Realm({
|
||||
path: `${ path }.realm`,
|
||||
schema,
|
||||
schemaVersion: 6,
|
||||
schemaVersion: 8,
|
||||
migration: (oldRealm, newRealm) => {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 6) {
|
||||
if (oldRealm.schemaVersion >= 3 && newRealm.schemaVersion <= 8) {
|
||||
const newSubs = newRealm.objects('subscriptions');
|
||||
|
||||
// eslint-disable-next-line no-plusplus
|
||||
|
@ -459,6 +458,10 @@ class DB {
|
|||
}
|
||||
const newMessages = newRealm.objects('messages');
|
||||
newRealm.delete(newMessages);
|
||||
const newThreads = newRealm.objects('threads');
|
||||
newRealm.delete(newThreads);
|
||||
const newThreadMessages = newRealm.objects('threadMessages');
|
||||
newRealm.delete(newThreadMessages);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -416,8 +416,6 @@ const RocketChat = {
|
|||
data = data.filtered('t != $0', 'd');
|
||||
}
|
||||
data = data.slice(0, 7);
|
||||
const array = Array.from(data);
|
||||
data = JSON.parse(JSON.stringify(array));
|
||||
|
||||
const usernames = data.map(sub => sub.name);
|
||||
try {
|
||||
|
@ -782,9 +780,17 @@ const RocketChat = {
|
|||
}
|
||||
return this.sdk.methodCall('unfollowMessage', { mid });
|
||||
},
|
||||
getThreadsList({ rid, limit, skip }) {
|
||||
getThreadsList({ rid, count, offset }) {
|
||||
// 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
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ const formatMsg = ({
|
|||
const arePropsEqual = (oldProps, newProps) => _.isEqual(oldProps, newProps);
|
||||
|
||||
const LastMessage = React.memo(({
|
||||
lastMessage, type, showLastMessage, username
|
||||
lastMessage, type, showLastMessage, username, alert
|
||||
}) => (
|
||||
<Text style={[styles.markdownText, alert && styles.markdownTextAlert]} numberOfLines={2}>
|
||||
{formatMsg({
|
||||
|
@ -54,7 +54,8 @@ LastMessage.propTypes = {
|
|||
lastMessage: PropTypes.object,
|
||||
type: PropTypes.string,
|
||||
showLastMessage: PropTypes.bool,
|
||||
username: PropTypes.string
|
||||
username: PropTypes.string,
|
||||
alert: PropTypes.bool
|
||||
};
|
||||
|
||||
export default LastMessage;
|
||||
|
|
|
@ -14,8 +14,8 @@ const UnreadBadge = React.memo(({ unread, userMentions, type }) => {
|
|||
const mentioned = userMentions > 0 && type !== 'd';
|
||||
|
||||
return (
|
||||
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentioned]}>
|
||||
<Text style={styles.unreadNumberText}>{ unread }</Text>
|
||||
<View style={[styles.unreadNumberContainer, mentioned && styles.unreadMentionedContainer]}>
|
||||
<Text style={[styles.unreadText, mentioned && styles.unreadMentionedText]}>{ unread }</Text>
|
||||
</View>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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}
|
||||
</View>
|
||||
<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} />
|
||||
</View>
|
||||
</View>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { StyleSheet, PixelRatio } from 'react-native';
|
|||
|
||||
import sharedStyles from '../../views/Styles';
|
||||
import {
|
||||
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_TEXT
|
||||
COLOR_SEPARATOR, COLOR_PRIMARY, COLOR_WHITE, COLOR_UNREAD, COLOR_TEXT
|
||||
} from '../../constants/colors';
|
||||
|
||||
export const ROW_HEIGHT = 75 * PixelRatio.getFontScale();
|
||||
|
@ -53,27 +53,30 @@ export default StyleSheet.create({
|
|||
...sharedStyles.textSemibold
|
||||
},
|
||||
unreadNumberContainer: {
|
||||
minWidth: 22,
|
||||
height: 22,
|
||||
minWidth: 21,
|
||||
height: 21,
|
||||
paddingVertical: 3,
|
||||
paddingHorizontal: 5,
|
||||
borderRadius: 14,
|
||||
backgroundColor: COLOR_TEXT,
|
||||
borderRadius: 10.5,
|
||||
backgroundColor: COLOR_UNREAD,
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
marginLeft: 10
|
||||
},
|
||||
unreadMentioned: {
|
||||
unreadMentionedContainer: {
|
||||
backgroundColor: COLOR_PRIMARY
|
||||
},
|
||||
unreadNumberText: {
|
||||
color: COLOR_WHITE,
|
||||
unreadText: {
|
||||
color: COLOR_TEXT,
|
||||
overflow: 'hidden',
|
||||
fontSize: 13,
|
||||
...sharedStyles.textRegular,
|
||||
...sharedStyles.textMedium,
|
||||
letterSpacing: 0.56,
|
||||
textAlign: 'center'
|
||||
},
|
||||
unreadMentionedText: {
|
||||
color: COLOR_WHITE
|
||||
},
|
||||
status: {
|
||||
marginRight: 7,
|
||||
marginTop: 3
|
||||
|
|
|
@ -4,6 +4,7 @@ const initialState = {
|
|||
message: {},
|
||||
actionMessage: {},
|
||||
replyMessage: {},
|
||||
replying: false,
|
||||
editing: false,
|
||||
showActions: false,
|
||||
showErrorActions: false,
|
||||
|
@ -64,12 +65,14 @@ export default function messages(state = initialState, action) {
|
|||
replyMessage: {
|
||||
...action.message,
|
||||
mention: action.mention
|
||||
}
|
||||
},
|
||||
replying: true
|
||||
};
|
||||
case types.MESSAGES.REPLY_CANCEL:
|
||||
return {
|
||||
...state,
|
||||
replyMessage: {}
|
||||
replyMessage: {},
|
||||
replying: false
|
||||
};
|
||||
case types.MESSAGES.SET_INPUT:
|
||||
return {
|
||||
|
|
|
@ -10,10 +10,14 @@ import log from '../../../utils/log';
|
|||
|
||||
const styles = StyleSheet.create({
|
||||
more: {
|
||||
marginHorizontal: 0, marginLeft: 0, marginRight: 5
|
||||
marginHorizontal: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 5
|
||||
},
|
||||
thread: {
|
||||
marginHorizontal: 0, marginLeft: 0, marginRight: 10
|
||||
marginHorizontal: 0,
|
||||
marginLeft: 0,
|
||||
marginRight: 15
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -34,6 +38,7 @@ class RightButtonsContainer extends React.PureComponent {
|
|||
constructor(props) {
|
||||
super(props);
|
||||
if (props.tmid) {
|
||||
// FIXME: it may be empty if the thread header isn't fetched yet
|
||||
this.thread = database.objectForPrimaryKey('messages', props.tmid);
|
||||
safeAddListener(this.thread, this.updateThread);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
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';
|
||||
|
@ -30,7 +29,7 @@ export class List extends React.PureComponent {
|
|||
.objects('threadMessages')
|
||||
.filtered('rid = $0', props.tmid)
|
||||
.sorted('ts', true);
|
||||
this.threads = [];
|
||||
this.threads = database.objects('threads').filtered('_id = $0', props.tmid);
|
||||
} else {
|
||||
this.data = database
|
||||
.objects('messages')
|
||||
|
@ -83,7 +82,7 @@ export class List extends React.PureComponent {
|
|||
});
|
||||
}, 300, { leading: true });
|
||||
|
||||
onEndReached = async() => {
|
||||
onEndReached = debounce(async() => {
|
||||
const {
|
||||
loading, end, messages
|
||||
} = this.state;
|
||||
|
@ -96,17 +95,17 @@ export class List extends React.PureComponent {
|
|||
try {
|
||||
let result;
|
||||
if (tmid) {
|
||||
result = await RocketChat.loadThreadMessages({ tmid, skip: messages.length });
|
||||
result = await RocketChat.loadThreadMessages({ tmid, offset: messages.length });
|
||||
} else {
|
||||
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) {
|
||||
this.setState({ loading: false });
|
||||
log('ListView.onEndReached', e);
|
||||
}
|
||||
}
|
||||
}, 300)
|
||||
|
||||
renderFooter = () => {
|
||||
const { loading } = this.state;
|
||||
|
@ -122,10 +121,7 @@ export class List extends React.PureComponent {
|
|||
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);
|
||||
if (tmsg) {
|
||||
tmsg = emojify(tmsg, { output: 'unicode' });
|
||||
}
|
||||
const tmsg = thread.msg || (thread.attachments && thread.attachments.length && thread.attachments[0].title);
|
||||
item = { ...item, tmsg };
|
||||
}
|
||||
}
|
||||
|
@ -134,15 +130,24 @@ export class List extends React.PureComponent {
|
|||
|
||||
render() {
|
||||
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 (
|
||||
<React.Fragment>
|
||||
<EmptyRoom length={messages.length} />
|
||||
<EmptyRoom length={data.length} />
|
||||
<FlatList
|
||||
testID='room-view-messages'
|
||||
ref={ref => this.list = ref}
|
||||
keyExtractor={item => item._id}
|
||||
data={messages}
|
||||
data={data}
|
||||
extraData={this.state}
|
||||
renderItem={this.renderItem}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
|
|
|
@ -46,6 +46,8 @@ import buildMessage from '../../lib/methods/helpers/buildMessage';
|
|||
token: state.login.user && state.login.user.token
|
||||
},
|
||||
actionMessage: state.messages.actionMessage,
|
||||
editing: state.messages.editing,
|
||||
replying: state.messages.replying,
|
||||
showActions: state.messages.showActions,
|
||||
showErrorActions: state.messages.showErrorActions,
|
||||
appState: state.app.ready && state.app.foreground ? 'foreground' : 'background',
|
||||
|
@ -87,6 +89,8 @@ export default class RoomView extends LoggedView {
|
|||
appState: PropTypes.string,
|
||||
useRealName: PropTypes.bool,
|
||||
isAuthenticated: PropTypes.bool,
|
||||
editing: PropTypes.bool,
|
||||
replying: PropTypes.bool,
|
||||
toggleReactionPicker: PropTypes.func.isRequired,
|
||||
actionsShow: PropTypes.func,
|
||||
editCancel: PropTypes.func,
|
||||
|
@ -176,12 +180,18 @@ export default class RoomView extends LoggedView {
|
|||
}
|
||||
|
||||
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 [room] = this.rooms;
|
||||
if (room) {
|
||||
let obj;
|
||||
if (this.tmid) {
|
||||
obj = database.objectForPrimaryKey('threads', this.tmid);
|
||||
} else {
|
||||
[obj] = this.rooms;
|
||||
}
|
||||
if (obj) {
|
||||
database.write(() => {
|
||||
room.draftMessage = text;
|
||||
obj.draftMessage = text;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -192,9 +202,14 @@ export default class RoomView extends LoggedView {
|
|||
if (this.beginAnimatingTimeout) {
|
||||
clearTimeout(this.beginAnimatingTimeout);
|
||||
}
|
||||
const { editCancel, replyCancel } = this.props;
|
||||
editCancel();
|
||||
replyCancel();
|
||||
if (editing) {
|
||||
const { editCancel } = this.props;
|
||||
editCancel();
|
||||
}
|
||||
if (replying) {
|
||||
const { replyCancel } = this.props;
|
||||
replyCancel();
|
||||
}
|
||||
if (this.didMountInteraction && this.didMountInteraction.cancel) {
|
||||
this.didMountInteraction.cancel();
|
||||
}
|
||||
|
@ -217,7 +232,7 @@ export default class RoomView extends LoggedView {
|
|||
this.initInteraction = InteractionManager.runAfterInteractions(async() => {
|
||||
const { room } = this.state;
|
||||
if (this.tmid) {
|
||||
RocketChat.loadThreadMessages({ tmid: this.tmid, t: this.t });
|
||||
await this.getThreadMessages();
|
||||
} else {
|
||||
await this.getMessages(room);
|
||||
|
||||
|
@ -241,7 +256,7 @@ export default class RoomView extends LoggedView {
|
|||
|
||||
onMessageLongPress = (message) => {
|
||||
const { actionsShow } = this.props;
|
||||
actionsShow(message);
|
||||
actionsShow({ ...message, rid: this.rid });
|
||||
}
|
||||
|
||||
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 });
|
||||
|
||||
joinRoom = async() => {
|
||||
|
@ -420,6 +444,7 @@ export default class RoomView extends LoggedView {
|
|||
|
||||
renderFooter = () => {
|
||||
const { joined, room } = this.state;
|
||||
const { navigation } = this.props;
|
||||
|
||||
if (!joined && !this.tmid) {
|
||||
return (
|
||||
|
@ -450,7 +475,16 @@ export default class RoomView extends LoggedView {
|
|||
</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 = () => {
|
||||
|
|
|
@ -175,45 +175,6 @@ export default class RoomsListView extends LoggedView {
|
|||
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;
|
||||
if (!isEqual(nextState.search, search)) {
|
||||
return true;
|
||||
|
@ -311,27 +272,20 @@ export default class RoomsListView extends LoggedView {
|
|||
updateState = debounce(() => {
|
||||
this.updateStateInteraction = InteractionManager.runAfterInteractions(() => {
|
||||
this.internalSetState({
|
||||
chats: this.getSnapshot(this.chats),
|
||||
unread: this.getSnapshot(this.unread),
|
||||
favorites: this.getSnapshot(this.favorites),
|
||||
discussions: this.getSnapshot(this.discussions),
|
||||
channels: this.getSnapshot(this.channels),
|
||||
privateGroup: this.getSnapshot(this.privateGroup),
|
||||
direct: this.getSnapshot(this.direct),
|
||||
livechat: this.getSnapshot(this.livechat),
|
||||
chats: this.chats,
|
||||
unread: this.unread,
|
||||
favorites: this.favorites,
|
||||
discussions: this.discussions,
|
||||
channels: this.channels,
|
||||
privateGroup: this.privateGroup,
|
||||
direct: this.direct,
|
||||
livechat: this.livechat,
|
||||
loading: false
|
||||
});
|
||||
this.forceUpdate();
|
||||
});
|
||||
}, 300);
|
||||
|
||||
getSnapshot = (data) => {
|
||||
if (data && data.length) {
|
||||
const array = Array.from(data);
|
||||
return JSON.parse(JSON.stringify(array));
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
initSearchingAndroid = () => {
|
||||
const { openSearchHeader, navigation } = this.props;
|
||||
this.setState({ searching: true });
|
||||
|
@ -441,26 +395,29 @@ export default class RoomsListView extends LoggedView {
|
|||
} = this.props;
|
||||
const id = item.rid.replace(userId, '').trim();
|
||||
|
||||
return (
|
||||
<RoomItem
|
||||
alert={item.alert}
|
||||
unread={item.unread}
|
||||
userMentions={item.userMentions}
|
||||
favorite={item.f}
|
||||
lastMessage={item.lastMessage}
|
||||
name={this.getRoomTitle(item)}
|
||||
_updatedAt={item.roomUpdatedAt}
|
||||
key={item._id}
|
||||
id={id}
|
||||
type={item.t}
|
||||
baseUrl={baseUrl}
|
||||
prid={item.prid}
|
||||
showLastMessage={StoreLastMessage}
|
||||
onPress={() => this._onPressItem(item)}
|
||||
testID={`rooms-list-view-item-${ item.name }`}
|
||||
height={ROW_HEIGHT}
|
||||
/>
|
||||
);
|
||||
if (item.search || (item.isValid && item.isValid())) {
|
||||
return (
|
||||
<RoomItem
|
||||
alert={item.alert}
|
||||
unread={item.unread}
|
||||
userMentions={item.userMentions}
|
||||
favorite={item.f}
|
||||
lastMessage={JSON.parse(JSON.stringify(item.lastMessage))}
|
||||
name={this.getRoomTitle(item)}
|
||||
_updatedAt={item.roomUpdatedAt}
|
||||
key={item._id}
|
||||
id={id}
|
||||
type={item.t}
|
||||
baseUrl={baseUrl}
|
||||
prid={item.prid}
|
||||
showLastMessage={StoreLastMessage}
|
||||
onPress={() => this._onPressItem(item)}
|
||||
testID={`rooms-list-view-item-${ item.name }`}
|
||||
height={ROW_HEIGHT}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderSectionHeader = header => (
|
||||
|
@ -481,11 +438,10 @@ export default class RoomsListView extends LoggedView {
|
|||
} else if (header === 'Chats' && groupByType) {
|
||||
return null;
|
||||
}
|
||||
if (data.length > 0) {
|
||||
if (data && data.length > 0) {
|
||||
return (
|
||||
<FlatList
|
||||
data={data}
|
||||
extraData={data}
|
||||
keyExtractor={keyExtractor}
|
||||
style={styles.list}
|
||||
renderItem={this.renderItem}
|
||||
|
@ -553,7 +509,6 @@ export default class RoomsListView extends LoggedView {
|
|||
<FlatList
|
||||
ref={this.getScrollRef}
|
||||
data={search.length ? search : chats}
|
||||
extraData={search.length ? search : chats}
|
||||
contentOffset={isIOS ? { x: 0, y: SCROLL_OFFSET } : {}}
|
||||
keyExtractor={keyExtractor}
|
||||
style={styles.list}
|
||||
|
|
|
@ -5,8 +5,6 @@ import {
|
|||
} 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';
|
||||
|
@ -22,6 +20,7 @@ import log from '../../utils/log';
|
|||
import debounce from '../../utils/debounce';
|
||||
|
||||
const Separator = React.memo(() => <View style={styles.separator} />);
|
||||
const API_FETCH_COUNT = 50;
|
||||
|
||||
@connect(state => ({
|
||||
baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
|
||||
|
@ -47,72 +46,136 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
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);
|
||||
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);
|
||||
this.state = {
|
||||
loading: false,
|
||||
messages: this.messages.slice(),
|
||||
end: false,
|
||||
total: 0
|
||||
messages: this.messages
|
||||
};
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.load();
|
||||
this.mountInteraction = InteractionManager.runAfterInteractions(() => {
|
||||
this.init();
|
||||
this.mounted = true;
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps, nextState) {
|
||||
const { loading, messages, end } = this.state;
|
||||
if (nextState.loading !== loading) {
|
||||
return true;
|
||||
componentWillUnmount() {
|
||||
this.messages.removeAllListeners();
|
||||
if (this.mountInteraction && this.mountInteraction.cancel) {
|
||||
this.mountInteraction.cancel();
|
||||
}
|
||||
if (!equal(nextState.messages, messages)) {
|
||||
return true;
|
||||
if (this.loadInteraction && this.loadInteraction.cancel) {
|
||||
this.loadInteraction.cancel();
|
||||
}
|
||||
if (!equal(nextState.end, end)) {
|
||||
return true;
|
||||
if (this.syncInteraction && this.syncInteraction.cancel) {
|
||||
this.syncInteraction.cancel();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
updateMessages = () => {
|
||||
this.setState({ messages: this.messages.slice() });
|
||||
// eslint-disable-next-line react/sort-comp
|
||||
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
|
||||
load = debounce(async() => {
|
||||
const {
|
||||
loading, end, total
|
||||
} = this.state;
|
||||
if (end || loading) {
|
||||
const { loading, end } = this.state;
|
||||
if (end || loading || !this.mounted) {
|
||||
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
|
||||
}));
|
||||
const result = await RocketChat.getThreadsList({
|
||||
rid: this.rid, count: API_FETCH_COUNT, offset: this.messages.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) {
|
||||
console.log('ThreadMessagesView -> catch -> error', error);
|
||||
console.log('ThreadMessagesView -> load -> error', error);
|
||||
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 => (
|
||||
lm ? moment(lm).calendar(null, {
|
||||
|
@ -133,28 +196,31 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
|
||||
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}
|
||||
/>
|
||||
);
|
||||
if (item.isValid && item.isValid()) {
|
||||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
@ -163,6 +229,7 @@ export default class ThreadMessagesView extends LoggedView {
|
|||
<StatusBar />
|
||||
<FlatList
|
||||
data={messages}
|
||||
extraData={this.state}
|
||||
renderItem={this.renderItem}
|
||||
style={styles.list}
|
||||
contentContainerStyle={styles.contentContainer}
|
||||
|
|
|
@ -284,10 +284,10 @@ describe('Room screen', () => {
|
|||
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();
|
||||
await waitFor(element(by.id(`message-thread-button-${ thread }`))).toExist().withTimeout(5000);
|
||||
await expect(element(by.id(`message-thread-button-${ thread }`))).toExist();
|
||||
await waitFor(element(by.id(`message-thread-replied-on-${ thread }`))).toExist().withTimeout(5000);
|
||||
await expect(element(by.id(`message-thread-replied-on-${ thread }`))).toExist();
|
||||
});
|
||||
|
||||
it('should navigate to thread from button', async() => {
|
||||
|
|
|
@ -263,7 +263,6 @@ export default (
|
|||
header={false}
|
||||
/>
|
||||
|
||||
{/* Legacy thread */}
|
||||
<Separator title='Message with reply' />
|
||||
<Message
|
||||
msg="I'm fine!"
|
||||
|
@ -290,6 +289,11 @@ export default (
|
|||
tcount={1}
|
||||
tlm={date}
|
||||
/>
|
||||
<Message
|
||||
msg='How are you?'
|
||||
tcount={9999}
|
||||
tlm={date}
|
||||
/>
|
||||
<Message
|
||||
msg="I'm fine!"
|
||||
tmid='1'
|
||||
|
@ -316,15 +320,15 @@ export default (
|
|||
tmsg={longText}
|
||||
/>
|
||||
<Message
|
||||
msg='How are you?'
|
||||
tcount={0}
|
||||
tlm={date}
|
||||
/>
|
||||
<Message
|
||||
msg='How are you?'
|
||||
tcount={9999}
|
||||
tlm={date}
|
||||
tmid='1'
|
||||
tmsg='Thread with attachment'
|
||||
attachments={[{
|
||||
title: 'This is a title',
|
||||
description: 'This is a description',
|
||||
audio_url: '/file-upload/c4wcNhrbXJLBvAJtN/1535569819516.aac'
|
||||
}]}
|
||||
/>
|
||||
|
||||
{/* <Message
|
||||
msg='How are you?'
|
||||
tcount={9999}
|
||||
|
|
Loading…
Reference in New Issue