Skip to content
Snippets Groups Projects
List.js 5.25 KiB
Newer Older
import React from 'react';
	ActivityIndicator, FlatList, InteractionManager
} from 'react-native';
import PropTypes from 'prop-types';
import orderBy from 'lodash/orderBy';
import { Q } from '@nozbe/watermelondb';

import styles from './styles';
import database from '../../lib/database';
import scrollPersistTaps from '../../utils/scrollPersistTaps';
import RocketChat from '../../lib/rocketchat';
import log from '../../utils/log';
import EmptyRoom from './EmptyRoom';
import { isIOS } from '../../utils/deviceInfo';
import { animateNextTransition } from '../../utils/layoutAnimation';
import debounce from '../../utils/debounce';
export class List extends React.Component {
	static propTypes = {
		onEndReached: PropTypes.func,
		renderFooter: PropTypes.func,
		renderRow: PropTypes.func,
Diego Sampaio's avatar
Diego Sampaio committed
		rid: PropTypes.string,
		t: PropTypes.string,
		tmid: PropTypes.string,
		animated: PropTypes.bool
	};

	constructor(props) {
		super(props);
Diego Sampaio's avatar
Diego Sampaio committed
		console.time(`${ this.constructor.name } init`);
		console.time(`${ this.constructor.name } mount`);
Diego Mello's avatar
Diego Mello committed

		this.mounted = false;
		this.state = {
			loading: true,
			end: false,
			messages: []
		this.init();
Diego Sampaio's avatar
Diego Sampaio committed
		console.timeEnd(`${ this.constructor.name } init`);
Diego Sampaio's avatar
Diego Sampaio committed
	componentDidMount() {
		this.mounted = true;
Diego Sampaio's avatar
Diego Sampaio committed
		console.timeEnd(`${ this.constructor.name } mount`);
	}

	// eslint-disable-next-line react/sort-comp
	async init() {
		const { rid, tmid } = this.props;
		const db = database.active;

		if (tmid) {
			try {
				this.thread = await db.collections
					.get('threads')
					.find(tmid);
			} catch (e) {
				console.log(e);
			}
			this.messagesObservable = db.collections
				.get('thread_messages')
				.query(Q.where('rid', tmid))
				.observeWithColumns(['_updated_at']);
		} else {
			this.messagesObservable = db.collections
				.get('messages')
				.query(Q.where('rid', rid))
				.observeWithColumns(['_updated_at']);
		}

		this.unsubscribeMessages();
		this.messagesSubscription = this.messagesObservable
			.subscribe((data) => {
				this.interaction = InteractionManager.runAfterInteractions(() => {
					if (tmid) {
						data = [this.thread, ...data];
					}
					const messages = orderBy(data, ['ts'], ['desc']);
					if (this.mounted) {
						this.setState({ messages }, () => this.debouncedUpdate());
					} else {
						this.state.messages = messages;
					}
				});
			});
	}

	// this.state.loading works for this.onEndReached and RoomView.init
	static getDerivedStateFromProps(props, state) {
		if (props.loading !== state.loading) {
			return {
				loading: props.loading
			};
		}
		return null;
	}

	shouldComponentUpdate(nextProps, nextState) {
		const { loading, end } = this.state;
		if (loading !== nextState.loading) {
			return true;
		}
		if (end !== nextState.end) {
			return true;
		}
		return false;
	}

	componentWillUnmount() {
		this.unsubscribeMessages();
		if (this.interaction && this.interaction.cancel) {
			this.interaction.cancel();
		if (this.onEndReached && this.onEndReached.stop) {
			this.onEndReached.stop();
Diego Mello's avatar
Diego Mello committed
		}
		if (this.debouncedUpdate && this.debouncedUpdate.stop) {
			this.debouncedUpdate.stop();
		}
Diego Sampaio's avatar
Diego Sampaio committed
		console.countReset(`${ this.constructor.name }.render calls`);
Diego Mello's avatar
Diego Mello committed
	onEndReached = debounce(async() => {
		const {
Diego Mello's avatar
Diego Mello committed
			loading, end, messages
		} = this.state;
Diego Mello's avatar
Diego Mello committed
		if (loading || end || messages.length < 50) {
Diego Mello's avatar
Diego Mello committed
		this.setState({ loading: true });
		const { rid, t, tmid } = this.props;
Diego Mello's avatar
Diego Mello committed
			let result;
			if (tmid) {
				// `offset` is `messages.length - 1` because we append thread start to `messages` obj
				result = await RocketChat.loadThreadMessages({ tmid, rid, offset: messages.length - 1 });
Diego Mello's avatar
Diego Mello committed
			} else {
				result = await RocketChat.loadMessagesForRoom({ rid, t, latest: messages[messages.length - 1].ts });
			}

Diego Mello's avatar
Diego Mello committed
			this.setState({ end: result.length < 50, loading: false });
		} catch (e) {
Diego Mello's avatar
Diego Mello committed
			this.setState({ loading: false });
Diego Mello's avatar
Diego Mello committed
	}, 300)
	// eslint-disable-next-line react/sort-comp
	update = () => {
		animateNextTransition();
		this.forceUpdate();
	};

	// eslint-disable-next-line react/sort-comp
	debouncedUpdate = debounce(() => {
		this.update();
	}, 300)

	unsubscribeMessages = () => {
		if (this.messagesSubscription && this.messagesSubscription.unsubscribe) {
			this.messagesSubscription.unsubscribe();
		}
	}

	renderFooter = () => {
Diego Mello's avatar
Diego Mello committed
		const { loading } = this.state;
		if (loading) {
			return <ActivityIndicator style={styles.loading} />;
Diego Mello's avatar
Diego Mello committed
	renderItem = ({ item, index }) => {
		const { messages } = this.state;
Diego Mello's avatar
Diego Mello committed
		const { renderRow } = this.props;
		return renderRow(item, messages[index + 1]);
	}

	render() {
Diego Sampaio's avatar
Diego Sampaio committed
		console.count(`${ this.constructor.name }.render calls`);
		const { messages } = this.state;
		return (
			<>
				<EmptyRoom length={messages.length} mounted={this.mounted} />
				<FlatList
					testID='room-view-messages'
					ref={ref => this.list = ref}
					keyExtractor={item => item.id}
					extraData={this.state}
Diego Mello's avatar
Diego Mello committed
					renderItem={this.renderItem}
Diego Mello's avatar
Diego Mello committed
					contentContainerStyle={styles.contentContainer}
					style={styles.list}
					inverted
					removeClippedSubviews={isIOS}
					initialNumToRender={7}
					onEndReached={this.onEndReached}
					onEndReachedThreshold={5}
					maxToRenderPerBatch={5}
					windowSize={10}
					ListFooterComponent={this.renderFooter}
					{...scrollPersistTaps}
				/>