Skip to content
Snippets Groups Projects
index.js 8.61 KiB
Newer Older
Diego Mello's avatar
Diego Mello committed
import React from 'react';
import PropTypes from 'prop-types';
import {
	FlatList, View, Text, InteractionManager
} from 'react-native';
import { connect } from 'react-redux';
import { SafeAreaView } from 'react-navigation';
Diego Mello's avatar
Diego Mello committed
import moment from 'moment';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb';
import { sanitizedRaw } from '@nozbe/watermelondb/RawRecord';
Diego Mello's avatar
Diego Mello committed

import styles from './styles';
import Message from '../../containers/message';
import RCActivityIndicator from '../../containers/ActivityIndicator';
import I18n from '../../i18n';
import RocketChat from '../../lib/rocketchat';
import database from '../../lib/database';
Diego Mello's avatar
Diego Mello committed
import StatusBar from '../../containers/StatusBar';
import buildMessage from '../../lib/methods/helpers/buildMessage';
import log from '../../utils/log';
import debounce from '../../utils/debounce';
import protectedFunction from '../../lib/methods/helpers/protectedFunction';
Diego Mello's avatar
Diego Mello committed

const Separator = React.memo(() => <View style={styles.separator} />);
Diego Mello's avatar
Diego Mello committed
const API_FETCH_COUNT = 50;
Diego Mello's avatar
Diego Mello committed

class ThreadMessagesView extends React.Component {
Diego Mello's avatar
Diego Mello committed
	static navigationOptions = {
		title: I18n.t('Threads')
	}

	static propTypes = {
		user: PropTypes.object,
		navigation: PropTypes.object,
		baseUrl: PropTypes.string,
		useRealName: PropTypes.bool,
		customEmojis: PropTypes.object
Diego Mello's avatar
Diego Mello committed
	}

	constructor(props) {
		super(props);
		this.mounted = false;
Diego Mello's avatar
Diego Mello committed
		this.rid = props.navigation.getParam('rid');
		this.t = props.navigation.getParam('t');
		this.state = {
			loading: false,
			end: false,
			messages: []
Diego Mello's avatar
Diego Mello committed
		};
Diego Mello's avatar
Diego Mello committed
	}

	componentDidMount() {
		this.mounted = true;
Diego Mello's avatar
Diego Mello committed
		this.mountInteraction = InteractionManager.runAfterInteractions(() => {
			this.init();
		});
Diego Mello's avatar
Diego Mello committed
	componentWillUnmount() {
		if (this.mountInteraction && this.mountInteraction.cancel) {
			this.mountInteraction.cancel();
Diego Mello's avatar
Diego Mello committed
		}
Diego Mello's avatar
Diego Mello committed
		if (this.loadInteraction && this.loadInteraction.cancel) {
			this.loadInteraction.cancel();
Diego Mello's avatar
Diego Mello committed
		}
Diego Mello's avatar
Diego Mello committed
		if (this.syncInteraction && this.syncInteraction.cancel) {
			this.syncInteraction.cancel();
Diego Mello's avatar
Diego Mello committed
		}
		if (this.subSubscription && this.subSubscription.unsubscribe) {
			this.subSubscription.unsubscribe();
		}
		if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
			this.messagesSubscription.unsubscribe();
		}
Diego Mello's avatar
Diego Mello committed
	// eslint-disable-next-line react/sort-comp
	subscribeData = () => {
		try {
			const db = database.active;
			this.subObservable = db.collections
				.get('subscriptions')
				.findAndObserve(this.rid);
			this.subSubscription = this.subObservable
				.subscribe((data) => {
					this.subscription = data;
				});
			this.messagesObservable = db.collections
				.get('threads')
				.query(
					Q.where('rid', this.rid),
					Q.where('t', Q.notEq('rm'))
				)
				.observeWithColumns(['updated_at']);
			this.messagesSubscription = this.messagesObservable
				.subscribe((data) => {
					const messages = orderBy(data, ['ts'], ['desc']);
					if (this.mounted) {
						this.setState({ messages });
					} else {
						this.state.messages = messages;
					}
				});
		} catch (e) {
			log(e);
		}
	}
	// eslint-disable-next-line react/sort-comp
Diego Mello's avatar
Diego Mello committed
	init = () => {
		if (!this.subscription) {
		try {
			const lastThreadSync = new Date();
			if (this.subscription.lastThreadSync) {
				this.sync(this.subscription.lastThreadSync);
			} else {
				this.load(lastThreadSync);
			}
		} catch (e) {
			log(e);
		}
	}
	updateThreads = async({ update, remove, lastThreadSync }) => {
		try {
			const db = database.active;
			const threadsCollection = db.collections.get('threads');
			const allThreadsRecords = await this.subscription.threads.fetch();
			let threadsToCreate = [];
			let threadsToUpdate = [];
			let threadsToDelete = [];

			if (update && update.length) {
				update = update.map(m => buildMessage(m));
				// filter threads
				threadsToCreate = update.filter(i1 => !allThreadsRecords.find(i2 => i1._id === i2.id));
				threadsToUpdate = allThreadsRecords.filter(i1 => update.find(i2 => i1.id === i2._id));
				threadsToCreate = threadsToCreate.map(thread => threadsCollection.prepareCreate(protectedFunction((t) => {
					t._raw = sanitizedRaw({ id: thread._id }, threadsCollection.schema);
					t.subscription.set(this.subscription);
					Object.assign(t, thread);
				})));
				threadsToUpdate = threadsToUpdate.map((thread) => {
					const newThread = update.find(t => t._id === thread.id);
					return thread.prepareUpdate(protectedFunction((t) => {
						Object.assign(t, newThread);
					}));
				});
			}

			if (remove && remove.length) {
				threadsToDelete = allThreadsRecords.filter(i1 => remove.find(i2 => i1.id === i2._id));
				threadsToDelete = threadsToDelete.map(t => t.prepareDestroyPermanently());
			}

			await db.action(async() => {
				await db.batch(
					...threadsToCreate,
					...threadsToUpdate,
					...threadsToDelete,
					this.subscription.prepareUpdate((s) => {
						s.lastThreadSync = lastThreadSync;
					})
				);
			});
		} catch (e) {
			log(e);
Diego Mello's avatar
Diego Mello committed
		}
Diego Mello's avatar
Diego Mello committed
	}

	// eslint-disable-next-line react/sort-comp
	load = debounce(async(lastThreadSync) => {
		const { loading, end, messages } = this.state;
Diego Mello's avatar
Diego Mello committed
		if (end || loading || !this.mounted) {
Diego Mello's avatar
Diego Mello committed
			return;
		}

		this.setState({ loading: true });

		try {
Diego Mello's avatar
Diego Mello committed
			const result = await RocketChat.getThreadsList({
				rid: this.rid, count: API_FETCH_COUNT, offset: messages.length
Diego Mello's avatar
Diego Mello committed
			});
Diego Mello's avatar
Diego Mello committed
			if (result.success) {
				this.loadInteraction = InteractionManager.runAfterInteractions(() => {
					this.updateThreads({ update: result.threads, lastThreadSync });
Diego Mello's avatar
Diego Mello committed

					this.setState({
						loading: false,
						end: result.count < API_FETCH_COUNT
					});
				});
			}
		} catch (e) {
			log(e);
Diego Mello's avatar
Diego Mello committed
			this.setState({ loading: false, end: true });
		}
Diego Mello's avatar
Diego Mello committed
	}, 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;
					this.updateThreads({ update, remove, lastThreadSync: updatedSince });
Diego Mello's avatar
Diego Mello committed
				});
			}
			this.setState({
				loading: false
			});
		} catch (e) {
			log(e);
Diego Mello's avatar
Diego Mello committed
			this.setState({ loading: false });
		}
	}
Diego Mello's avatar
Diego Mello committed

	formatMessage = lm => (
		lm ? moment(lm).calendar(null, {
			lastDay: `[${ I18n.t('Yesterday') }]`,
			sameDay: 'h:mm A',
			lastWeek: 'dddd',
			sameElse: 'MMM D'
		}) : null
	)

	getCustomEmoji = (name) => {
		const { customEmojis } = this.props;
		const emoji = customEmojis[name];
		if (emoji) {
			return emoji;
		}
		return null;
	}

	onThreadPress = debounce((item) => {
		const { navigation } = this.props;
		navigation.push('RoomView', {
			rid: item.subscription.id, tmid: item.id, name: item.msg, t: 'thread'
		});
	}, 1000, true)

Diego Mello's avatar
Diego Mello committed
	renderSeparator = () => <Separator />

	renderEmpty = () => (
		<View style={styles.listEmptyContainer} testID='thread-messages-view'>
			<Text style={styles.noDataFound}>{I18n.t('No_thread_messages')}</Text>
		</View>
	)

	renderItem = ({ item }) => {
		const {
			user, navigation, baseUrl, useRealName
		} = this.props;
		return (
			<Message
				key={item.id}
				item={item}
				user={user}
				archived={false}
				broadcast={false}
				status={item.status}
				navigation={navigation}
				timeFormat='MMM D'
				customThreadTimeFormat='MMM Do YYYY, h:mm:ss a'
				onThreadPress={this.onThreadPress}
				baseUrl={baseUrl}
				useRealName={useRealName}
				getCustomEmoji={this.getCustomEmoji}
			/>
		);
Diego Mello's avatar
Diego Mello committed
	}

	render() {
Diego Mello's avatar
Diego Mello committed
		const { loading, messages } = this.state;
Diego Mello's avatar
Diego Mello committed

		if (!loading && messages.length === 0) {
Diego Mello's avatar
Diego Mello committed
			return this.renderEmpty();
		}

		return (
			<SafeAreaView style={styles.list} testID='thread-messages-view' forceInset={{ vertical: 'never' }}>
Diego Mello's avatar
Diego Mello committed
				<StatusBar />
				<FlatList
					data={messages}
Diego Mello's avatar
Diego Mello committed
					extraData={this.state}
Diego Mello's avatar
Diego Mello committed
					renderItem={this.renderItem}
					style={styles.list}
					contentContainerStyle={styles.contentContainer}
					keyExtractor={item => item.id}
Diego Mello's avatar
Diego Mello committed
					onEndReached={this.load}
					onEndReachedThreshold={0.5}
					maxToRenderPerBatch={5}
					initialNumToRender={1}
					ItemSeparatorComponent={this.renderSeparator}
					ListFooterComponent={loading ? <RCActivityIndicator /> : null}
				/>
			</SafeAreaView>
		);
	}
}

const mapStateToProps = state => ({
	baseUrl: state.settings.Site_Url || state.server ? state.server.server : '',
	user: {
		id: state.login.user && state.login.user.id,
		username: state.login.user && state.login.user.username,
		token: state.login.user && state.login.user.token
	},
	useRealName: state.settings.UI_Use_Real_Name,
	customEmojis: state.customEmojis
});

export default connect(mapStateToProps)(ThreadMessagesView);