Reference Source

src/components/models/profile-model.js

import Model from './model';
import Database from '../../util/database';
import AuthState from '../../util/authenticationstate';
import PersistStorage from '../../util/persistentstorage';
import ImageUtil from '../../util/imageutility';

const PROFILE_TYPE = ImageUtil.getTypes().PROFILE;

/**
 * Class for the Profile model to be used by the ProfilePresenter
 * @extends Model
 */
class ProfileModel extends Model {
	/**
	 * Creates an instance of ProfileModel. Sets the default callback, creates an observerlist,
	 * and registers an on read from the database.
	 *
	 * @constructor
	 */
	constructor() {
		super();
		this._callback = this._defaultCallback;

		this._data = {data: []};
		this._createObserverList();
		this._getUserDataFromDB();		
	}

	/**
	 * Default callback
	 */
	_defaultCallback(message) {
		console.log(message);
	}

	/**
	 * Set the model's callback to a new callback. This callback can be used anywhere and is usually passed in from a presenter.
	 *
	 * @param {Function} callback - A callback to run when certain code is executed
	 */
	setCallback(callback) {
		this._callback = callback;
	}

	/**
	 * Get the user ID from the database, authstate or persistent storage, then read from the database.
	 */
	_getUserDataFromDB() {
		const userID = AuthState.getCurrentUserID(); // Check this first, data might already be loaded
		if (userID == null) {
			this.checkAuthenticationState((verifiedUserID) => {
				this._readDBUserOnce(verifiedUserID);
			})
		} else {
			this._readDBUserOnce(userID);
		}
	}

	/**
	 * Read from the database once with the user id.
	 *
	 * @param {string} userID - The user's ID
	 */
	_readDBUserOnce(userID) {
		Database.readProfileDataOnce(userID, (snapshot) => {
			const retrievedData = snapshot.val();
			if (retrievedData != null && retrievedData != undefined) {
				const toStore = {
					profilePicture: retrievedData.thumbnail[0],
					full_name: retrievedData.full_name,
					email: retrievedData.email,
					phone_num: retrievedData.phone_num
				}
				this._insertDataOnRead(userID, retrievedData);
				this._addProfileDataLocally(userID, toStore);
				this._notifyAll(null); // Don't supply data to force a refresh by the presenter
			}
		});
	}

	/**
	 * Check the authentication state of the user.
	 * TODO : Consider extracting out to AuthenticationModel
	 *
	 * @param {Function} onComplete - A callback function to call when authentication has completed
	 */
	async checkAuthenticationState(onComplete) {
		// Offline authentication check first, if it fails, then check database
		await PersistStorage.retrieveData('userToken', async (userToken) => {
			if (userToken == null || userToken == undefined) {
				// Only check database user if no user token stored
				await Database.getCurrentUser((userID) => {
					onComplete(userID);
				});	
			} else {
				onComplete(userToken);
			}
		}, (error) => {
			onComplete(null);
		});
	}

	/**
	 * Get method for presenters to get data.
	 *
	 * @return {Object} data stored in the model
	 */
	get() {
		return {...this._data} // Immutable
	}

	/**
	 * Gets the profile data from Asyncstorage.
	 * @deprecated Use getProfileData instead
	 */
	getProfilePicture(callback) {
		this.getProfileData(callback);
	}

	/**
	 * Gets the profile data from Asyncstorage.
	 * @param {Function} callback - A callback to call with the retrieved data
	 */
	async getProfileData(callback) {
		const DEFAULT_PROFILE_IMAGE = ImageUtil.getDefaultImage(PROFILE_TYPE);
		const userID = AuthState.getCurrentUserID();
		const default_data = {id: userID, full_name: 'Not Found', thumbnail: [DEFAULT_PROFILE_IMAGE]};
		await PersistStorage.retrieveData(userID, (data) => {
			// console.log(data);
			if (data != null && data.startsWith('{') && data.endsWith('}')) {
				callback(JSON.parse(data));
				this._notifyAll(data); // Must supply data otherwise recursive call will start
			}
		}, (error) => {
			callback(default_data);
			this._notifyAll(default_data); // Must supply data otherwise recursive call will start
			console.log(error);
		});
	}

	/**
	 * Writes the profile data to Asyncstorage so it can be used later.
	 *
	 * @param {string} userID - The current user's id
	 * @param {Object} data - The data to be stored
	 */
	async _addProfileDataLocally(userID, data) {
		if (data != null && data != undefined && data != {}) {
			await PersistStorage.storeData(userID, JSON.stringify(data), (error) => {console.log(error)});
		}
	}

	/**
	 * Update method for presenters to update the model's data. Datetime and Owner are handled in database class.
	 *
	 * @param {Object} newData - New data to add
	 */
	update(newData) {
		if (newData.data.id === '' || newData.data.id == undefined) {
			newData.data.id = AuthState.getCurrentUserID();
			// This should never happen, but a fail safe just in-case
			if (newData.data.id == null || newData.data.id == undefined) {
				this._callback(false);
				return;
			}
		}

		try {
			// Check if it exists, and get index
			const {exists, index} = this._profileDataExists(newData);

			if (exists && this._checkImages(index, newData.data.thumbnail)) {
				newData.data.thumbnail = this._removeIllustrationKey(newData.data.thumbnail);
				this._insertDataOnUpdate(newData);
				this._editExistingInDatabase(newData.data, (result) => {this._callback(true); this._notifyAll(this._data);});

			} else {
				const { ProfileImages } = Database.getImageFolders();

				// Write to database
				this._writeImageToDBStorage(newData.data.id, newData.data.thumbnail, ProfileImages, (uploaded_images) => {
					newData.data.thumbnail = uploaded_images;

					// Check if there's actually images 
					if (!ImageUtil.checkImageListValid(uploaded_images)) {
						this._callback(false);
						return;
					}

					this._insertDataOnUpdate(newData);

					const funcCall = exists ? this._editExistingInDatabase : this._writeNewInDatabase;
					funcCall(newData.data, (result) => {

						this._callback(result); 
						this._notifyAll(this._data);
					});
					
					const toStore = {
						email: newData.data.email,
						full_name: newData.data.full_name,
						id: newData.data.id,
						phoneNum: newData.data.phoneNum,
						profilePicture: newData.data.thumbnail[0]
					}
					this._addProfileDataLocally(newData.data.id, toStore);

				}, this._callback);
			}
		} catch (error) {
			console.log(error);
			this._callback(false);
		}
	}

	/**
	 * Checks if there are new images in the bike stored vs what was passed in.
	 *
	 * @param {Number} index - The index of the bike in the local data
	 * @param {List} thumbnails - A list of thumbnails
	 * @return {Boolean} true: If the thumbnails are the same; false: If the thumbnails are different or if the bike doesn't exist
	 */
	_checkImages(index, thumbnails) {
		if (index >= 0) {
			const profile = this._data.data[index];
			return JSON.stringify(profile.thumbnail) == JSON.stringify(thumbnails);
		} else {
			return false; // Profile does not exist
		}
	}

	/**
	 * Removes the illustration key from the object and only adds the actual link.
	 *
	 * @param {List} thumbnails - A list of thumbnail objects with the property 'illustration'
	 * @return {List} A list of thumbnails
	 */
	_removeIllustrationKey(thumbnails) {
		let new_thumbnails = [];
		for (let i=0; i < thumbnails.length; i++) {
			new_thumbnails.push(thumbnails[i].illustration);
		}
		return new_thumbnails;
	}

	/**
	 * Write the image to the firebase storage and call the callbacks with the urls that were defined.
	 *
	 * @param {Number} id - The id of the profile corresponding to the image
	 * @param {List} images - A list of objects with the property 'illustration'
	 * @param {string} imagesFolder - The folder to upload images to
	 * @param {Function} onSuccess - A callback to call when an image has been successfully uploaded
	 * @param {Function} onError - A callback to call when an image has failed to upload
	 */
	_writeImageToDBStorage(id, images, imagesFolder, onSuccess, onError) {
		const FILE_EXTENSION = '.jpg';
		const DEFAULT_INDEX = 0;
		let uploaded_pictures = [];

		// If there are no images, return
		if (!ImageUtil.checkImageListValid(images)) {
			onError(false);
			return;
		}
	
		// Check if there's a default image, if so, skip it and just use the default image
		if (ImageUtil.isDefaultImage(PROFILE_TYPE, images[DEFAULT_INDEX].illustration)) {
			onSuccess([images[DEFAULT_INDEX].illustration]);
			return;
		}

		// Check if the image has already been uploaded, if it has, just skip it
		if (ImageUtil.isAlreadyUploaded(images[DEFAULT_INDEX].illustration)) {
			uploaded_pictures.push(images[DEFAULT_INDEX].illustration);
			onSuccess(uploaded_pictures);
			return;
		}

		// Name of file is the index and the file extension
		const filename = DEFAULT_INDEX + ImageUtil.getFileExtension();
		// Write image to database
		Database.writeImage(id, images[DEFAULT_INDEX].illustration, filename, imagesFolder, (url) => {
			uploaded_pictures.push(url);
			onSuccess(uploaded_pictures);
			return url;
		}, (error) => {
			console.log(error);
			onError(false);
		});

	}

	// Could generalize _writeNewInDatabase and _editExistingInDatabase into one function

	/**
	 * Write new data in database and call the function callback depending on if it was successful or not.
	 *
	 * @param {Object} newData - Data to be written to the database
	 * @param {Function} callback - A function to call on the success or failure of the call
	 */
	_writeNewInDatabase(newData, callback) {
		return Database.writeProfileData(newData, (data) => {
			// console.log(data);
			callback(typeof data !== 'undefined' && data !== undefined);
			// return typeof data !== 'undefined' && data !== undefined
			// this._callback(typeof data !== 'undefined' && data !== undefined);
		},(error) => {
			console.log(error);
			callback(false);
			// this._callback(false);
		});
	}

	/**
	 * Overwrite existing data in database and call the function callback depending on if it was successful or not.
	 *
	 * @param {Object} newData - Data to be written to the database
	 * @param {Function} callback - A function to call on the success or failure of the call
	 */
	_editExistingInDatabase(newData, callback) {
		return Database.editProfileData(newData, (data) => {
			// console.log(data);
			callback(typeof data !== 'undefined' && data !== undefined);
			// return typeof data !== 'undefined' && data !== undefined;
			// this._callback(typeof data !== 'undefined' && data !== undefined);
		},(error) => {
			console.log(error);
			callback(false);
			// this._callback(false);
		});
	}

	/**
	 * Insert data into the data object on an update trigger (from Presenter).
	 *
	 * @param {Object} newData - New data passed in, of the form : {data: []}
	 * @return {Boolean} true: Data was an edited value; false: Data was a new value
	 */
	_insertDataOnUpdate(newData) {
		let i = 0;

		// If only one piece, just insert it
		if (this._data.data.length === 0) {
			this._data.data.push(newData.data);
			return false;
		}

		this._data.data[0] = newData.data;  // Data found, overwrite
		return true;
	}

	/**
	 * Checks if the profile exists based on the data of the profile.
	 * 
	 * @param {Object} profileData - The data to check
	 * @return {Boolean, Number} exists: true: If the profile exists; false: otherwise. index - The index of the profile if it exists, -1 if not
	 */
	_profileDataExists(profileData) {
		return this._profileIDExists(profileData.data.id)
	}

	/**
	 * Checks if the profile exists based on the id.
	 *
	 * @param {string} id - The id of a profile
	 * @return {Boolean, Number} exists: true: If the profile exists; false: otherwise. index - The index of the profile if it exists, -1 if not
	 */
	_profileIDExists(id) {
		const exists = this._data.data[0].id === id;
		const index = exists ? 0 : -1;
		return { exists, index };
	}

	/**
	 * Checks if an object has a certain property.
	 * 
	 * @param {Object} obj - An object to check
	 * @param {string} property - The name of a property
	 * @return {Boolean} true: if the object has the property; false: otherwise
	 */
	_hasProperty(obj, property) {
		return obj.hasOwnProperty(property);
	}

	/**
	 * Insert data into the data object on a read from the database.
	 *
	 * @param {string} currentUser - The user id of the current user
	 * @param {Object} databaseData - An objects of objects containing data from the database.
	 */
	_insertDataOnRead(currentUser, databaseData) {
		let tempData = {data:[]};
		if (databaseData != null) { // Check if there are objects in the database

			// If it doesn't have an id, skip it because it isn't valid
			if (!this._hasProperty(databaseData, 'id') || currentUser == null || currentUser != databaseData.id) { 
				return;
			}

			tempData.data.push(databaseData);
			this._data = tempData;
		}
		// console.log(this._data);
	}
}

export default ProfileModel;