Reference Source

src/components/presenters/profile-presenter.js

import BasePresenter from './presenter';
import { ProfileM } from '../models/export-models'; // Using the ProfileModel class because an ProfileModel class would have the same purpose
import ImageUtil from '../../util/imageutility';

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

/**
 * Class for the Profile presenter and view
 * @extends BasePresenter
 */
class ProfilePresenter extends BasePresenter {
	/**
	 * Creates an instance of ProfilePresenter
	 *
	 * @constructor
	 * @param {Object} view - An instance of a view class
	 */
	constructor(view) {
		super();
		this.view = view;
		this.currentPhotos = Object.assign(ImageUtil.getPhotoEntries(PROFILE_TYPE));
		ProfileM.subscribe(this);
	}

	/**
	 * Updates the profile model with new data.
	 *
	 * @param {Object} newData - New data to update the model's data with.
	 * @param {Function} callback - A function that will execute a callback when accessing is complete
	 */
	update = (newData, callback) => {
		const builtData = this._buildDataFromView(newData);

		// TODO : Proper checking to see if it was uploaded. Consider adding callback to onUpdated
		ProfileM.setCallback(callback);

		ProfileM.update(builtData);
	}

	/**
	 * Build the data obtained from the view and insert it into a new data object.
	 * Current attributes of newData object: 
	 * 			{Object} inputTextData: [{name, multiline, profile_editable, text}]
	 * 			{Object} picture: uri
	 *		
	 *
	 * @param {Object} newData - The new data from the view. 
	 * @return {Object} The built data of the object. Attributes: data
	 */
	_buildDataFromView = (newData) => {
		const inputTextData = newData.inputTextData;
		const pictureSource = newData.picture;
		const currentID = newData.currentID;

		let builtData = {
			data: {
				id: currentID,
				email: inputTextData[inputDataList.index.email].text,
				phoneNum: inputTextData[inputDataList.index.phoneNum].text,
				full_name: inputTextData[inputDataList.index.full_name].text,
				thumbnail: pictureSource != null ? pictureSource : [{illustration: ImageUtil.getDefaultImage(PROFILE_TYPE)}]
			}
		}

		return builtData;
	}


	/**
	 * Called when the model is updated with new data. Refreshes the state of the view.
	 * Method is supplied with the data to add.
	 * Better way to refresh the state?
	 *
	 * @param {Object} newData - New data to add.
	 */
	onUpdated = (newData) => {
		// Do something with the new data or let the view auto update?
		this.view.refreshState();
	};


	/**
	 * Called when the model is updated with new data. Refreshes the state of the view.
	 * Better way to refresh the state?
	 */
	onUpdated = () => {
	 	this.view.refreshState();
	};

	/**
	 * Gets the data from the model and returns it to the caller.
	 *
	 * @return {Object} Data from the model.
	 */
	getData = () => {
		return ProfileM.get().data;
	};

	/**
	 * If the view or presenter is destroyed, unsubscribe the presenter from the model.
	 */
	onDestroy = () => {
		ProfileM.unsubscribe(this);
	};


	/**
	 * Checks the editing state of the view and calls one of its passed in functions.
	 *
	 * @param {Boolean} editingState - The editing state of the view
	 * @param {Function} success - A function to call on a true value of the editing state
	 * @param {Function} failure - A function to call on a false value of the editing state
	 */
	checkEditingState = (editingState, success, failure) => {
		// Do any checks on the editing state
		if (editingState) {
			success();
		} else {
			failure();
		}
	}

	
	/**
	 * Open the image picker. Set the editing option to true.
	 *
	 * @param {Object} imagePicker - The ImagePicker class from react-native-image-picker
	 * @param {Function} setEditing - A function so the presenter can set the editing value
	 * @param {Number} id - The index of the photo to change
	 * @param {List} photos - A list of photos as strings
	 */
	selectPhotoTapped(imagePicker, setEditing, id, photos) {
		const options = {
			quality: 1.0,
			maxWidth: 500,
			maxHeight: 500,
			storageOptions: {
				skipBackup: true,
			},
		};

		 // Set the editing state to true by calling a passed in function where the view can do what it needs to
		setEditing(true);

		imagePicker.showImagePicker(options, (response) => {
			console.log('Response = ', response);

			if (response.didCancel) {
				console.log('User cancelled photo picker');
			} else if (response.error) {
				console.log('ImagePicker Error: ', response.error);
			} else if (response.customButton) {
				console.log('User tapped custom button: ', response.customButton);
			} else {
				let source = { uri: response.uri };

				// You can also display the image using data:
				// let source = { uri: 'data:image/jpeg;base64,' + response.data };

				photos[id].illustration = source;
				this.currentPhotos = photos;

				console.log(id, photos);

				this.view.setState({
					photoEntries: JSON.parse(JSON.stringify(this.currentPhotos)),
				});

				this.view.refreshState();
			}
		});
	}

	/**
	 * @private
	 * Makes sure the object with the key exists.
	 */
	getProp = (object, key) => object && this.check(object[key]);

	/**
	 * @private
	 * Simple regex check
	 * 
	 * return {Boolean}
	 */
	check = (s) => {
		return s.replace(/[\W\[\] ]/g, function (a) {
			return a;
		})
	};

	/**
	 * Checks the input data for required inputs and calls an alert function if inputs are missing.
	 *
	 * @param {List} inputData - A list of input data (see inputDataList for structure)
	 * @param {Function} inputRequirementFailure - A function that will define the alert to be displayed.
	 * @return {Boolean} true: some required inputs are blank; false: required inputs are not blank
	 */
	checkInputs = (inputData, inputRequirementFailure) => {
		let required = this._getRequiredInputs(inputData);
		let names = [];
		for (let i=0; i < required.length; i++) {
			if (required[i].text === "") {
				names.push(required[i].name);
			}
		}
		if (names.length !== 0) {
			inputRequirementFailure(names);
			return true;
		} else {
			return false;
		}
	}

	/**
	 * Returns the required inputs based on the required property
	 *
	 * @param {List} inputs - A list of input data
	 * @return {List} A list of required text inputs
	 */
	_getRequiredInputs = (inputs) => {
		return inputs.filter(obj => {return obj.required});
	}


	/**
	 * Return the data for the text inputs.
	 * Object:
	 *		name: the name/label of the text input
	 * 		multiline: true: if the input is allowed to span multiple lines; false: otherwise
	 *		text: initial text of the input
	 *
	 * @param {Object} data - type 'Object' if there is data, type 'string' if no data
	 * @return {List} A list of data objects (name, multiline, text)
	 */
	getTextInputData = (data) => {
		return data === NO_DATA ? this._deepCopy(inputDataList.data) : this._translateDataToInput(data);
	}


	/**
	 * Translates data input (Profile data) to the text inputs. Could be refactored to be made easier for adaptations.
	 *
	 * @param {Object} data - The data from the view (=== 'NO-DATA' if not set)
	 * @return {List} A copy of the data that is now in the form of the text input
	 */
	_translateDataToInput = (data) => {
		let dataCopy = this._deepCopy(inputDataList.data);

		// We take data[0] because the data is a list
		dataCopy[inputDataList.index.email].text 	= this._getString(data[0].email);
		dataCopy[inputDataList.index.full_name].text 	= this._getString(data[0].full_name);
		dataCopy[inputDataList.index.phoneNum].text 	= this._getString(data[0].phoneNum);

		this.currentPhotos = ImageUtil.formThumbnail(data[0].thumbnail);
		this.view.setState({ currentID: data.id });

		return this._deepCopy(dataCopy); 
	}


	/**
	 * Checks if the value is valid and if so, convert it to a string.
	 *
	 * @param {Number/string} val - A number or string to check
	 * @return {string} Value converted to a string
	 */
	_getString = (val) => {
		return val == undefined || val == null ? '' : val.toString();
	}

	/**
	 * Resets the current photos to the default photos.
	 */
	clearPhotos = () => {
		this.currentPhotos = ImageUtil.getDefaultPhotos(PROFILE_TYPE);
	}

	/**
	 * Returns a deep copy of the current photos.
	 *
	 * @return {List} A list of the current photos
	 */
	getCurrentPhotos = () => {
		return JSON.parse(JSON.stringify(this.currentPhotos));
	}

	/**
	 * Returns a deep copy of the array by reassigning the values. This is to make sure we can clear the data.
	 *
	 * @return {List} A list to copy
	 */
	_deepCopy = (array) => {
		return array.map(a => Object.assign({}, a));
	}
}

export default ProfilePresenter;


// List of text inputs for adding Profile information. Items in list appear in this order
/*
 * data:
 *		name: The name/label of the text input
 *		multiline: true: if the user's text can span multiple lines; false: otherwise
 *		text: The initial text of the name/label
 */
const inputDataList = {
	index: {
		email: 		0,
		full_name: 	1,
		phoneNum: 	2,
	},
	data: [
		{
			name: 'Email',
			multiline: false,
			disabled: true,
			required: false,
			text: '',
			keyboardType: 'default'
		},
		{
			name: 'Full Name',
			multiline: false,
			disabled: false,
			required: false,
			text: '',
			keyboardType: 'default'
		},
		{
			name: 'Phone Number',
			multiline: false,
			disabled: false,
			required: false,
			text: '',
			keyboardType: 'number-pad'
		},
	]
}