Reference Source

src/components/views/profile-view.js

import React, { Component } from 'react';
import { StyleSheet, Text, View, Button, PixelRatio, TouchableOpacity, Image, SafeAreaView, Alert, ScrollView, FlatList, ActivityIndicator, KeyboardAvoidingView } from 'react-native';
import { Icon } from 'react-native-elements';
import ImagePicker from 'react-native-image-picker';
import { HeaderBackButton } from 'react-navigation';
import { TextInput } from 'react-native-paper';
import SectionedMultiSelect from 'react-native-sectioned-multi-select';

import { styles, text, edit_styles } from './stylesheets/edit-styles';

import BaseView from './view';
import SafeArea from './helpers/safearea';
import HandleBack from './helpers/handleback';
import ImageCarousel from './helpers/imagecarousel';
import ProfilePresenter from '../presenters/profile-presenter';
import ImageUtil from '../../util/imageutility';

const PROFILE_TYPE = ImageUtil.getTypes().PROFILE;
const NO_DATA = 'NO-DATA';

/**
 * Class for the Profile view
 * @extends BaseView
 */
class ProfileView extends BaseView {
	state = { // Initializing the state
		editing: false, // Checks if user is editing
		refresh: true, // Triggers a view refresh
		loaderVisible: false,

		inputData: [], // Input text data is at each index

		currentID: '',

		photoEntries: [],
	};

	/**
	 * Set the navigation options, change the header to handle a back button.
	 *
	 * @return {Object} Navigation option
	 */
	static navigationOptions = ({navigation, transitioning}) => {
		const { params = {} } = navigation.state;
		const back = params._onBack ? params._onBack : () => 'default';
		const clear = params._clearData ? params._clearData : () => 'default';
		return {
			headerLeft: (<HeaderBackButton disabled={transitioning} onPress={()=>{back()}}/>),
			headerRight: (<Button disabled={transitioning} onPress={()=>{clear()}} title='Clear'/>),
			title: navigation.getParam('title', 'Profile')
		}
	}

	/**
	 * Creates an instance of the profile view
	 *
	 * @constructor
	 * @param {Object} props - Component properties
	 */
	constructor(props) {
		super(props);
		this.ProfileP = new ProfilePresenter(this);
	}

	/**
	 * Component is about to mount, initialize the data.
	 * This function is called before componentDidMount
	 */
	componentWillMount = () => {
		this.props.navigation.setParams({
			_onBack: this._onBack,
			_clearData: this._clearData
		});

		this.setState({
			inputData: this.ProfileP.getTextInputData(this.ProfileP.getData()),
			photoEntries: this.ProfileP.getCurrentPhotos()
		});
	}

	/**
	 * Component will unmount after this method is called, do any clean up here
	 * Call viewUnmounting in base class so it can do any cleanup for the view before calling the presenter destroy method
	 */
	componentWillUnmount = () => {
		this.viewUnmounting(this.ProfileP);
	}

	/**
	 * Refreshes the state of the component so new data is fetched.
	 */
	refreshState = () => {
		this.setState({ 
			refresh: !this.state.refresh
		});
	}

	/**
	 * Toggle the editing mode.
	 */
	toggleEditing = () => {
		this.setState({ editing: !editing });
	}

	/**
	 * Set the editing value to the passed in value.
	*
	 * @param {Boolean} edit - true: user is editing; false: user is not editing
	 */
	setEditing = (edit) => {
		this.setState({ editing: edit });
	}

	/**
	 * When the back button is clicked, check if the user was editing.
	 */
	_onBack = () => {
		if (!this.state.loaderVisible) {
			this.ProfileP.checkEditingState(this.state.editing, this.editingSuccess, this.editingFailure);
		}
	}

	/**
	 * A function to execute when the editing state is true.
	 */
	editingSuccess = () => {
		Alert.alert(
			"You're still editing!",
			"Are you sure you want to go back with your edits not saved?",
			[
				{ text: "Keep Editing", onPress: () => {}, style: "cancel" },
				{ text: "Go Back", onPress: () => this.resetAllOnBack() },
			],
			{ cancelable: false },
		);
	}

	/**
	 * A function to execute when the editing state is false.
	 */
	editingFailure = () => {
		// Clear the data just in case
		this.resetAllOnBack(); // If not editing then go back
	}

	/**
	 * Resets all the data and goes back to the bike page
	 */
	resetAllOnBack = () => {
		this._clearData();
		this.props.navigation.navigate('Tabs');
	}

	/**
	 * Clears all the data
	 */
	_clearData = () => {
		if (!this.state.loaderVisible) {
			this.ProfileP.clearPhotos();
			let inputData = this.ProfileP.getTextInputData(NO_DATA); // inputData is a property in state
			let photoEntries = ImageUtil.getDefaultPhotos(PROFILE_TYPE);
			this.setState({ inputData, photoEntries });
			this.setEditing(false); // Set editing to false so user can easily go back (for clear button)
		}
	}

	/**
	 * Render a text input item.
	 * 
	 * @param {Object} item - A list item, index - The index of the item in the data list
	 */
	_renderItem = ({item, index}) => (
		<TextInput
			style={text.textInput}
			label={item.disabled ? this._renderName(item.name) : item.name}
			multiline={item.multiline}
			disabled={item.disabled}
			keyboardType={item.keyboardType}
			value={this.state.inputData[index].text}
			onChangeText={(text) => {
				let { inputData } = this.state; // inputData is a keyword in state
				inputData[index].text = text;
				this.setState({ inputData });
				this.setEditing(true); // Now editing
			}
		}/>
	);

	/**
	 * Renders the name of a required field.
	 */
	_renderName = (name) => (
		<Text style={[{color: 'black'}]}>{name}</Text>
	);


	/**
	 * Extract the key from the item and index
	 */
	_keyExtractor = (item, index) => item.name;

	/**
	 * Add the new selected items to the state and update
	 *
	 * @param {List} selectedItems - List of selected items
	 */
	_onSelectedItemsChange = (selectedItems) => {
		this.setEditing(true); // Now editing
		this.setState({ selectedItems });
	} 


	/**
	 * Get the data from the state and send an update to the presenter
	 */
	_getDataToUpdate = () => {
		if (this.ProfileP.checkInputs(this.state.inputData, this._inputRequirementFailure)) {
			return;
		}

		this._enableLoader();
		this.refreshState();

		let updateData = {
			currentID: this.state.currentID,
			inputTextData: this.state.inputData, 
			picture: this.state.photoEntries
		};

		this.ProfileP.update(updateData, this.alertCallback);
	}

	/**
	 * Enables the loader.
	 */
	_enableLoader = () => { this.setState({ loaderVisible: true }); }

	/**
	 * Disables the loader.
	 */
	_disableLoader = () => { this.setState({ loaderVisible: false }); }

	/**
	 * Alert for requirement input failure
	 */
	_inputRequirementFailure = (names) => {
		const joiner = names.length > 1 ? " are" : " is";
		Alert.alert(
			"Required (*) inputs cannot be blank.",
			names.join(', ') + joiner + " required.",
			[
				{ text: "Ok", onPress: () => {}, style: "ok" },
			],
			{ cancelable: false },
		);
	}

	/**
	 * Sets a callback on what to do if there is a success or error when a bike is uploaded.
	 *
	 * @param {Boolean} success - true: Uploading successful; false: Uploading failed
	 */
	alertCallback = (success) => {
		this._disableLoader();
		this.refreshState();
		if (success) {
			Alert.alert(
				"Profile successfully updated!",
				"",
				[
					{ text: "Ok", onPress: () => this.resetAllOnBack(), style: "ok" },
				],
				{ cancelable: false },
			);
		} else {
			Alert.alert(
				"Profile was not able to be updated.",
				"Please try again.",
				[
					{ text: "Ok", onPress: () => {}, style: "ok" },
				],
				{ cancelable: false },
			);
		}
	} 

	/**
	 * DON'T USE THIS METHOD UNLESS ABSOLUTELY NECESSARY.
	 * Force a refresh of the view.
	 */
	_forceRefresh = () => {
		this.forceUpdate();
	}

	/**
	 * Renders items to the screen
	 *
	 * @return {Component} 
	 */
	render() {
		return (
			<KeyboardAvoidingView
				style={styles.container}
				behavior="padding"
				enabled>
			<HandleBack onBack={this._onBack}>
				<SafeArea/>
				<View style={styles.container}>
					<ScrollView 
						contentContainerStyle={edit_styles.contentContainer}
						keyboardShouldPersistTaps='always'
						keyboardDismissMode='interactive'>

						<ImageCarousel
							photos={this.state.photoEntries}
							selected={(id) => {this.ProfileP.selectPhotoTapped(ImagePicker, this.setEditing, id, this.state.photoEntries)}} />

						{/* List of text inputs */}
						<FlatList
							style={edit_styles.flatList}
							data={this.ProfileP.getTextInputData(NO_DATA)}
							extraData={this.state}
							keyExtractor={this._keyExtractor}
							renderItem={this._renderItem}/>

						<TouchableOpacity style={edit_styles.submitTouchable}>
							<Button
								title='Save'
								onPress={() => this._getDataToUpdate()}/>
						</TouchableOpacity>

						{
							this.state.loaderVisible &&
							<View style={styles.loading} pointerEvents="none">
								<ActivityIndicator size='large' color="#0000ff" />
							</View>
						}
					</ScrollView>
				</View>
				<SafeAreaView style={{ flex:0, backgroundColor: '#F5FCFF' }} />
			</HandleBack>
			</KeyboardAvoidingView>
		);
	}
}

export default ProfileView;