/** * Copyright (c) 2015-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @flow */ 'use strict'; var React = require('react-native'); var { ListView, ScrollView, ActivityIndicatorIOS, StyleSheet, Text, TextInput, View, } = React; var TimerMixin = require('react-timer-mixin'); var MovieCell = require('./MovieCell'); var MovieScreen = require('./MovieScreen'); var fetch = require('fetch'); var API_URL = 'http://api.rottentomatoes.com/api/public/v1.0/'; var API_KEYS = ['7waqfqbprs7pajbz28mqf6vz', 'y4vwv8m33hed9ety83jmv52f']; // Results should be cached keyed by the query // with values of null meaning "being fetched" // and anything besides null and undefined // as the result of a valid query var resultsCache = { dataForQuery: {}, nextPageNumberForQuery: {}, totalForQuery: {}, }; var LOADING = {}; var SearchScreen = React.createClass({ mixins: [TimerMixin], timeoutID: (null: any), getInitialState: function() { return { isLoading: false, isLoadingTail: false, dataSource: new ListView.DataSource({ rowHasChanged: (row1, row2) => row1 !== row2, }), filter: '', queryNumber: 0, }; }, componentDidMount: function() { this.searchMovies(''); }, _urlForQueryAndPage: function(query: string, pageNumber: ?number): string { var apiKey = API_KEYS[this.state.queryNumber % API_KEYS.length]; if (query) { return ( API_URL + 'movies.json?apikey=' + apiKey + '&q=' + encodeURIComponent(query) + '&page_limit=20&page=' + pageNumber ); } else { // With no query, load latest movies return ( API_URL + 'lists/movies/in_theaters.json?apikey=' + apiKey + '&page_limit=20&page=' + pageNumber ); } }, searchMovies: function(query: string) { this.timeoutID = null; this.setState({filter: query}); var cachedResultsForQuery = resultsCache.dataForQuery[query]; if (cachedResultsForQuery) { if (!LOADING[query]) { this.setState({ dataSource: this.getDataSource(cachedResultsForQuery), isLoading: false }); } else { this.setState({isLoading: true}); } return; } LOADING[query] = true; resultsCache.dataForQuery[query] = null; this.setState({ isLoading: true, queryNumber: this.state.queryNumber + 1, isLoadingTail: false, }); fetch(this._urlForQueryAndPage(query, 1)) .then((response) => response.json()) .catch((error) => { LOADING[query] = false; resultsCache.dataForQuery[query] = undefined; this.setState({ dataSource: this.getDataSource([]), isLoading: false, }); }) .then((responseData) => { LOADING[query] = false; resultsCache.totalForQuery[query] = responseData.total; resultsCache.dataForQuery[query] = responseData.movies; resultsCache.nextPageNumberForQuery[query] = 2; if (this.state.filter !== query) { // do not update state if the query is stale return; } this.setState({ isLoading: false, dataSource: this.getDataSource(responseData.movies), }); }) .done(); }, hasMore: function(): boolean { var query = this.state.filter; if (!resultsCache.dataForQuery[query]) { return true; } return ( resultsCache.totalForQuery[query] !== resultsCache.dataForQuery[query].length ); }, onEndReached: function() { var query = this.state.filter; if (!this.hasMore() || this.state.isLoadingTail) { // We're already fetching or have all the elements so noop return; } if (LOADING[query]) { return; } LOADING[query] = true; this.setState({ queryNumber: this.state.queryNumber + 1, isLoadingTail: true, }); var page = resultsCache.nextPageNumberForQuery[query]; fetch(this._urlForQueryAndPage(query, page)) .then((response) => response.json()) .catch((error) => { console.error(error); LOADING[query] = false; this.setState({ isLoadingTail: false, }); }) .then((responseData) => { var moviesForQuery = resultsCache.dataForQuery[query].slice(); LOADING[query] = false; // We reached the end of the list before the expected number of results if (!responseData.movies) { resultsCache.totalForQuery[query] = moviesForQuery.length; } else { for (var i in responseData.movies) { moviesForQuery.push(responseData.movies[i]); } resultsCache.dataForQuery[query] = moviesForQuery; resultsCache.nextPageNumberForQuery[query] += 1; } if (this.state.filter !== query) { // do not update state if the query is stale return; } this.setState({ isLoadingTail: false, dataSource: this.getDataSource(resultsCache.dataForQuery[query]), }); }) .done(); }, getDataSource: function(movies: Array): ListView.DataSource { return this.state.dataSource.cloneWithRows(movies); }, selectMovie: function(movie: Object) { this.props.navigator.push({ title: movie.title, component: MovieScreen, passProps: {movie}, }); }, onSearchChange: function(event: Object) { var filter = event.nativeEvent.text.toLowerCase(); this.clearTimeout(this.timeoutID); this.timeoutID = this.setTimeout(() => this.searchMovies(filter), 100); }, renderFooter: function() { if (!this.hasMore() || !this.state.isLoadingTail) { return ; } return ; }, renderRow: function(movie: Object) { return ( this.selectMovie(movie)} movie={movie} /> ); }, render: function() { var content = this.state.dataSource.getRowCount() === 0 ? : ; return ( this.refs.listview.getScrollResponder().scrollTo(0, 0)} /> {content} ); }, }); var NoMovies = React.createClass({ render: function() { var text = ''; if (this.props.filter) { text = `No results for “${this.props.filter}”`; } else if (!this.props.isLoading) { // If we're looking at the latest movies, aren't currently loading, and // still have no results, show a message text = 'No movies found'; } return ( {text} ); } }); var SearchBar = React.createClass({ render: function() { return ( ); } }); var styles = StyleSheet.create({ container: { flex: 1, backgroundColor: 'white', }, centerText: { alignItems: 'center', }, noMoviesText: { marginTop: 80, color: '#888888', }, searchBar: { marginTop: 64, padding: 3, paddingLeft: 8, flexDirection: 'row', alignItems: 'center', }, searchBarInput: { fontSize: 15, flex: 1, height: 30, }, separator: { height: 1, backgroundColor: '#eeeeee', }, spinner: { width: 30, }, scrollSpinner: { marginVertical: 20, }, }); module.exports = SearchScreen;