From 3a6dff4f4fe3d910c15bed7fa625681865f79f3a Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Mon, 6 Feb 2017 12:21:13 -0800 Subject: [PATCH 1/5] CLI: Add support for project templates Summary: Currently it is not trivial for people to get started with React Native. `react-native init MyApp` just creates a simple app with a single screen. People have to spend time figuring out how to add more screens, or how to accomplish very basic tasks such as rendering a list of data or handling text input. Let's add an option: `react-native init --template navigation` - this creates a "starter" app which can be easily tweaked into the actual app the person wants to build. **Test plan (required)** - Checked that 'react-native init MyApp' still works as before: screenshot 2017-02-02 16 56 28 screenshot 2017-02-02 16 58 04 - Ran 'react-native init MyNavApp --template'. This prints the available templates: ``` $ react-native init MyNavApp Closes https://github.com/facebook/react-native/pull/12170 Differential Revision: D4516241 Pulled By: mkonicek fbshipit-source-id: 8ac081157919872e92947ed64ea64fb48078614d --- local-cli/generator/templates.js | 112 ++++++++++++++++++ local-cli/init/init.js | 26 ++-- local-cli/runAndroid/runAndroid.js | 2 +- .../HelloNavigation/dependencies.json | 3 + .../HelloNavigation/index.android.js | 2 +- .../templates/HelloNavigation/index.ios.js | 2 +- .../views/HomeScreenTabNavigator.js | 8 +- .../views/chat/ChatListScreen.js | 2 +- .../WelcomeScreen.js} | 11 +- .../views/welcome/WelcomeText.android.js | 51 ++++++++ .../views/welcome/WelcomeText.ios.js | 51 ++++++++ .../welcome-icon.png} | Bin react-native-cli/index.js | 7 +- 13 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 local-cli/generator/templates.js create mode 100644 local-cli/templates/HelloNavigation/dependencies.json rename local-cli/templates/HelloNavigation/views/{friends/FriendListScreen.js => welcome/WelcomeScreen.js} (75%) create mode 100644 local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js create mode 100644 local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js rename local-cli/templates/HelloNavigation/views/{friends/friend-icon.png => welcome/welcome-icon.png} (100%) diff --git a/local-cli/generator/templates.js b/local-cli/generator/templates.js new file mode 100644 index 00000000000..dfbf598e255 --- /dev/null +++ b/local-cli/generator/templates.js @@ -0,0 +1,112 @@ +/** + * 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. + */ +'use strict'; + +const copyProjectTemplateAndReplace = require('./copyProjectTemplateAndReplace'); +const execSync = require('child_process').execSync; +const fs = require('fs'); +const path = require('path'); + +const availableTemplates = { + navigation: 'HelloNavigation', +}; + +function listTemplatesAndExit(newProjectName, options) { + if (options.template === true) { + // Just listing templates using 'react-native init --template'. + // Not creating a new app. + // Print available templates and exit. + const templateKeys = Object.keys(availableTemplates); + if (templateKeys.length === 0) { + // Just a guard, should never happen as long availableTemplates + // above is defined correctly :) + console.log( + 'There are no templates available besides ' + + 'the default "Hello World" one.' + ); + } else { + console.log( + 'The available templates are:\n' + + templateKeys.join('\n') + + '\nYou can use these to create an app based on a template, for example: ' + + 'you could run: ' + + 'react-native init ' + newProjectName + ' --template ' + templateKeys[0] + ); + } + // Exit 'react-native init' + return true; + } + // Continue 'react-native init' + return false; +} + +/** + * @param newProjectName For example 'AwesomeApp'. + * @param templateKey Template to use, for example 'navigation'. + * @param yarnVersion Version of yarn available on the system, or null if + * yarn is not available. For example '0.18.1'. + */ +function createProjectFromTemplate(destPath, newProjectName, templateKey, yarnVersion) { + // Expand the basic 'HelloWorld' template + copyProjectTemplateAndReplace( + path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'), + destPath, + newProjectName + ); + + if (templateKey !== undefined) { + // Keep the files from the 'HelloWorld' template, and overwrite some of them + // with the specified project template. + // The 'HelloWorld' template contains the native files (these are used by + // all templates) and every other template only contains additional JS code. + // Reason: + // This way we don't have to duplicate the native files in every template. + // If we duplicated them we'd make RN larger and risk that people would + // forget to maintain all the copies so they would go out of sync. + const templateName = availableTemplates[templateKey]; + if (templateName) { + copyProjectTemplateAndReplace( + path.resolve( + 'node_modules', 'react-native', 'local-cli', 'templates', templateName + ), + destPath, + newProjectName + ); + } else { + throw new Error('Uknown template: ' + templateKey); + } + + // Add dependencies: + + // dependencies.json is a special file that lists additional dependencies + // that are required by this template + const dependenciesJsonPath = path.resolve( + 'node_modules', 'react-native', 'local-cli', 'templates', templateName, 'dependencies.json' + ); + if (fs.existsSync(dependenciesJsonPath)) { + console.log('Adding dependencies for the project...'); + const dependencies = JSON.parse(fs.readFileSync(dependenciesJsonPath)); + for (let depName in dependencies) { + const depVersion = dependencies[depName]; + const depToInstall = depName + '@' + depVersion; + console.log('Adding ' + depToInstall + '...'); + if (yarnVersion) { + execSync(`yarn add ${depToInstall}`, {stdio: 'inherit'}); + } else { + execSync(`npm install ${depToInstall} --save --save-exact`, {stdio: 'inherit'}); + } + } + } + } +} + +module.exports = { + listTemplatesAndExit, + createProjectFromTemplate, +}; diff --git a/local-cli/init/init.js b/local-cli/init/init.js index 15f9837dfe8..3537a97ba68 100644 --- a/local-cli/init/init.js +++ b/local-cli/init/init.js @@ -8,7 +8,10 @@ */ 'use strict'; -const copyProjectTemplateAndReplace = require('../generator/copyProjectTemplateAndReplace'); +const { + listTemplatesAndExit, + createProjectFromTemplate, +} = require('../generator/templates'); const execSync = require('child_process').execSync; const fs = require('fs'); const minimist = require('minimist'); @@ -23,15 +26,15 @@ const yarn = require('../util/yarn'); * @param projectDir Templates will be copied here. * @param argsOrName Project name or full list of custom arguments * for the generator. + * @param options Command line options passed from the react-native-cli directly. + * E.g. `{ version: '0.43.0', template: 'navigation' }` */ function init(projectDir, argsOrName) { - console.log('Setting up new React Native app in ' + projectDir); - const args = Array.isArray(argsOrName) ? argsOrName // argsOrName was e.g. ['AwesomeApp', '--verbose'] : [argsOrName].concat(process.argv.slice(4)); // argsOrName was e.g. 'AwesomeApp' - // args array is e.g. ['AwesomeApp', '--verbose'] + // args array is e.g. ['AwesomeApp', '--verbose', '--template', 'navigation'] if (!args || args.length === 0) { console.error('react-native init requires a project name.'); return; @@ -40,7 +43,14 @@ function init(projectDir, argsOrName) { const newProjectName = args[0]; const options = minimist(args); - generateProject(projectDir, newProjectName, options); + if (listTemplatesAndExit(newProjectName, options)) { + // Just listing templates using 'react-native init --template' + // Not creating a new app. + return; + } else { + console.log('Setting up new React Native app in ' + projectDir); + generateProject(projectDir, newProjectName, options); + } } /** @@ -67,11 +77,7 @@ function generateProject(destinationRoot, newProjectName, options) { yarn.getYarnVersionIfAvailable() && yarn.isGlobalCliUsingYarn(destinationRoot); - copyProjectTemplateAndReplace( - path.resolve('node_modules', 'react-native', 'local-cli', 'templates', 'HelloWorld'), - destinationRoot, - newProjectName - ); + createProjectFromTemplate(destinationRoot, newProjectName, options.template, yarnVersion); if (yarnVersion) { console.log('Adding React...'); diff --git a/local-cli/runAndroid/runAndroid.js b/local-cli/runAndroid/runAndroid.js index 2adf2386e0b..815c8ebaa74 100644 --- a/local-cli/runAndroid/runAndroid.js +++ b/local-cli/runAndroid/runAndroid.js @@ -193,7 +193,7 @@ function runOnAllDevices(args, cmd, packageName, adbPath){ } console.log(chalk.bold( - `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')}...` + `Building and installing the app on the device (cd android && ${cmd} ${gradleArgs.join(' ')})...` )); child_process.execFileSync(cmd, gradleArgs, { diff --git a/local-cli/templates/HelloNavigation/dependencies.json b/local-cli/templates/HelloNavigation/dependencies.json new file mode 100644 index 00000000000..130c08ed81f --- /dev/null +++ b/local-cli/templates/HelloNavigation/dependencies.json @@ -0,0 +1,3 @@ +{ + "react-navigation": "1.0.0-beta.1" +} diff --git a/local-cli/templates/HelloNavigation/index.android.js b/local-cli/templates/HelloNavigation/index.android.js index b122bdc5dcf..e9ea66bf64b 100644 --- a/local-cli/templates/HelloNavigation/index.android.js +++ b/local-cli/templates/HelloNavigation/index.android.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/index.ios.js b/local-cli/templates/HelloNavigation/index.ios.js index b122bdc5dcf..e9ea66bf64b 100644 --- a/local-cli/templates/HelloNavigation/index.ios.js +++ b/local-cli/templates/HelloNavigation/index.ios.js @@ -2,4 +2,4 @@ import { AppRegistry } from 'react-native'; import MainNavigator from './views/MainNavigator'; -AppRegistry.registerComponent('ChatExample', () => MainNavigator); +AppRegistry.registerComponent('HelloWorld', () => MainNavigator); diff --git a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js index 123ee3a78a1..bcbfaaa5de7 100644 --- a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js +++ b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js @@ -7,18 +7,18 @@ import { import { TabNavigator } from 'react-navigation'; import ChatListScreen from './chat/ChatListScreen'; -import FriendListScreen from './friends/FriendListScreen'; +import WelcomeScreen from './welcome/WelcomeScreen'; /** * Screen with tabs shown on app startup. */ const HomeScreenTabNavigator = TabNavigator({ + Welcome: { + screen: WelcomeScreen, + }, Chats: { screen: ChatListScreen, }, - Friends: { - screen: FriendListScreen, - }, }); export default HomeScreenTabNavigator; diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js index a29fa1443af..146e7e21431 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js @@ -10,7 +10,7 @@ import ListItem from '../../components/ListItem'; export default class ChatListScreen extends Component { static navigationOptions = { - title: 'Chats', + title: 'Friends', header: { visible: Platform.OS === 'ios', }, diff --git a/local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js similarity index 75% rename from local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js rename to local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js index 85d8cdd7949..d149714ff73 100644 --- a/local-cli/templates/HelloNavigation/views/friends/FriendListScreen.js +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeScreen.js @@ -8,11 +8,12 @@ import { } from 'react-native'; import ListItem from '../../components/ListItem'; +import WelcomeText from './WelcomeText'; -export default class FriendListScreen extends Component { +export default class WelcomeScreen extends Component { static navigationOptions = { - title: 'Friends', + title: 'Welcome', header: { visible: Platform.OS === 'ios', }, @@ -20,7 +21,7 @@ export default class FriendListScreen extends Component { icon: ({ tintColor }) => ( ), @@ -29,9 +30,7 @@ export default class FriendListScreen extends Component { render() { return ( - - A list of friends here. - + ); } } diff --git a/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js new file mode 100644 index 00000000000..a082661a2de --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.android.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; +import { + AppRegistry, + StyleSheet, + Text, + View +} from 'react-native'; + +export default class WelcomeText extends Component { + render() { + return ( + + + Welcome to React Native! + + + This app shows the basics of navigating between a few screens, + working with ListView and handling text input. + + + Modify any files to get started. For example try changing the + file views/welcome/WelcomeText.android.js. + + + Press Cmd+R to reload,{'\n'} + Cmd+D or shake for dev menu. + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'white', + padding: 20, + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 16, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 12, + }, +}); diff --git a/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js new file mode 100644 index 00000000000..9d5ce075fad --- /dev/null +++ b/local-cli/templates/HelloNavigation/views/welcome/WelcomeText.ios.js @@ -0,0 +1,51 @@ +import React, { Component } from 'react'; +import { + AppRegistry, + StyleSheet, + Text, + View +} from 'react-native'; + +export default class WelcomeText extends Component { + render() { + return ( + + + Welcome to React Native! + + + This app shows the basics of navigating between a few screens, + working with ListView and handling text input. + + + Modify any files to get started. For example try changing the + file{'\n'}views/welcome/WelcomeText.ios.js. + + + Press Cmd+R to reload,{'\n'} + Cmd+D or shake for dev menu. + + + ); + } +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: 'white', + padding: 20, + }, + welcome: { + fontSize: 20, + textAlign: 'center', + margin: 16, + }, + instructions: { + textAlign: 'center', + color: '#333333', + marginBottom: 12, + }, +}); diff --git a/local-cli/templates/HelloNavigation/views/friends/friend-icon.png b/local-cli/templates/HelloNavigation/views/welcome/welcome-icon.png similarity index 100% rename from local-cli/templates/HelloNavigation/views/friends/friend-icon.png rename to local-cli/templates/HelloNavigation/views/welcome/welcome-icon.png diff --git a/react-native-cli/index.js b/react-native-cli/index.js index 95355f835a6..795e8143edb 100755 --- a/react-native-cli/index.js +++ b/react-native-cli/index.js @@ -48,6 +48,7 @@ var semver = require('semver'); * if you are in a RN app folder * init - to create a new project and npm install it * --verbose - to print logs while init + * --template - name of the template to use, e.g. --template navigation * --version - override default (https://registry.npmjs.org/react-native@latest), * package to install, examples: * - "0.22.0-rc1" - A new app will be created using a specific version of React Native from npm repo @@ -129,7 +130,8 @@ if (cli) { ' Options:', '', ' -h, --help output usage information', - ' -v, --version output the version number', + ' -v, --version use a specific version of React Native', + ' --template use an app template. Use --template to see available templates.', '', ].join('\n')); process.exit(0); @@ -264,8 +266,7 @@ function getInstallPackage(rnPackage) { } function run(root, projectName, options) { - // E.g. '0.38' or '/path/to/archive.tgz' - const rnPackage = options.version; + const rnPackage = options.version; // e.g. '0.38' or '/path/to/archive.tgz' const forceNpmClient = options.npm; const yarnVersion = (!forceNpmClient) && getYarnVersionIfAvailable(); var installCommand; From 2bc2ae761a0c56b91ff9fb63b4fefbf0d705aff5 Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Tue, 7 Feb 2017 09:03:00 -0800 Subject: [PATCH 2/5] CLI: Update Navigation app template Summary: Update the template that will be used by `react-native init --template navigation`. The goal is to make it easier for people to get started by demonstrating a few very basic concepts such as navigation, lists and text input. **Test plan (required)** Android Example iOS Example Closes https://github.com/facebook/react-native/pull/12260 Differential Revision: D4521758 Pulled By: mkonicek fbshipit-source-id: d7d9e481dd3373917ac68ec9169b9ac3267547a9 --- .../templates/HelloNavigation/lib/Backend.js | 116 ++++++++++++++++++ .../views/HomeScreenTabNavigator.js | 5 - .../views/chat/ChatListScreen.js | 44 +++++-- .../HelloNavigation/views/chat/ChatScreen.js | 77 +++++++++--- .../views/welcome/WelcomeScreen.js | 1 - .../views/welcome/WelcomeText.android.js | 7 +- .../views/welcome/WelcomeText.ios.js | 3 +- 7 files changed, 216 insertions(+), 37 deletions(-) create mode 100644 local-cli/templates/HelloNavigation/lib/Backend.js diff --git a/local-cli/templates/HelloNavigation/lib/Backend.js b/local-cli/templates/HelloNavigation/lib/Backend.js new file mode 100644 index 00000000000..e80ad3a0527 --- /dev/null +++ b/local-cli/templates/HelloNavigation/lib/Backend.js @@ -0,0 +1,116 @@ + +// This file just a dummy example of a HTTP API to talk to the backend. +// The state of the "database" that would normally live on the server +// is simply held here in memory. + +const backendStateForLoggedInPerson = { + chats: [ + { + name: 'Claire', + messages: [ + { + name: 'Claire', + text: 'I ❤️ React Native!', + }, + ], + }, + { + name: 'John', + messages: [ + { + name: 'John', + text: 'I ❤️ React Native!', + }, + ], + } + ], +}; + +/** + * Randomly simulate network failures. + * It is useful to enable this during development to make sure our app works + * in real-world conditions. + */ +function isNetworkFailure() { + const chanceOfFailure = 0; // 0..1 + return Math.random() < chanceOfFailure; +} + +/** + * Helper for the other functions in this file. + * Simulates a short delay and then returns a provided value or failure. + * This is just a dummy example. Normally we'd make a HTTP request, + * see http://facebook.github.io/react-native/docs/network.html + */ +function _makeSimulatedNetworkRequest(getValue) { + const durationMs = 400; + return new Promise(function (resolve, reject) { + setTimeout(function () { + if (isNetworkFailure()) { + reject(new Error('Network failure')); + } else { + getValue(resolve, reject); + } + }, durationMs); + }); +} + +/** + * Fetch a list of all chats for the logged in person. + */ +async function fetchChatList() { + return _makeSimulatedNetworkRequest((resolve, reject) => { + resolve(backendStateForLoggedInPerson.chats.map(chat => chat.name)) + }); +} + +/** + * Fetch a single chat. + */ +async function fetchChat(name) { + return _makeSimulatedNetworkRequest((resolve, reject) => { + resolve( + backendStateForLoggedInPerson.chats.find( + chat => chat.name === name + ) + ) + }); +} + +/** + * Send given message to given person. + */ +async function sendMessage({name, message}) { + return _makeSimulatedNetworkRequest((resolve, reject) => { + const chat = backendStateForLoggedInPerson.chats.find( + chat => chat.name === name + ); + if (chat) { + chat.messages.push({ + name: 'Me', + text: message, + }); + resolve(); + } else { + reject(new Error('Uknown person: ' + name)); + } + }); +} + +const Backend = { + fetchChatList, + fetchChat, + sendMessage, +}; + +export default Backend; + +// In case you are looking into using Redux for state management, +// this is how network requests are done in the f8 app which uses Redux: +// - To load some data, a Component fires a Redux action, such as loadSession() +// - That action makes the HTTP requests and then dispatches a redux action +// {type: 'LOADED_SESSIONS', results} +// - Then all reducers get called and one of them updates a part of the application +// state by storing the results +// - Redux re-renders the connected Components +// See https://github.com/fbsamples/f8app/search?utf8=%E2%9C%93&q=loaded_sessions diff --git a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js index bcbfaaa5de7..b3819950da3 100644 --- a/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js +++ b/local-cli/templates/HelloNavigation/views/HomeScreenTabNavigator.js @@ -1,9 +1,4 @@ import React, { Component } from 'react'; -import { - ListView, - Platform, - Text, -} from 'react-native'; import { TabNavigator } from 'react-navigation'; import ChatListScreen from './chat/ChatListScreen'; diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js index 146e7e21431..79a247c0b2c 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatListScreen.js @@ -1,16 +1,19 @@ import React, { Component } from 'react'; import { + ActivityIndicator, Image, ListView, Platform, StyleSheet, + View, } from 'react-native'; import ListItem from '../../components/ListItem'; +import Backend from '../../lib/Backend'; export default class ChatListScreen extends Component { static navigationOptions = { - title: 'Friends', + title: 'Chats', header: { visible: Platform.OS === 'ios', }, @@ -29,28 +32,48 @@ export default class ChatListScreen extends Component { super(props); const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); this.state = { - dataSource: ds.cloneWithRows([ - 'Claire', 'John' - ]) + isLoading: true, + dataSource: ds, }; } + async componentDidMount() { + const chatList = await Backend.fetchChatList(); + const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + this.setState((prevState) => ({ + dataSource: prevState.dataSource.cloneWithRows(chatList), + isLoading: false, + })); + } + // Binding the function so it can be passed to ListView below - // and 'this' works properly inside _renderRow - _renderRow = (name) => { + // and 'this' works properly inside renderRow + renderRow = (name) => { return ( this.props.navigation.navigate('Chat', {name: name})} + onPress={() => { + // Start fetching in parallel with animating + this.props.navigation.navigate('Chat', { + name: name, + }); + }} /> ) } render() { + if (this.state.isLoading) { + return ( + + + + ); + } return ( ); @@ -58,6 +81,11 @@ export default class ChatListScreen extends Component { } const styles = StyleSheet.create({ + loadingScreen: { + backgroundColor: 'white', + paddingTop: 8, + flex: 1, + }, listView: { backgroundColor: 'white', }, diff --git a/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js index 8b732f9a0ac..8199667e4e6 100644 --- a/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js +++ b/local-cli/templates/HelloNavigation/views/chat/ChatScreen.js @@ -1,14 +1,15 @@ import React, { Component } from 'react'; import { + ActivityIndicator, Button, ListView, - Platform, StyleSheet, Text, TextInput, View, } from 'react-native'; import KeyboardSpacer from '../../components/KeyboardSpacer'; +import Backend from '../../lib/Backend'; export default class ChatScreen extends Component { @@ -19,23 +20,59 @@ export default class ChatScreen extends Component { constructor(props) { super(props); const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); - const messages = [ - { - name: props.navigation.state.params.name, - name: 'Claire', - text: 'I ❤️ React Native!', - }, - ]; this.state = { - messages: messages, - dataSource: ds.cloneWithRows(messages), + messages: [], + dataSource: ds, myMessage: '', + isLoading: true, + }; + } + + async componentDidMount() { + let chat; + try { + chat = await Backend.fetchChat(this.props.navigation.state.params.name); + } catch (err) { + // Here we would handle the fact the request failed, e.g. + // set state to display "Messages could not be loaded". + // We should also check network connection first before making any + // network requests - maybe we're offline? See React Native's NetInfo + // module. + this.setState({ + isLoading: false, + }); + return; + } + const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); + this.setState((prevState) => ({ + messages: chat.messages, + dataSource: prevState.dataSource.cloneWithRows(chat.messages), + isLoading: false, + })); + } + + onAddMessage = async () => { + // Optimistically update the UI + this.addMessageLocal(); + // Send the request + try { + await Backend.sendMessage({ + name: this.props.navigation.state.params.name, + // TODO Is reading state like this outside of setState OK? + // Can it contain a stale value? + message: this.state.myMessage, + }); + } catch (err) { + // Here we would handle the request failure, e.g. call setState + // to display a visual hint showing the message could not be sent. } } - addMessage = () => { + addMessageLocal = () => { this.setState((prevState) => { - if (!prevState.myMessage) return prevState; + if (!prevState.myMessage) { + return prevState; + } const messages = [ ...prevState.messages, { name: 'Me', @@ -51,7 +88,7 @@ export default class ChatScreen extends Component { this.refs.textInput.clear(); } - myMessageChange = (event) => { + onMyMessageChange = (event) => { this.setState({myMessage: event.nativeEvent.text}); } @@ -63,6 +100,13 @@ export default class ChatScreen extends Component { ) render() { + if (this.state.isLoading) { + return ( + + + + ); + } return ( {this.state.myMessage !== '' && (