<commit_before>/**
 * This is the main bot file. All bot logic should go here.
 */
'use strict'

// imports
const Fs = require('fs');
const Client = require('node-rest-client').Client;
const Task = require('./model/task.js');
const TaskMessageAction = require('./model/taskMessageAction.js');
const User = require('./model/user.js');
const Util = require('./util.js');
const WebClient = require('@slack/client').WebClient;
const Scheduler = require('./messageScheduler.js');
const Constants = require("./constants");

function Bot() {
	// tasks is a two-dimensional array where the first dimension is the group
	// and the second dimension are the tasks in the group.
	// It is to make it easier to group recurring tasks.
	this.tasks = [];
	this.util = new Util();
	this.client = new Client();
	this.client.registerMethod("slackTeams", "https://beepboophq.com/api/v1/slack-teams", "GET");
	this.initializeWebClient();
	//Calling here so it doesnt have to wait the scheduled task.
	this.generateTasks();
	//As soon as the bot start it should setup the reminder.
	this.setupRecurrentTasks();

}

/**
 * Reads the tasks.json file and create the tasks.
 */
Bot.prototype.generateTasks = function() {
	// empty the tasks first
	this.tasks = new Array(5);

	// read the JSON file
	var tasksJSON = JSON.parse(Fs.readFileSync('model/tasks.json', 'utf8')).tasks;

	//Create new array to hold tasks
	for (var i = 0; i < 5; i++) {
		this.tasks[i] = [];
	}
	//insert tasks in the array
	for (var i in tasksJSON) {
		var title = tasksJSON[i].title;
		var description = tasksJSON[i].description;
		var tacos = tasksJSON[i].tacos;
		var days = tasksJSON[i].days;
		var id = tasksJSON[i].id;
		for (var j in days) {
			var task = new Task(id, title, description, tacos, days[j]);
			this.tasks[days[j]].push(task);
		}
	}
}

/**
 * Lists the tasks in the channel
 *
 * @param msg Slack message
 * @param replace boolean to know if the message should be replaced or not
 */
Bot.prototype.listTasks = function(msg, replace) {
	// create the attachments
	var attachments = this.getTodayTaskAttachments();
	var msgTitle = Constants.DayTasksTitle;
	//If not a valid day in the array should be empty tasks
	if (!attachments) {
		attachments = [];
		msgTitle = Constants.NoTasksTitle;
	}

	if (replace) {
		msg.respond({
			text: msgTitle,
			attachments: attachments,
		});
	} else {
		msg.say({
			text: msgTitle,
			attachments: attachments,
		});
	}
}

/**
 * Lists the tasks in a specific channel
 *
 * @param channel_id Id of the channel to list
 */
Bot.prototype.listTasksOnChannel = function(channel_id) {
	// create the attachments
	var attachments = this.getTodayTaskAttachments();
	var msgTitle = Constants.DayTasksTitle;
	//If not a valid day in the array should be empty tasks
	if (!attachments) {
		attachments = [];
		msgTitle = Constants.NoTasksTitle;
	}

	this.webClient.chat.postMessage(channel_id,
		msgTitle, {
			attachments: attachments,
			as_user: true
		},
		function(err, res) {
			if (err) {
				console.log('Error:', err);
			} else {
				console.log('Message sent: ', res);
			}
		});

}

/**
 * Get list of attachments for today's tasks.
 */
Bot.prototype.getTodayTaskAttachments = function() {
		var currentDay = this.util.currentDay();
		//If not a valid day in the array should be empty tasks
		var tasksToList = this.tasks[currentDay];
		if (tasksToList) {
			return this.getTasksListMessageAttachments(tasksToList);
		}
		return null
	}
	/**
	 * Get the attachments for tasks listing message
	 *
	 * @param tasks tasks to be sent in the message
	 * @return formatted attachment to post as message
	 */
Bot.prototype.getTasksListMessageAttachments = function(tasks) {
	var attachments = [];
	for (var i in tasks) {
		var task = tasks[i];
		if (!task.assignee) {
			var actions = [new TaskMessageAction('pick', 'Pick', 'button', tasks[i].id)];
			attachments.push(this.getTaskMessageAttachment(task, Constants.TaskListCB, actions));
		}
	}
	return attachments;
}

/**
 * Lists atask for a given user. Sent in a private message.
 *
 * @param task_id id of the task to be displayed
 * @param user_id id of the user
 */
Bot.prototype.displayTaskForUser = function(task_id, user_id) {

	var task = this.getTaskForUser(task_id, user_id);
	if (task) {
		//FIXME: Add proper message
		this.webClient.chat.postMessage(user_id,
			'You have a new task!', {
				attachments: this.getUserTasksListMessageAttachments([task]),
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	} else {
		this.webClient.chat.postMessage(user_id,
			Constants.UserNoTasksTitle, {
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	}
}

/**
 * Lists tasks for a given user. Sent in a private message.
 *
 * @param user_id id of the user
 */
Bot.prototype.listUserTasks = function(user_id) {
	// FIXME IMO not very optimal to generate this array when only tasks for one user are required
	var tasksDictionary = this.getUsersTasks();
	var tasks = tasksDictionary[user_id];
	if (tasks) {
		this.webClient.chat.postMessage(user_id,
			Constants.UserTasksTitle, {
				attachments: this.getUserTasksListMessageAttachments(tasks),
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	} else {
		this.webClient.chat.postMessage(user_id,
			Constants.UserNoTasksTitle, {
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	}
}

/**
 * Creates a list of message attachments based on a array of tasks
 *
 * @param tasks tasks to be sent in the message
 * @param callback_id callback to listen to the response
 * @param actions actions for this tasks
 */
Bot.prototype.getUserTasksListMessageAttachments = function(tasks) {
	var attachments = [];
	for (var i in tasks) {
		//I Think we rather create a new array than mutate it every loop, but not sure in js.
		//change this if wrong or remove comment when validated.
		var actions = [new TaskMessageAction('unpick', 'Unpick', 'button', tasks[i].id), new TaskMessageAction('done', 'Done', 'button', tasks[i].id)]
		attachments.push(this.getTaskMessageAttachment(tasks[i], Constants.UserTaskListCB, actions));
	}
	return attachments;
}

/**
 * Returns the message attachment for a task
 *
 * @param task The task itself
 * @param callback_id callback to listen to the response
 * @param taskActions actions for this task
 */
Bot.prototype.getTaskMessageAttachment = function(task, callback_id, taskActions) {
	var actions = [];
	for (var i in taskActions) {
		actions.push({
			name: taskActions[i].name,
			text: taskActions[i].text,
			type: taskActions[i].type,
			value: taskActions[i].value
		});
	}
	return {
		title: task.title,
		text: task.description,
		fields: [{
			title: "Tacos",
			value: task.tacos,
			short: true
		}],
		callback_id: callback_id,
		attachment_type: "default",
		actions: actions
	};
}

/**
 * Assign a task
 *
 * @param user JSON representing a user returned by slack API
 * @param task_id id of the task to be assigned
 */
Bot.prototype.assignTask = function(user, task_id) {

	var task = this.getTask(task_id, this.util.currentDay());
	task.assignee = new User(user.id, user.name);
	this.displayTaskForUser(task_id, user.id);
}

/**
 * Unassign a user from a task
 *
 * @param task_id id of task to unassign
 */
Bot.prototype.unassignUserFromTask = function(task_id) {
	var task = this.getTask(task_id, this.util.currentDay());
	task.assignee = null;
}

/**
 * Unassign a task
 *
 * @param msg Slack message
 * @param task_id id of task to unassign
 */
Bot.prototype.unassignTask = function(msg, task_id) {

	this.unassignUserFromTask(task_id);
	//FIXME: Add proper message.
	var field = {
		title: "",
		value: "You unpicked the task. You\'re really a :hankey:",
		short: false
	}
	this.taskMessageUpdate(msg, field);

}

/**
 * Complete a task
 *
 * @param msg Slack message
 * @param task_id id of the completed task
 */
Bot.prototype.completeTask = function(msg, task_id) {
	var task = this.getTask(task_id, this.util.currentDay());
	task.done = true;

	var field = {
		title: "",
		value: "You completed a task! :presidio:",
		short: false
	}

	this.taskMessageUpdate(msg, field);

}

/**
 * Complete a task
 *
 * @param msg Slack message
 * @param field field to be added to the message. this should represent the update on the message
 */
Bot.prototype.taskMessageUpdate = function(msg, field) {

	var attachments = msg.body.original_message.attachments;
	var attachmentIndex = msg.body.attachment_id - 1;
	attachments[attachmentIndex].actions = [];
	attachments[attachmentIndex].fields.push(field);

	msg.respond({
		text: msg.body.original_message.text,
		attachments: attachments,
	});
}

/**
 * Get a task by id and day
 *
 * @param task_id id of the task
 * @param day selected day of the task
 * @return Task
 */
Bot.prototype.getTask = function(task_id, day) {
	var tasks = this.tasks[day];
	for (var i in tasks) {
		if (tasks[i].id == task_id) {
			return tasks[i];
		}
	}
	return null;
}

/**
 * Initialize Slack Web API client.
 * It gets the Access Token from BeepBoop.
 *
 */
Bot.prototype.initializeWebClient = function() {
	var beepBoopToken = process.env.BEEPBOOP_TOKEN || '';
	var args = {
		headers: {
			"Authorization": "Bearer " + beepBoopToken
		}
	};

	var self = this;
	this.client.methods.slackTeams(args, function(data, response) {
		self.webClient = new WebClient(data.results[0].slack_bot_access_token);
	});
}

/**
 *  Setup system recurrent tasks.
 *
 */
Bot.prototype.setupRecurrentTasks = function() {
	var scheduler = new Scheduler();
	this.setupTaskReminder(scheduler);
	this.setupTaskGeneration(scheduler);
	this.setupTaskListing(scheduler);
}

/**
 * Setup a listing of user's own tasks every day at 9pm
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskReminder = function(remindScheduler) {
	var self = this;
	remindScheduler.scheduleCallback([1, 2, 3, 4, 5], [21], [0], function() {
		self.remindUserTasks();
	});
}

/**
 * Setup task generation for every monday at 8:29 am
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskGeneration = function(remindScheduler) {
	var self = this;
	remindScheduler.scheduleCallback([1], [8], [29], function() {
		self.generateTasks();
	});
}

/**
 * Setup system to list tasks on a channel(TBD) for every monday at 8:30 am
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskListing = function(remindScheduler) {
		var self = this;
		remindScheduler.scheduleCallback([1, 2, 3, 4, 5], [8], [30], function() {
			//FIXME: Hardcoded channel id on officebots for the moment, change when decided the proper one.
			self.bot.listTasksOnChannel('G3466NZT4');
		});
	}
	/**
	 * Reminds all users of their tasks in private message.
	 *
	 */
Bot.prototype.remindUserTasks = function() {
	var usersTasks = this.getUsersTasks();
	for (var user_id in usersTasks) {
		// FIXME
		// var formatedTasks = this.listUserTasksPM([usersTasks[user_id]], user_id);
		// this.listUserTasksPM(formatedTasks, user_id);
	}
}

/**
 * Returns a dictionnary of tasks where keys are user_id
 *
 */
Bot.prototype.getUsersTasks = function() {
	var userTaskDictionary = {};
	this.tasks.forEach(function(taskGroup) {
		taskGroup.forEach(function(task) {
			if (task.assignee) {
				if (!userTaskDictionary[task.assignee.id]) {
					userTaskDictionary[task.assignee.id] = [];
				}
				userTaskDictionary[task.assignee.id].push(task);
			}
		});
	});
	return userTaskDictionary;
}

/**
 * Returns a list of tasks assigned to a user_id
 *
 */
Bot.prototype.getUserTasks = function(user_id) {
	var userTasks = [];
	this.tasks.forEach(function(taskGroup) {
		taskGroup.forEach(function(task) {
			if (task.assignee && task.assignee.id == user_id) {
				userTasks.push(task);
			}
		});
	});
	return userTasks;
}

/**
 * return a task with that ID associated to a user
 *
 * @param user_id id of the user.
 * @param task_id id of the task.
 */
Bot.prototype.getTaskForUser = function(task_id, user_id) {
	var task;
	for (var i in this.tasks) {
		for(var j in this.tasks[i]) {
			task = this.tasks[i][j];
			if (task.id == task_id && task.assignee && task.assignee.id == user_id) {
				return task;
			}
		}
	}
	return null;
}

module.exports = Bot;<commit_msg>bot now reminds users at 15 and 20 of their tasks<commit_after>/**
 * This is the main bot file. All bot logic should go here.
 */
'use strict'

// imports
const Fs = require('fs');
const Client = require('node-rest-client').Client;
const Task = require('./model/task.js');
const TaskMessageAction = require('./model/taskMessageAction.js');
const User = require('./model/user.js');
const Util = require('./util.js');
const WebClient = require('@slack/client').WebClient;
const Scheduler = require('./messageScheduler.js');
const Constants = require("./constants");

function Bot() {
	// tasks is a two-dimensional array where the first dimension is the group
	// and the second dimension are the tasks in the group.
	// It is to make it easier to group recurring tasks.
	this.tasks = [];
	this.util = new Util();
	this.client = new Client();
	this.client.registerMethod("slackTeams", "https://beepboophq.com/api/v1/slack-teams", "GET");
	this.initializeWebClient();
	//Calling here so it doesnt have to wait the scheduled task.
	this.generateTasks();
	//As soon as the bot start it should setup the reminder.
	this.setupRecurrentTasks();

}

/**
 * Reads the tasks.json file and create the tasks.
 */
Bot.prototype.generateTasks = function() {
	// empty the tasks first
	this.tasks = new Array(5);

	// read the JSON file
	var tasksJSON = JSON.parse(Fs.readFileSync('model/tasks.json', 'utf8')).tasks;

	//Create new array to hold tasks
	for (var i = 0; i < 5; i++) {
		this.tasks[i] = [];
	}
	//insert tasks in the array
	for (var i in tasksJSON) {
		var title = tasksJSON[i].title;
		var description = tasksJSON[i].description;
		var tacos = tasksJSON[i].tacos;
		var days = tasksJSON[i].days;
		var id = tasksJSON[i].id;
		for (var j in days) {
			var task = new Task(id, title, description, tacos, days[j]);
			this.tasks[days[j]].push(task);
		}
	}
}

/**
 * Lists the tasks in the channel
 *
 * @param msg Slack message
 * @param replace boolean to know if the message should be replaced or not
 */
Bot.prototype.listTasks = function(msg, replace) {
	// create the attachments
	var attachments = this.getTodayTaskAttachments();
	var msgTitle = Constants.DayTasksTitle;
	//If not a valid day in the array should be empty tasks
	if (!attachments) {
		attachments = [];
		msgTitle = Constants.NoTasksTitle;
	}

	if (replace) {
		msg.respond({
			text: msgTitle,
			attachments: attachments,
		});
	} else {
		msg.say({
			text: msgTitle,
			attachments: attachments,
		});
	}
}

/**
 * Lists the tasks in a specific channel
 *
 * @param channel_id Id of the channel to list
 */
Bot.prototype.listTasksOnChannel = function(channel_id) {
	// create the attachments
	var attachments = this.getTodayTaskAttachments();
	var msgTitle = Constants.DayTasksTitle;
	//If not a valid day in the array should be empty tasks
	if (!attachments) {
		attachments = [];
		msgTitle = Constants.NoTasksTitle;
	}

	this.webClient.chat.postMessage(channel_id,
		msgTitle, {
			attachments: attachments,
			as_user: true
		},
		function(err, res) {
			if (err) {
				console.log('Error:', err);
			} else {
				console.log('Message sent: ', res);
			}
		});

}

/**
 * Get list of attachments for today's tasks.
 */
Bot.prototype.getTodayTaskAttachments = function() {
		var currentDay = this.util.currentDay();
		//If not a valid day in the array should be empty tasks
		var tasksToList = this.tasks[currentDay];
		if (tasksToList) {
			return this.getTasksListMessageAttachments(tasksToList);
		}
		return null
	}
	/**
	 * Get the attachments for tasks listing message
	 *
	 * @param tasks tasks to be sent in the message
	 * @return formatted attachment to post as message
	 */
Bot.prototype.getTasksListMessageAttachments = function(tasks) {
	var attachments = [];
	for (var i in tasks) {
		var task = tasks[i];
		if (!task.assignee) {
			var actions = [new TaskMessageAction('pick', 'Pick', 'button', tasks[i].id)];
			attachments.push(this.getTaskMessageAttachment(task, Constants.TaskListCB, actions));
		}
	}
	return attachments;
}

/**
 * display a task for a given user. Sent in a private message.
 *
 * @param task_id id of the task to be displayed
 * @param user_id id of the user
 */
Bot.prototype.displayTaskForUser = function(task_id, user_id) {

	var task = this.getTaskForUser(task_id, user_id);
	if (task) {
		//FIXME: Add proper message
		this.webClient.chat.postMessage(user_id,
			'You have a new task!', {
				attachments: this.getUserTasksListMessageAttachments([task]),
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	} else {
		this.webClient.chat.postMessage(user_id,
			Constants.UserNoTasksTitle, {
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	}
}

/**
 * Lists tasks for a given user. Sent in a private message.
 *
 * @param user_id id of the user
 */
Bot.prototype.listUserTasks = function(user_id) {
	var tasks = this.getUserUncompletedTasks(user_id);
	if (tasks) {
		this.webClient.chat.postMessage(user_id,
			Constants.UserTasksTitle, {
				attachments: this.getUserTasksListMessageAttachments(tasks),
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	} else {
		this.webClient.chat.postMessage(user_id,
			Constants.UserNoTasksTitle, {
				as_user: true
			},
			function(err, res) {
				if (err) {
					console.log('Error:', err);
				}
			});
	}
}

/**
 * Creates a list of message attachments based on a array of tasks
 *
 * @param tasks tasks to be sent in the message
 * @param callback_id callback to listen to the response
 * @param actions actions for this tasks
 */
Bot.prototype.getUserTasksListMessageAttachments = function(tasks) {
	var attachments = [];
	for (var i in tasks) {
		//I Think we rather create a new array than mutate it every loop, but not sure in js.
		//change this if wrong or remove comment when validated.
		var actions = [new TaskMessageAction('unpick', 'Unpick', 'button', tasks[i].id), new TaskMessageAction('done', 'Done', 'button', tasks[i].id)]
		attachments.push(this.getTaskMessageAttachment(tasks[i], Constants.UserTaskListCB, actions));
	}
	return attachments;
}

/**
 * Returns the message attachment for a task
 *
 * @param task The task itself
 * @param callback_id callback to listen to the response
 * @param taskActions actions for this task
 */
Bot.prototype.getTaskMessageAttachment = function(task, callback_id, taskActions) {
	var actions = [];
	for (var i in taskActions) {
		actions.push({
			name: taskActions[i].name,
			text: taskActions[i].text,
			type: taskActions[i].type,
			value: taskActions[i].value
		});
	}
	return {
		title: task.title,
		text: task.description,
		fields: [{
			title: "Tacos",
			value: task.tacos,
			short: true
		}],
		callback_id: callback_id,
		attachment_type: "default",
		actions: actions
	};
}

/**
 * Assign a task
 *
 * @param user JSON representing a user returned by slack API
 * @param task_id id of the task to be assigned
 */
Bot.prototype.assignTask = function(user, task_id) {

	var task = this.getTask(task_id, this.util.currentDay());
	task.assignee = new User(user.id, user.name);
	this.displayTaskForUser(task_id, user.id);
}

/**
 * Unassign a user from a task
 *
 * @param task_id id of task to unassign
 */
Bot.prototype.unassignUserFromTask = function(task_id) {
	var task = this.getTask(task_id, this.util.currentDay());
	task.assignee = null;
}

/**
 * Unassign a task
 *
 * @param msg Slack message
 * @param task_id id of task to unassign
 */
Bot.prototype.unassignTask = function(msg, task_id) {

	this.unassignUserFromTask(task_id);
	//FIXME: Add proper message.
	var field = {
		title: "",
		value: "You unpicked the task. You\'re really a :hankey:",
		short: false
	}
	this.taskMessageUpdate(msg, field);

}

/**
 * Complete a task
 *
 * @param msg Slack message
 * @param task_id id of the completed task
 */
Bot.prototype.completeTask = function(msg, task_id) {
	var task = this.getTask(task_id, this.util.currentDay());
	task.done = true;

	var field = {
		title: "",
		value: "You completed a task! :presidio:",
		short: false
	}

	this.taskMessageUpdate(msg, field);

}

/**
 * Complete a task
 *
 * @param msg Slack message
 * @param field field to be added to the message. this should represent the update on the message
 */
Bot.prototype.taskMessageUpdate = function(msg, field) {

	var attachments = msg.body.original_message.attachments;
	var attachmentIndex = msg.body.attachment_id - 1;
	attachments[attachmentIndex].actions = [];
	attachments[attachmentIndex].fields.push(field);

	msg.respond({
		text: msg.body.original_message.text,
		attachments: attachments,
	});
}

/**
 * Get a task by id and day
 *
 * @param task_id id of the task
 * @param day selected day of the task
 * @return Task
 */
Bot.prototype.getTask = function(task_id, day) {
	var tasks = this.tasks[day];
	for (var i in tasks) {
		if (tasks[i].id == task_id) {
			return tasks[i];
		}
	}
	return null;
}

/**
 * Initialize Slack Web API client.
 * It gets the Access Token from BeepBoop.
 *
 */
Bot.prototype.initializeWebClient = function() {
	var beepBoopToken = process.env.BEEPBOOP_TOKEN || '';
	var args = {
		headers: {
			"Authorization": "Bearer " + beepBoopToken
		}
	};

	var self = this;
	this.client.methods.slackTeams(args, function(data, response) {
		self.webClient = new WebClient(data.results[0].slack_bot_access_token);
	});
}

/**
 *  Setup system recurrent tasks.
 *
 */
Bot.prototype.setupRecurrentTasks = function() {
	var scheduler = new Scheduler();
	this.setupTaskReminder(scheduler);
	this.setupTaskGeneration(scheduler);
	this.setupTaskListing(scheduler);
	this.setupUncompletedTasksReminder(scheduler);
}

/**
 * Setup a listing of user's uncompleted tasks every day at 3pm and 8pm
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupUncompletedTasksReminder = function(remindScheduler) {
	var self = this;
	//FIXME: left 2 separated methods in case we want to call with a different message. validate this, if not merge with one bellow.
	remindScheduler.scheduleCallback([1, 2, 3, 4, 5], [15, 20], [0], function() {
		self.remindUserTasks();
	});
}

/**
 * Setup a listing of user's own tasks every day at 9pm
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskReminder = function(remindScheduler) {
	var self = this;
	remindScheduler.scheduleCallback([1, 2, 3, 4, 5], [21], [0], function() {
		self.remindUserTasks();
	});
}

/**
 * Setup task generation for every monday at 8:29 am
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskGeneration = function(remindScheduler) {
	var self = this;
	remindScheduler.scheduleCallback([1], [8], [29], function() {
		self.generateTasks();
	});
}

/**
 * Setup system to list tasks on a channel(TBD) for every monday at 8:30 am
 *
 * @param remindScheduler scheduler for task setup.
 */
Bot.prototype.setupTaskListing = function(remindScheduler) {
		var self = this;
		remindScheduler.scheduleCallback([1, 2, 3, 4, 5], [8], [30], function() {
			//FIXME: Hardcoded channel id on officebots for the moment, change when decided the proper one.
			self.bot.listTasksOnChannel('G3466NZT4');
		});
	}
	/**
	 * Reminds all users of their tasks in private message.
	 *
	 */
Bot.prototype.remindUserTasks = function() {
	var usersTasks = this.getUsersUncompletedTasks();
	for (var user_id in usersTasks) {
		this.listUserTasks(user_id);
	}
}

/**
 * Returns a dictionnary of tasks where keys are user_id
 *
 */
Bot.prototype.getUsersTasks = function() {
	var userTaskDictionary = {};
	this.tasks.forEach(function(taskGroup) {
		taskGroup.forEach(function(task) {
			if (task.assignee) {
				if (!userTaskDictionary[task.assignee.id]) {
					userTaskDictionary[task.assignee.id] = [];
				}
				userTaskDictionary[task.assignee.id].push(task);
			}
		});
	});
	return userTaskDictionary;
}

/**
 * Returns a dictionnary of tasks where keys are user_id
 *
 */
Bot.prototype.getUsersUncompletedTasks = function() {
	var userTaskDictionary = {};
	this.tasks.forEach(function(taskGroup) {
		taskGroup.forEach(function(task) {
			if (task.assignee && !task.done) {
				if (!userTaskDictionary[task.assignee.id]) {
					userTaskDictionary[task.assignee.id] = [];
				}
				userTaskDictionary[task.assignee.id].push(task);
			}
		});
	});
	return userTaskDictionary;
}

/**
 * Returns a list of tasks assigned to a user_id
 *
 */
Bot.prototype.getUserUncompletedTasks = function(user_id) {
	var userTasks = [];
	this.tasks.forEach(function(taskGroup) {
		taskGroup.forEach(function(task) {
			if (!task.done && task.assignee && task.assignee.id == user_id) {
				userTasks.push(task);
			}
		});
	});
	return userTasks;
}

/**
 * return a task with that ID associated to a user
 *
 * @param user_id id of the user.
 * @param task_id id of the task.
 */
Bot.prototype.getTaskForUser = function(task_id, user_id) {
	var task;
	for (var i in this.tasks) {
		for (var j in this.tasks[i]) {
			task = this.tasks[i][j];
			if (task.id == task_id && task.assignee && task.assignee.id == user_id) {
				return task;
			}
		}
	}
	return null;
}

module.exports = Bot;<|endoftext|>


<commit_before>import Prefab from './entityPrefab'

// client side class for Playable Characters
export default class Player extends Prefab {
  constructor(x, y, key) {
    // call it's superclass constructor
    super(x, y, key)
    // Send context as first argument!!
    this.anchor.set(0.25, 0.35)
    this.orientation = 4 // down
    this.speed = this.game.playerSpeed
    // TODO create dialogue logic.. maybe
    // this.dialoguesMemory = {}
    this.maxLife = this.game.playerLife
    this.life = this.maxLife
    this.inFight = false
    this.frames = {
        // the third value is the frame to come back to at the end of the animation
        // TODO reassign frame values based on the sprite.
      'attack_right': [0, 4, 9],
      'right': [5, 8],
      'idle_right': [9, 10],
      'attack_up': [11, 15, 20],
      'up': [16, 19],
      'idle_up': [20, 21],
      'attack_down': [22, 26, 31],
      'down': [27, 30],
      'idle_down': [31, 32],
      'attack_left': [33, 37, 42],
      'left': [38, 41],
      'idle_left': [42, 43]
    }
    // TODO reassign these children based on our assets
    // this.addChild(this.weapon = this.this.game.add.sprite(0, 0, 'atlas3'))
    // this.addChild(this.shadow = this.this.game.add.sprite(0, 5, 'atlas1', 'shadow'))
    this.events.onKilled.add(function(player) {
      this.game.displayedPlayers.delete(player.id)
    }, this)
    this.loadControls()
  }
  // changes color of name text if Player is the main Player
  setIsPlayer(flag) {
    this.isPlayer = flag
    if (this.isPlayer) this.nameHolder.addColor('#f4d442', 0)
  }
  // sets the player's name
  setName(name) {
    this.nameHolder.text = name
    this.nameHolder.x = Math.floor(16 - (this.nameHolder.width/2))
  }

  equipWeapon(key) {
    this.weapon.name = key
    this.weapon.frameName = key + '_0' // sets initial weapon animation frame
    this.weapon.absorbProperties(Game.itemsInfo[key]) // assigns stats to weapon
    this.atk = this.weapon.attack
    this.adjustWeapon() // will change weapons position on char sprite
    this.setAnimations(this.weapon)
    if (this.isPlayer) {
      Game.weaponIcon.frameName = this.weapon.icon+'_0' // weapons icon on screen?
      Client.setWeapon(key)
    }
    return true
  }

  adjustWeapon() {
    this.weapon.position.set(this.weapon.offsets.x, this.weapon.offsets.y)
  }

  equipArmor(key) {
    const armorInfo = Game.itemsInfo[key]
    this.def = armorInfo.def
    this.armorName = key
    return true
  }

  loadControls() {
    this.cursors = {}
    this.cursors.up = this.game.input.keyboard.addKey(Phaser.Keyboard.W)
    this.cursors.down = this.game.input.keyboard.addKey(Phaser.Keyboard.S)
    this.cursors.right = this.game.input.keyboard.addKey(Phaser.Keyboard.D)
    this.cursors.left = this.game.input.keyboard.addKey(Phaser.Keyboard.A)
    this.cursors.attack = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
    this.cursors.chat = this.game.input.keyboard.addKey(Phaser.Keyboard.TAB)
  }
  // TODO reimplement this fn when we manage life
  updateLife() {
    if (this.life < 0) this.life = 0
    const width = Game.computeLifeBarWidth()
    const tweenWidth = this.game.add.tween(Game.health.getChildAt(0)) // tween for the "body" of the bar
    const tweenEnd = this.game.add.tween(Game.health.getChildAt(1)) // tween for the curved tip
    tweenWidth.to({ width: width }, 200, null, false, 200)
    tweenEnd.to({ x: width }, 200, null, false, 200)
    tweenWidth.start()
    tweenEnd.start()
  }

  die(animate) {
    if (this.tween) this.stopMovement(false)
    this.target = null
    this.life = 0
    if (this.isPlayer) {
      // Game.moveTarget.visible = false
      this.updateLife()
      // setTimeout(Game.displayDeathScroll, Phaser.Timer.SECOND*2)
    }
    if (animate && this.inCamera) {
      this.frameName = 'death_0'
      this.animate('death', false)
      // Game.sounds.play('death')
    }
    this.delayedKill(750)
  }
  respawn() {
    this.revive() // method from the Phaser Sprite class
    this.orientation = this.game.rnd.between(1, 4)
    if (this.isPlayer) {
      this.life = this.maxLife
      this.updateLife()
    }
    this.idle(true)
  }
  update() {
    // so it doesn't float off into space...
    this.body.setZeroVelocity()
    if (this.cursors.up.isDown) {
      this.animations.play('up')
      this.body.moveUp(200)
    }
    if (this.cursors.down.isDown) {
      this.animations.play('down')
      this.body.moveDown(200)
    }
    if (this.cursors.left.isDown) {
      this.animations.play('left')
      this.body.moveLeft(200)
    }
    if (this.cursors.up.isDown) {
      this.animations.play('right')
      this.body.moveRight(200)
    }
    if (this.cursors.attack.isDown && this.orientation === 1) {
      this.animations.play('attack_up')
    }
    if (this.cursors.attack.isDown && this.orientation === 2) {
      this.animations.play('attack_left')
    }
    if (this.cursors.attack.isDown && this.orientation === 3) {
      this.animations.play('attack_right')
    }
    if (this.cursors.attack.isDown && this.orientation === 4) {
      this.animations.play('attack_down')
    }
  }
}
<commit_msg>ref: change args for constructor its super method for player.js<commit_after>import Prefab from './entityPrefab'

// client side class for Playable Characters
export default class Player extends Prefab {
  constructor(game, name, position, spriteKey) {
    // call it's superclass constructor
    super(game, name, position, spriteKey)
    // Send context as first argument!!
    this.anchor.set(0.25, 0.35)
    this.orientation = 4 // down
    this.speed = this.game.playerSpeed
    // TODO create dialogue logic.. maybe
    // this.dialoguesMemory = {}
    this.maxLife = this.game.playerLife
    this.life = this.maxLife
    this.inFight = false
    this.frames = {
        // the third value is the frame to come back to at the end of the animation
        // TODO reassign frame values based on the sprite.
      'attack_right': [0, 4, 9],
      'right': [5, 8],
      'idle_right': [9, 10],
      'attack_up': [11, 15, 20],
      'up': [16, 19],
      'idle_up': [20, 21],
      'attack_down': [22, 26, 31],
      'down': [27, 30],
      'idle_down': [31, 32],
      'attack_left': [33, 37, 42],
      'left': [38, 41],
      'idle_left': [42, 43]
    }
    // TODO reassign these children based on our assets
    // this.addChild(this.weapon = this.this.game.add.sprite(0, 0, 'atlas3'))
    // this.addChild(this.shadow = this.this.game.add.sprite(0, 5, 'atlas1', 'shadow'))
    this.events.onKilled.add(function(player) {
      this.game.displayedPlayers.delete(player.id)
    }, this)
    this.loadControls()
  }
  // changes color of name text if Player is the main Player
  setIsPlayer(flag) {
    this.isPlayer = flag
    if (this.isPlayer) this.nameHolder.addColor('#f4d442', 0)
  }
  // sets the player's name
  setName(name) {
    this.nameHolder.text = name
    this.nameHolder.x = Math.floor(16 - (this.nameHolder.width/2))
  }

  equipWeapon(key) {
    this.weapon.name = key
    this.weapon.frameName = key + '_0' // sets initial weapon animation frame
    this.weapon.absorbProperties(Game.itemsInfo[key]) // assigns stats to weapon
    this.atk = this.weapon.attack
    this.adjustWeapon() // will change weapons position on char sprite
    this.setAnimations(this.weapon)
    if (this.isPlayer) {
      Game.weaponIcon.frameName = this.weapon.icon+'_0' // weapons icon on screen?
      Client.setWeapon(key)
    }
    return true
  }

  adjustWeapon() {
    this.weapon.position.set(this.weapon.offsets.x, this.weapon.offsets.y)
  }

  equipArmor(key) {
    const armorInfo = Game.itemsInfo[key]
    this.def = armorInfo.def
    this.armorName = key
    return true
  }

  loadControls() {
    this.cursors = {}
    this.cursors.up = this.game.input.keyboard.addKey(Phaser.Keyboard.W)
    this.cursors.down = this.game.input.keyboard.addKey(Phaser.Keyboard.S)
    this.cursors.right = this.game.input.keyboard.addKey(Phaser.Keyboard.D)
    this.cursors.left = this.game.input.keyboard.addKey(Phaser.Keyboard.A)
    this.cursors.attack = this.game.input.keyboard.addKey(Phaser.Keyboard.SPACEBAR)
    this.cursors.chat = this.game.input.keyboard.addKey(Phaser.Keyboard.TAB)
  }
  // TODO reimplement this fn when we manage life
  updateLife() {
    if (this.life < 0) this.life = 0
    const width = Game.computeLifeBarWidth()
    const tweenWidth = this.game.add.tween(Game.health.getChildAt(0)) // tween for the "body" of the bar
    const tweenEnd = this.game.add.tween(Game.health.getChildAt(1)) // tween for the curved tip
    tweenWidth.to({ width: width }, 200, null, false, 200)
    tweenEnd.to({ x: width }, 200, null, false, 200)
    tweenWidth.start()
    tweenEnd.start()
  }

  die(animate) {
    if (this.tween) this.stopMovement(false)
    this.target = null
    this.life = 0
    if (this.isPlayer) {
      // Game.moveTarget.visible = false
      this.updateLife()
      // setTimeout(Game.displayDeathScroll, Phaser.Timer.SECOND*2)
    }
    if (animate && this.inCamera) {
      this.frameName = 'death_0'
      this.animate('death', false)
      // Game.sounds.play('death')
    }
    this.delayedKill(750)
  }
  respawn() {
    this.revive() // method from the Phaser Sprite class
    this.orientation = this.game.rnd.between(1, 4)
    if (this.isPlayer) {
      this.life = this.maxLife
      this.updateLife()
    }
    this.idle(true)
  }
  update() {
    // so it doesn't float off into space...
    this.body.setZeroVelocity()
    if (this.cursors.up.isDown) {
      this.animations.play('up')
      this.body.moveUp(200)
    }
    if (this.cursors.down.isDown) {
      this.animations.play('down')
      this.body.moveDown(200)
    }
    if (this.cursors.left.isDown) {
      this.animations.play('left')
      this.body.moveLeft(200)
    }
    if (this.cursors.up.isDown) {
      this.animations.play('right')
      this.body.moveRight(200)
    }
    if (this.cursors.attack.isDown && this.orientation === 1) {
      this.animations.play('attack_up')
    }
    if (this.cursors.attack.isDown && this.orientation === 2) {
      this.animations.play('attack_left')
    }
    if (this.cursors.attack.isDown && this.orientation === 3) {
      this.animations.play('attack_right')
    }
    if (this.cursors.attack.isDown && this.orientation === 4) {
      this.animations.play('attack_down')
    }
  }
}
<|endoftext|>


<commit_before>var request = require('request')
var nude = require('nudity')
module.exports = {
  events: {
    'url:i.imgur.com': function (bot, url, nick, to) {
      var path = url.path
      /*
       * The script hangs on large images, so rely on Imgur to play nice.
       * Appending "l" to a filename forces it to be a large thumbnail.
       */
      path = url.path.split('.')
      if (path[1] === 'jpg' || path[1] === 'gif') {
        path = path[0] + 'l.' + path[1]
      } else {
        return
      }
      request({
        url: url.protocol + '//' + url.host + path,
        headers: {
          accept: 'image/jpeg'
        },
        encoding: null // we want binary
      }, function (err, res, body) {
        if (err) {
          throw err
        }
        nude.scanData(body, function (err, red) {
          if (err) {
            return
          }
          if (red) {
            bot.shout(to, nick + ': lewd')
          }
        })
      })
    }
  }
}
<commit_msg>Remove nudity module<commit_after><|endoftext|>


<commit_before>import React, { Component } from 'react';
import {
    Button,
} from 'react-bootstrap'
import './App.css';

import Header from './Header';
import Documentation from './Documentation';
import Shapes from './Shapes';
import Transforms from './Transforms';
import TurtleCanvas from './TurtleCanvas';
import GCodeGenerator from './GCode';

// I'm trying to define a simple struct that we can use everywhere we need vertices. I don't see a
// problem letting this bloat a little.
//
// Currently, I'm using this as input to the GCodeGenerator for the locations of gcode.
//
function vertex (x, y, speed=0) {
  return {
    x: x,
    y: y,
    f: speed
  }
}

class App extends Component {

  constructor(props) {
    super(props)
    this.state = {
      vertices: [
        vertex(0.0, 0.0),
        vertex(1.0, 1.0),
      ],
    };
  }

  render() {

    return (

          <Documentation/>

          <Shapes />
          <Transforms />

            <TurtleCanvas width={300} height={300} rotation={45}/>

            <GCodeGenerator vertices={this.state.vertices}/>

    );
  }
}

export default App;
<commit_msg>Remove last React-Bootstrap item from App.<commit_after>import React, { Component } from 'react';
import './App.css';

import Header from './Header';
import Documentation from './Documentation';
import Shapes from './Shapes';
import Transforms from './Transforms';
import TurtleCanvas from './TurtleCanvas';
import GCodeGenerator from './GCode';

// I'm trying to define a simple struct that we can use everywhere we need vertices. I don't see a
// problem letting this bloat a little.
//
// Currently, I'm using this as input to the GCodeGenerator for the locations of gcode.
//
function vertex (x, y, speed=0) {
  return {
    x: x,
    y: y,
    f: speed
  }
}

class App extends Component {

  constructor(props) {
    super(props)
    this.state = {
      vertices: [
        vertex(0.0, 0.0),
        vertex(1.0, 1.0),
      ],
    };
  }

  render() {

    return (

          <Documentation/>

          <Shapes />
          <Transforms />

            <TurtleCanvas width={300} height={300} rotation={45}/>

            <GCodeGenerator vertices={this.state.vertices}/>

    );
  }
}

export default App;
<|endoftext|>


<commit_before>/* global ImageData, URL */
var registerComponent = require('../../core/component').registerComponent;
var THREE = require('../../lib/three');

var VERTEX_SHADER = [
  'attribute vec3 position;',
  'attribute vec2 uv;',
  'uniform mat4 projectionMatrix;',
  'uniform mat4 modelViewMatrix;',
  'varying vec2 vUv;',
  'void main()  {',
  '  vUv = vec2( 1.- uv.x, uv.y );',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );'
].join('\n');

var FRAGMENT_SHADER = [
  'precision mediump float',
  'uniform samplerCube map',
  'varying vec2 vUv',
  '#define M_PI 3.141592653589793238462643383279',
  'void main()  ',
  '  vec2 uv = vUv',
  '  float longitude = uv.x * 2. * M_PI - M_PI + M_PI / 2.',
  '  float latitude = uv.y * M_PI',
  '  vec3 dir = vec3',
  '    - sin( longitude ) * sin( latitude )',
  '    cos( latitude )',
  '    - cos( longitude ) * sin( latitude ',
  '  )',
  '  normalize( dir )',
  '  gl_FragColor = vec4( textureCube( map, dir ).rgb, 1.0 )'
].join('\n');

/**
 * Component to take screenshots of the scene using a keboard shortcut (alt+s).
 * It can be configured to either take 360° captures (`equirectangular`)
 * or regular screenshots (`projection`)
 *
 * This is based on https://github.com/spite/THREE.CubemapToEquirectangular
 * To capture an equirectangular projection of the scene a THREE.CubeCamera is used
 * The cube map produced by the CubeCamera is projected on a quad and then rendered to
 * WebGLRenderTarget with an ortographic camera.
 */
module.exports.Component = registerComponent('screenshot', {
  schema: {
    width: {default: 4096},
    height: {default: 2048},
    projection: {default: 'equirectangular', oneOf: ['equirectangular', 'perspective']}
  },

  init: function () {
    var el = this.el;
    var self = this;
    if (el.renderer) {
      setup();
    } else {
      el.addEventListener('render-target-loaded', setup);
    }
    function setup () {
      var gl = el.renderer.getContext();
      self.cubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
      self.material = new THREE.RawShaderMaterial({
        uniforms: {map: {type: 't', value: null}},
        vertexShader: VERTEX_SHADER,
        fragmentShader: FRAGMENT_SHADER,
        side: THREE.DoubleSide
      });
      self.quad = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(1, 1),
        self.material
      );
      self.quad.visible = false;
      self.camera = new THREE.OrthographicCamera(1 / -2, 1 / 2, 1 / 2, 1 / -2, -10000, 10000);
      self.canvas = document.createElement('canvas');
      self.ctx = self.canvas.getContext('2d');
      if (el.camera) { el.camera.add(self.quad); }
      self.onKeyDown = self.onKeyDown.bind(self);
      self.onCameraActive = self.onCameraActive.bind(self);
      el.addEventListener('camera-set-active', self.onCameraActive);
    }
  },

  getRenderTarget: function (width, height) {
    return new THREE.WebGLRenderTarget(width, height, {
      minFilter: THREE.LinearFilter,
      magFilter: THREE.LinearFilter,
      wrapS: THREE.ClampToEdgeWrapping,
      wrapT: THREE.ClampToEdgeWrapping,
      format: THREE.RGBAFormat,
      type: THREE.UnsignedByteType
    });
  },

  resize: function (width, height) {
    // Resize quad
    this.quad.scale.set(width, height, 1);

    // Resize Camera
    this.camera.left = width / -2;
    this.camera.right = width / 2;
    this.camera.top = height / 2;
    this.camera.bottom = height / -2;
    this.camera.updateProjectionMatrix();

    // Resize canvas
    this.canvas.width = width;
    this.canvas.height = height;
  },

  play: function () {
    window.addEventListener('keydown', this.onKeyDown);
  },

  onCameraActive: function (evt) {
    var cameraParent = this.quad.parent;
    if (cameraParent) { cameraParent.remove(this.quad); }
    evt.detail.cameraEl.getObject3D('camera').add(this.quad);
  },

  /**
   * <ctrl> + <alt> + s keyboard shortcut.
   */
  onKeyDown: function (evt) {
    var shortcutPressed = evt.keyCode === 83 && evt.ctrlKey && evt.altKey;
    if (!this.data || !shortcutPressed) { return; }
    this.capture();
  },

  capture: function () {
    var el = this.el;
    var renderer = el.renderer;
    var size;
    var camera;
    var cubeCamera;
    // Configure camera
    if (this.data.projection === 'perspective') {
      // the quad is used for projection in the equirectangular mode
      // we hide it in this case.
      this.quad.visible = false;
      // use scene camera
      camera = el.camera;
      size = renderer.getSize();
    } else {
      // use ortho camera
      camera = this.camera;
      // copy position and rotation of scene camera into the ortho one
      camera.position.copy(el.camera.getWorldPosition());
      camera.rotation.copy(el.camera.getWorldRotation());
      // create cube camera and copy position from scene camera
      cubeCamera = new THREE.CubeCamera(el.camera.near, el.camera.far, Math.min(this.cubeMapSize, 2048));
      cubeCamera.position.copy(el.camera.getWorldPosition());
      cubeCamera.rotation.copy(el.camera.getWorldRotation());
      // render scene with cube camera
      cubeCamera.updateCubeMap(el.renderer, el.object3D);
      this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
      size = {width: this.data.width, height: this.data.height};
      // use quad to project image taken by the cube camera
      this.quad.visible = true;
    }
    this.renderCapture(camera, size);
    // Trigger file download
    this.saveCapture();
  },

  renderCapture: function (camera, size) {
    var autoClear = this.el.renderer.autoClear;
    var el = this.el;
    var imageData;
    var output;
    var pixels;
    var renderer = this.el.renderer;
    // Create rendering target and buffer to store the read pixels
    output = this.getRenderTarget(size.width, size.height);
    pixels = new Uint8Array(4 * size.width * size.height);
    // Resize quad, camera and canvas
    this.resize(size.width, size.height);
    // Render scene to render target
    renderer.autoClear = true;
    renderer.render(el.object3D, camera, output, true);
    renderer.autoClear = autoClear;
    // Read image pizels back
    renderer.readRenderTargetPixels(output, 0, 0, size.width, size.height, pixels);
    if (this.data.projection === 'perspective') {
      pixels = this.flipPixelsVertically(pixels, size.width, size.height);
    }
    imageData = new ImageData(new Uint8ClampedArray(pixels), size.width, size.height);
    // Hide quad after projecting the image
    this.quad.visible = false;
    // Copy pixels into canvas
    this.ctx.putImageData(imageData, 0, 0);
  },

  flipPixelsVertically: function (pixels, width, height) {
    var flippedPixels = pixels.slice(0);
    for (var x = 0; x < width; ++x) {
      for (var y = 0; y < height; ++y) {
        flippedPixels[x * 4 + y * width * 4] = pixels[x * 4 + (height - y) * width * 4];
        flippedPixels[x * 4 + 1 + y * width * 4] = pixels[x * 4 + 1 + (height - y) * width * 4];
        flippedPixels[x * 4 + 2 + y * width * 4] = pixels[x * 4 + 2 + (height - y) * width * 4];
        flippedPixels[x * 4 + 3 + y * width * 4] = pixels[x * 4 + 3 + (height - y) * width * 4];
      }
    }
    return flippedPixels;
  },

  saveCapture: function () {
    this.canvas.toBlob(function (blob) {
      var url = URL.createObjectURL(blob);
      var fileName = 'screenshot-' + document.title + '-' + Date.now() + '.png';
      var aEl = document.createElement('a');
      aEl.href = url;
      aEl.setAttribute('download', fileName);
      aEl.innerHTML = 'downloading...';
      aEl.style.display = 'none';
      document.body.appendChild(aEl);
      setTimeout(function () {
        aEl.click();
        document.body.removeChild(aEl);
      }, 1);
    }, 'image/png');
  }
});
<commit_msg>Fix screenshot shader syntax errors<commit_after>/* global ImageData, URL */
var registerComponent = require('../../core/component').registerComponent;
var THREE = require('../../lib/three');

var VERTEX_SHADER = [
  'attribute vec3 position;',
  'attribute vec2 uv;',
  'uniform mat4 projectionMatrix;',
  'uniform mat4 modelViewMatrix;',
  'varying vec2 vUv;',
  'void main()  {',
  '  vUv = vec2( 1.- uv.x, uv.y );',
  '  gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
  '}'
].join('\n');

var FRAGMENT_SHADER = [
  'precision mediump float;',
  'uniform samplerCube map;',
  'varying vec2 vUv;',
  '#define M_PI 3.141592653589793238462643383279',
  'void main() {',
  '  vec2 uv = vUv;',
  '  float longitude = uv.x * 2. * M_PI - M_PI + M_PI / 2.;',
  '  float latitude = uv.y * M_PI;',
  '  vec3 dir = vec3(',
  '    - sin( longitude ) * sin( latitude ),',
  '    cos( latitude ),',
  '    - cos( longitude ) * sin( latitude )',
  '  );',
  '  normalize( dir );',
  '  gl_FragColor = vec4( textureCube( map, dir ).rgb, 1.0 );',
  '}'
].join('\n');

/**
 * Component to take screenshots of the scene using a keboard shortcut (alt+s).
 * It can be configured to either take 360° captures (`equirectangular`)
 * or regular screenshots (`projection`)
 *
 * This is based on https://github.com/spite/THREE.CubemapToEquirectangular
 * To capture an equirectangular projection of the scene a THREE.CubeCamera is used
 * The cube map produced by the CubeCamera is projected on a quad and then rendered to
 * WebGLRenderTarget with an ortographic camera.
 */
module.exports.Component = registerComponent('screenshot', {
  schema: {
    width: {default: 4096},
    height: {default: 2048},
    projection: {default: 'equirectangular', oneOf: ['equirectangular', 'perspective']}
  },

  init: function () {
    var el = this.el;
    var self = this;
    if (el.renderer) {
      setup();
    } else {
      el.addEventListener('render-target-loaded', setup);
    }
    function setup () {
      var gl = el.renderer.getContext();
      self.cubeMapSize = gl.getParameter(gl.MAX_CUBE_MAP_TEXTURE_SIZE);
      self.material = new THREE.RawShaderMaterial({
        uniforms: {map: {type: 't', value: null}},
        vertexShader: VERTEX_SHADER,
        fragmentShader: FRAGMENT_SHADER,
        side: THREE.DoubleSide
      });
      self.quad = new THREE.Mesh(
        new THREE.PlaneBufferGeometry(1, 1),
        self.material
      );
      self.quad.visible = false;
      self.camera = new THREE.OrthographicCamera(1 / -2, 1 / 2, 1 / 2, 1 / -2, -10000, 10000);
      self.canvas = document.createElement('canvas');
      self.ctx = self.canvas.getContext('2d');
      if (el.camera) { el.camera.add(self.quad); }
      self.onKeyDown = self.onKeyDown.bind(self);
      self.onCameraActive = self.onCameraActive.bind(self);
      el.addEventListener('camera-set-active', self.onCameraActive);
    }
  },

  getRenderTarget: function (width, height) {
    return new THREE.WebGLRenderTarget(width, height, {
      minFilter: THREE.LinearFilter,
      magFilter: THREE.LinearFilter,
      wrapS: THREE.ClampToEdgeWrapping,
      wrapT: THREE.ClampToEdgeWrapping,
      format: THREE.RGBAFormat,
      type: THREE.UnsignedByteType
    });
  },

  resize: function (width, height) {
    // Resize quad
    this.quad.scale.set(width, height, 1);

    // Resize Camera
    this.camera.left = width / -2;
    this.camera.right = width / 2;
    this.camera.top = height / 2;
    this.camera.bottom = height / -2;
    this.camera.updateProjectionMatrix();

    // Resize canvas
    this.canvas.width = width;
    this.canvas.height = height;
  },

  play: function () {
    window.addEventListener('keydown', this.onKeyDown);
  },

  onCameraActive: function (evt) {
    var cameraParent = this.quad.parent;
    if (cameraParent) { cameraParent.remove(this.quad); }
    evt.detail.cameraEl.getObject3D('camera').add(this.quad);
  },

  /**
   * <ctrl> + <alt> + s keyboard shortcut.
   */
  onKeyDown: function (evt) {
    var shortcutPressed = evt.keyCode === 83 && evt.ctrlKey && evt.altKey;
    if (!this.data || !shortcutPressed) { return; }
    this.capture();
  },

  capture: function () {
    var el = this.el;
    var renderer = el.renderer;
    var size;
    var camera;
    var cubeCamera;
    // Configure camera
    if (this.data.projection === 'perspective') {
      // the quad is used for projection in the equirectangular mode
      // we hide it in this case.
      this.quad.visible = false;
      // use scene camera
      camera = el.camera;
      size = renderer.getSize();
    } else {
      // use ortho camera
      camera = this.camera;
      // copy position and rotation of scene camera into the ortho one
      camera.position.copy(el.camera.getWorldPosition());
      camera.rotation.copy(el.camera.getWorldRotation());
      // create cube camera and copy position from scene camera
      cubeCamera = new THREE.CubeCamera(el.camera.near, el.camera.far, Math.min(this.cubeMapSize, 2048));
      cubeCamera.position.copy(el.camera.getWorldPosition());
      cubeCamera.rotation.copy(el.camera.getWorldRotation());
      // render scene with cube camera
      cubeCamera.updateCubeMap(el.renderer, el.object3D);
      this.quad.material.uniforms.map.value = cubeCamera.renderTarget.texture;
      size = {width: this.data.width, height: this.data.height};
      // use quad to project image taken by the cube camera
      this.quad.visible = true;
    }
    this.renderCapture(camera, size);
    // Trigger file download
    this.saveCapture();
  },

  renderCapture: function (camera, size) {
    var autoClear = this.el.renderer.autoClear;
    var el = this.el;
    var imageData;
    var output;
    var pixels;
    var renderer = this.el.renderer;
    // Create rendering target and buffer to store the read pixels
    output = this.getRenderTarget(size.width, size.height);
    pixels = new Uint8Array(4 * size.width * size.height);
    // Resize quad, camera and canvas
    this.resize(size.width, size.height);
    // Render scene to render target
    renderer.autoClear = true;
    renderer.render(el.object3D, camera, output, true);
    renderer.autoClear = autoClear;
    // Read image pizels back
    renderer.readRenderTargetPixels(output, 0, 0, size.width, size.height, pixels);
    if (this.data.projection === 'perspective') {
      pixels = this.flipPixelsVertically(pixels, size.width, size.height);
    }
    imageData = new ImageData(new Uint8ClampedArray(pixels), size.width, size.height);
    // Hide quad after projecting the image
    this.quad.visible = false;
    // Copy pixels into canvas
    this.ctx.putImageData(imageData, 0, 0);
  },

  flipPixelsVertically: function (pixels, width, height) {
    var flippedPixels = pixels.slice(0);
    for (var x = 0; x < width; ++x) {
      for (var y = 0; y < height; ++y) {
        flippedPixels[x * 4 + y * width * 4] = pixels[x * 4 + (height - y) * width * 4];
        flippedPixels[x * 4 + 1 + y * width * 4] = pixels[x * 4 + 1 + (height - y) * width * 4];
        flippedPixels[x * 4 + 2 + y * width * 4] = pixels[x * 4 + 2 + (height - y) * width * 4];
        flippedPixels[x * 4 + 3 + y * width * 4] = pixels[x * 4 + 3 + (height - y) * width * 4];
      }
    }
    return flippedPixels;
  },

  saveCapture: function () {
    this.canvas.toBlob(function (blob) {
      var url = URL.createObjectURL(blob);
      var fileName = 'screenshot-' + document.title + '-' + Date.now() + '.png';
      var aEl = document.createElement('a');
      aEl.href = url;
      aEl.setAttribute('download', fileName);
      aEl.innerHTML = 'downloading...';
      aEl.style.display = 'none';
      document.body.appendChild(aEl);
      setTimeout(function () {
        aEl.click();
        document.body.removeChild(aEl);
      }, 1);
    }, 'image/png');
  }
});
<|endoftext|>


<commit_before>//
// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//     You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//     See the License for the specific language governing permissions and
// limitations under the License.

(function(shared, scope) {

function ComputedStylePropertyMap(element) {
  this._element = element;
}

ComputedStylePropertyMap.prototype = Object.create(shared.StylePropertyMap.prototype);

ComputedStylePropertyMap.prototype.append = function(property, value) {
  throw new TypeError('ComputedStylePropertyMap is immutable');
};

ComputedStylePropertyMap.prototype.get = function(property) {
  if (typeof property != 'string') {
    throw new TypeError('parameter 1 is not of type \'string\'');
  }
  // TODO: define types
  throw new TypeError('Not implemented yet');
};

function getComputedStyleMap(element) {
  return new ComputedStylePropertyMap(element);
}

scope.getComputedStyleMap = getComputedStyleMap;
scope.ComputedStylePropertyMap = ComputedStylePropertyMap;

})(baseClasses, window)
<commit_msg>Fill in implementation of ComputedStyleMap.get<commit_after>//
// Copyright 2015 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//     You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//     See the License for the specific language governing permissions and
// limitations under the License.

(function(shared, scope) {

function ComputedStylePropertyMap(element) {
  this._element = element;
}

ComputedStylePropertyMap.prototype = Object.create(shared.StylePropertyMap.prototype);

ComputedStylePropertyMap.prototype.append = function(property, value) {
  throw new TypeError('ComputedStylePropertyMap is immutable');
};

ComputedStylePropertyMap.prototype.get = function(property) {
  if (typeof property != 'string') {
    throw new TypeError('parameter 1 is not of type \'string\'');
  }

  var value = window.getComputedStyle(this._element)[property];
  if (!value) {
    return null;
  }
  if (value == 'inherit') {
    // TODO: Other keywords
    throw new TypeError('Not implemented yet');
  }

  // TODO: The rest of the properties once the rest of the StyleValues are defined.
  switch (property) {
    // These properties always take numbers or a keyword handled above.
    case "z-index":
    case "opacity":
    case "pitch-range":
    case "richness":
    case "stress":
      return new scope.NumberValue(value);

    case "line-height":
      // normal | <number> | <length> | <percentage> | inherit
      throw new TypeError('Not implemented yet');

    case "speech-rate":
      // <number> | x-slow | slow | medium | fast | x-fast | faster | slower | inherit
      throw new TypeError('Not implemented yet');

    case "volume":
      // <number> | <percentage> | silent | x-soft | soft | medium | loud | x-loud | inherit
      throw new TypeError('Not implemented yet');

    default:
      throw new TypeError('Not implemented yet');
  }
};

function getComputedStyleMap(element) {
  return new ComputedStylePropertyMap(element);
}

scope.getComputedStyleMap = getComputedStyleMap;
scope.ComputedStylePropertyMap = ComputedStylePropertyMap;

})(baseClasses, window)
<|endoftext|>


<commit_before>const promiseAllWithHoles = promises => new Promise((resolve, reject) => {
  const length = promises.length >>> 0;
  const result = Array(length);
  let [pending, i] = Array(2).fill(length);
  if (!length) return resolve(result);
  const settlePromise = i => Promise.resolve(promises[i]).then(value => {
    if (i in promises) result[i] = value;
    if (!--pending) resolve(result);
  }, reject);
  while (i--) settlePromise(i);
});

export default promiseAllWithHoles;
<commit_msg>refactor: work around bug in minification<commit_after>const promiseAllWithHoles = promises => new Promise((resolve, reject) => {
  const length = promises.length >>> 0;
  const result = Array(length);
  let pending = length;
  let i = length;
  if (!length) return resolve(result);
  const settlePromise = i => Promise.resolve(promises[i]).then(value => {
    if (i in promises) result[i] = value;
    if (!--pending) resolve(result);
  }, reject);
  while (i--) settlePromise(i);
});

export default promiseAllWithHoles;
<|endoftext|>


<commit_before>"use strict";

var net = require('net');

exports.module = function() {
	this.onModuleStart = function(settings) {

		if(!this.settings.allowedip)
			this.settings.allowedip = "127.0.0.1";

		var self = this;
		this.server = net.createServer(function(co) {
			if(self.settings.allowedip != "any" && co.remoteAddress != self.settings.allowedip)
			{
				co.end();
				console.log("[Warning] Killing unauthorized connection for post report plugin. Port "+self.settings.port+", IP "+co.remoteAddress);
				return;
			}

			co.on("data", function(data) {
				self.channel.say(data);
			});

		});

		if(this.settings.allowedip == "127.0.0.1")
			this.server.listen(this.settings.port, "127.0.0.1");
		else
			this.server.listen(this.settings.port);
	};

	this.onModuleDestroy = function() {
		this.server.close();
	};
};
<commit_msg>That's better.<commit_after>"use strict";

var net = require('net');

exports.module = function() {
	this.onModuleStart = function(settings) {

		if(!this.settings.allowedip)
			this.settings.allowedip = "127.0.0.1";

		var self = this;
		this.server = net.createServer(function(co) {
			if(self.settings.allowedip != "any" && co.remoteAddress != self.settings.allowedip)
			{
				co.end();
				console.log("[Warning] Killing report server's unauthorized connection from IP "+co.remoteAddress);
				return;
			}

			co.on("data", function(data) {
				self.channel.say(data);
			});
		});

		if(this.settings.allowedip == "127.0.0.1")
			this.server.listen(this.settings.port, "127.0.0.1");
		else
			this.server.listen(this.settings.port);
	};

	this.onModuleDestroy = function() {
		this.server.close();
	};
};
<|endoftext|>


<commit_before>// @flow

import type { AppState } from '../redux/redux-setup';
import type { LoadingStatus } from 'lib/types/loading-types';
import { loadingStatusPropType } from 'lib/types/loading-types';
import {
  type ThreadInfo,
  threadInfoPropType,
  type ThreadType,
  threadTypes,
  threadTypePropType,
  type NewThreadRequest,
  type NewThreadResult,
} from 'lib/types/thread-types';
import {
  type AccountUserInfo,
  accountUserInfoPropType,
  type UserInfo,
  userInfoPropType,
} from 'lib/types/user-types';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import type { UserSearchResult } from 'lib/types/search-types';
import type { ChatNavigationProp } from './chat.react';
import type { NavigationRoute } from '../navigation/route-names';

import * as React from 'react';
import PropTypes from 'prop-types';
import { View, Text, Alert } from 'react-native';
import invariant from 'invariant';
import _flow from 'lodash/fp/flow';
import _filter from 'lodash/fp/filter';
import _sortBy from 'lodash/fp/sortBy';
import { createSelector } from 'reselect';

import { connect } from 'lib/utils/redux-utils';
import { newThreadActionTypes, newThread } from 'lib/actions/thread-actions';
import { searchUsersActionTypes, searchUsers } from 'lib/actions/user-actions';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import {
  userInfoSelectorForOtherMembersOfThread,
  userSearchIndexForOtherMembersOfThread,
} from 'lib/selectors/user-selectors';
import SearchIndex from 'lib/shared/search-index';
import {
  threadInChatList,
  userIsMember,
  threadInfoFromRawThreadInfo,
} from 'lib/shared/thread-utils';
import { getUserSearchResults } from 'lib/shared/search-utils';
import { registerFetchKey } from 'lib/reducers/loading-reducer';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';

import TagInput from '../components/tag-input.react';
import UserList from '../components/user-list.react';
import ThreadList from '../components/thread-list.react';
import LinkButton from '../components/link-button.react';
import { MessageListRouteName } from '../navigation/route-names';
import ThreadVisibility from '../components/thread-visibility.react';
import {
  type Colors,
  colorsPropType,
  colorsSelector,
  styleSelector,
} from '../themes/colors';

const tagInputProps = {
  placeholder: 'username',
  autoFocus: true,
  returnKeyType: 'go',
};

export type ComposeThreadParams = {|
  threadType?: ThreadType,
  parentThreadInfo?: ThreadInfo,
|};

type Props = {|
  navigation: ChatNavigationProp<'ComposeThread'>,
  route: NavigationRoute<'ComposeThread'>,
  // Redux state
  parentThreadInfo: ?ThreadInfo,
  loadingStatus: LoadingStatus,
  otherUserInfos: { [id: string]: AccountUserInfo },
  userSearchIndex: SearchIndex,
  threadInfos: { [id: string]: ThreadInfo },
  colors: Colors,
  styles: typeof styles,
  viewerID: ?string,
  userInfos: { [id: string]: UserInfo },
  // Redux dispatch functions
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  newThread: (request: NewThreadRequest) => Promise<NewThreadResult>,
  searchUsers: (usernamePrefix: string) => Promise<UserSearchResult>,
|};
type State = {|
  usernameInputText: string,
  userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
|};
type PropsAndState = {| ...Props, ...State |};
class ComposeThread extends React.PureComponent<Props, State> {
  static propTypes = {
    navigation: PropTypes.shape({
      setParams: PropTypes.func.isRequired,
      setOptions: PropTypes.func.isRequired,
      navigate: PropTypes.func.isRequired,
      pushNewThread: PropTypes.func.isRequired,
    }).isRequired,
    route: PropTypes.shape({
      key: PropTypes.string.isRequired,
      params: PropTypes.shape({
        threadType: threadTypePropType,
        parentThreadInfo: threadInfoPropType,
      }).isRequired,
    }).isRequired,
    parentThreadInfo: threadInfoPropType,
    loadingStatus: loadingStatusPropType.isRequired,
    otherUserInfos: PropTypes.objectOf(accountUserInfoPropType).isRequired,
    userSearchIndex: PropTypes.instanceOf(SearchIndex).isRequired,
    threadInfos: PropTypes.objectOf(threadInfoPropType).isRequired,
    colors: colorsPropType.isRequired,
    styles: PropTypes.objectOf(PropTypes.object).isRequired,
    viewerID: PropTypes.string,
    userInfos: PropTypes.objectOf(userInfoPropType).isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    newThread: PropTypes.func.isRequired,
    searchUsers: PropTypes.func.isRequired,
  };
  state = {
    usernameInputText: '',
    userInfoInputArray: [],
  };
  tagInput: ?TagInput<AccountUserInfo>;
  createThreadPressed = false;

  constructor(props: Props) {
    super(props);
    this.setLinkButton(true);
  }

  setLinkButton(enabled: boolean) {
    this.props.navigation.setOptions({
      headerRight: () => (
        <LinkButton
          text="Create"
          onPress={this.onPressCreateThread}
          disabled={!enabled}
        />
      ),
    });
  }

  componentDidMount() {
    this.searchUsers('');
  }

  componentDidUpdate(prevProps: Props) {
    const oldReduxParentThreadInfo = prevProps.parentThreadInfo;
    const newReduxParentThreadInfo = this.props.parentThreadInfo;
    if (
      newReduxParentThreadInfo &&
      newReduxParentThreadInfo !== oldReduxParentThreadInfo
    ) {
      this.props.navigation.setParams({
        parentThreadInfo: newReduxParentThreadInfo,
      });
    }
  }

  static getParentThreadInfo(props: {
    route: NavigationRoute<'ComposeThread'>,
  }): ?ThreadInfo {
    return props.route.params.parentThreadInfo;
  }

  userSearchResultsSelector = createSelector(
    (propsAndState: PropsAndState) => propsAndState.usernameInputText,
    (propsAndState: PropsAndState) => propsAndState.otherUserInfos,
    (propsAndState: PropsAndState) => propsAndState.userSearchIndex,
    (propsAndState: PropsAndState) => propsAndState.userInfoInputArray,
    (propsAndState: PropsAndState) =>
      ComposeThread.getParentThreadInfo(propsAndState),
    (
      text: string,
      userInfos: { [id: string]: AccountUserInfo },
      searchIndex: SearchIndex,
      userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
      parentThreadInfo: ?ThreadInfo,
    ) =>
      getUserSearchResults(
        text,
        userInfos,
        searchIndex,
        userInfoInputArray.map(userInfo => userInfo.id),
        parentThreadInfo,
      ),
  );

  get userSearchResults() {
    return this.userSearchResultsSelector({ ...this.props, ...this.state });
  }

  existingThreadsSelector = createSelector(
    (propsAndState: PropsAndState) =>
      ComposeThread.getParentThreadInfo(propsAndState),
    (propsAndState: PropsAndState) => propsAndState.threadInfos,
    (propsAndState: PropsAndState) => propsAndState.userInfoInputArray,
    (
      parentThreadInfo: ?ThreadInfo,
      threadInfos: { [id: string]: ThreadInfo },
      userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
    ) => {
      const userIDs = userInfoInputArray.map(userInfo => userInfo.id);
      if (userIDs.length === 0) {
        return [];
      }
      return _flow(
        _filter(
          (threadInfo: ThreadInfo) =>
            threadInChatList(threadInfo) &&
            (!parentThreadInfo ||
              threadInfo.parentThreadID === parentThreadInfo.id) &&
            userIDs.every(userID => userIsMember(threadInfo, userID)),
        ),
        _sortBy(
          ([
            'members.length',
            (threadInfo: ThreadInfo) => (threadInfo.name ? 1 : 0),
          ]: $ReadOnlyArray<string | ((threadInfo: ThreadInfo) => mixed)>),
        ),
      )(threadInfos);
    },
  );

  get existingThreads() {
    return this.existingThreadsSelector({ ...this.props, ...this.state });
  }

  render() {
    let existingThreadsSection = null;
    const { existingThreads, userSearchResults } = this;
    if (existingThreads.length > 0) {
      existingThreadsSection = (
        <View style={this.props.styles.existingThreads}>
          <View style={this.props.styles.existingThreadsRow}>
            <Text style={this.props.styles.existingThreadsLabel}>
              Existing threads
            </Text>
          </View>
          <View style={this.props.styles.existingThreadList}>
            <ThreadList
              threadInfos={existingThreads}
              onSelect={this.onSelectExistingThread}
              itemTextStyle={this.props.styles.listItem}
            />
          </View>
        </View>
      );
    }
    let parentThreadRow = null;
    const parentThreadInfo = ComposeThread.getParentThreadInfo(this.props);
    if (parentThreadInfo) {
      const threadType = this.props.route.params.threadType;
      invariant(
        threadType !== undefined && threadType !== null,
        `no threadType provided for ${parentThreadInfo.id}`,
      );
      const threadVisibilityColor = this.props.colors.modalForegroundLabel;
      parentThreadRow = (
        <View style={this.props.styles.parentThreadRow}>
          <ThreadVisibility
            threadType={threadType}
            color={threadVisibilityColor}
          />
          <Text style={this.props.styles.parentThreadLabel}>within</Text>
          <Text style={this.props.styles.parentThreadName}>
            {parentThreadInfo.uiName}
          </Text>
        </View>
      );
    }
    const inputProps = {
      ...tagInputProps,
      onSubmitEditing: this.onPressCreateThread,
    };
    return (
      <View style={this.props.styles.container}>
        {parentThreadRow}
        <View style={this.props.styles.userSelectionRow}>
          <Text style={this.props.styles.tagInputLabel}>To: </Text>
          <View style={this.props.styles.tagInputContainer}>
            <TagInput
              value={this.state.userInfoInputArray}
              onChange={this.onChangeTagInput}
              text={this.state.usernameInputText}
              onChangeText={this.setUsernameInputText}
              labelExtractor={this.tagDataLabelExtractor}
              inputProps={inputProps}
              innerRef={this.tagInputRef}
            />
          </View>
        </View>
        <View style={this.props.styles.userList}>
          <UserList
            userInfos={userSearchResults}
            onSelect={this.onUserSelect}
            itemTextStyle={this.props.styles.listItem}
          />
        </View>
        {existingThreadsSection}
      </View>
    );
  }

  tagInputRef = (tagInput: ?TagInput<AccountUserInfo>) => {
    this.tagInput = tagInput;
  };

  onChangeTagInput = (userInfoInputArray: $ReadOnlyArray<AccountUserInfo>) => {
    this.setState({ userInfoInputArray });
  };

  tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username;

  setUsernameInputText = (text: string) => {
    this.searchUsers(text);
    this.setState({ usernameInputText: text });
  };

  searchUsers(usernamePrefix: string) {
    this.props.dispatchActionPromise(
      searchUsersActionTypes,
      this.props.searchUsers(usernamePrefix),
    );
  }

  onUserSelect = (userID: string) => {
    for (let existingUserInfo of this.state.userInfoInputArray) {
      if (userID === existingUserInfo.id) {
        return;
      }
    }
    const userInfoInputArray = [
      ...this.state.userInfoInputArray,
      this.props.otherUserInfos[userID],
    ];
    this.setState({
      userInfoInputArray,
      usernameInputText: '',
    });
  };

  onPressCreateThread = () => {
    if (this.createThreadPressed) {
      return;
    }
    this.createThreadPressed = true;
    if (this.state.userInfoInputArray.length === 0) {
      Alert.alert(
        'Chatting to yourself?',
        'Are you sure you want to create a thread containing only yourself?',
        [
          { text: 'Cancel', style: 'cancel' },
          { text: 'Confirm', onPress: this.dispatchNewChatThreadAction },
        ],
      );
    } else {
      this.dispatchNewChatThreadAction();
    }
  };

  dispatchNewChatThreadAction = async () => {
    const promise = this.newChatThreadAction();
    this.props.dispatchActionPromise(newThreadActionTypes, promise);
    const { newThreadInfo: rawThreadInfo } = await promise;
    const threadInfo = threadInfoFromRawThreadInfo(
      rawThreadInfo,
      this.props.viewerID,
      this.props.userInfos,
    );
    this.props.navigation.pushNewThread(threadInfo);
  };

  async newChatThreadAction() {
    this.setLinkButton(false);
    try {
      const threadTypeParam = this.props.route.params.threadType;
      const threadType = threadTypeParam
        ? threadTypeParam
        : threadTypes.CHAT_SECRET;
      const initialMemberIDs = this.state.userInfoInputArray.map(
        (userInfo: AccountUserInfo) => userInfo.id,
      );
      const parentThreadInfo = ComposeThread.getParentThreadInfo(this.props);
      return await this.props.newThread({
        type: threadType,
        parentThreadID: parentThreadInfo ? parentThreadInfo.id : null,
        initialMemberIDs,
        color: parentThreadInfo ? parentThreadInfo.color : null,
      });
    } catch (e) {
      this.createThreadPressed = false;
      this.setLinkButton(true);
      Alert.alert(
        'Unknown error',
        'Uhh... try again?',
        [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }],
        { cancelable: false },
      );
      throw e;
    }
  }

  onErrorAcknowledged = () => {
    invariant(this.tagInput, 'tagInput should be set');
    this.tagInput.focus();
  };

  onUnknownErrorAlertAcknowledged = () => {
    this.setState({ usernameInputText: '' }, this.onErrorAcknowledged);
  };

  onSelectExistingThread = (threadID: string) => {
    const threadInfo = this.props.threadInfos[threadID];
    this.props.navigation.navigate({
      name: MessageListRouteName,
      params: { threadInfo },
      key: `${MessageListRouteName}${threadInfo.id}`,
    });
  };
}

const styles = {
  container: {
    flex: 1,
  },
  existingThreadList: {
    backgroundColor: 'modalBackground',
    flex: 1,
    paddingRight: 12,
  },
  existingThreads: {
    flex: 1,
  },
  existingThreadsLabel: {
    color: 'modalForegroundSecondaryLabel',
    fontSize: 16,
    paddingLeft: 12,
    textAlign: 'center',
  },
  existingThreadsRow: {
    backgroundColor: 'modalForeground',
    borderBottomWidth: 1,
    borderColor: 'modalForegroundBorder',
    borderTopWidth: 1,
    paddingVertical: 6,
  },
  listItem: {
    color: 'modalForegroundLabel',
  },
  parentThreadLabel: {
    color: 'modalSubtextLabel',
    fontSize: 16,
    paddingLeft: 6,
  },
  parentThreadName: {
    color: 'modalForegroundLabel',
    fontSize: 16,
    paddingLeft: 6,
  },
  parentThreadRow: {
    alignItems: 'center',
    backgroundColor: 'modalSubtext',
    flexDirection: 'row',
    paddingLeft: 12,
    paddingVertical: 6,
  },
  tagInputContainer: {
    flex: 1,
    marginLeft: 8,
    paddingRight: 12,
  },
  tagInputLabel: {
    color: 'modalForegroundSecondaryLabel',
    fontSize: 16,
    paddingLeft: 12,
  },
  userList: {
    backgroundColor: 'modalBackground',
    flex: 1,
    paddingLeft: 35,
    paddingRight: 12,
  },
  userSelectionRow: {
    alignItems: 'center',
    backgroundColor: 'modalForeground',
    borderBottomWidth: 1,
    borderColor: 'modalForegroundBorder',
    flexDirection: 'row',
    paddingVertical: 6,
  },
};
const stylesSelector = styleSelector(styles);

const loadingStatusSelector = createLoadingStatusSelector(newThreadActionTypes);
registerFetchKey(searchUsersActionTypes);

export default connect(
  (
    state: AppState,
    ownProps: {
      route: NavigationRoute<'ComposeThread'>,
    },
  ) => {
    let reduxParentThreadInfo = null;
    const parentThreadInfo = ownProps.route.params.parentThreadInfo;
    if (parentThreadInfo) {
      reduxParentThreadInfo = threadInfoSelector(state)[parentThreadInfo.id];
    }
    return {
      parentThreadInfo: reduxParentThreadInfo,
      loadingStatus: loadingStatusSelector(state),
      otherUserInfos: userInfoSelectorForOtherMembersOfThread((null: ?string))(
        state,
      ),
      userSearchIndex: userSearchIndexForOtherMembersOfThread(null)(state),
      threadInfos: threadInfoSelector(state),
      colors: colorsSelector(state),
      styles: stylesSelector(state),
      viewerID: state.currentUserInfo && state.currentUserInfo.id,
      userInfos: state.userInfos,
    };
  },
  { newThread, searchUsers },
)(ComposeThread);
<commit_msg>[native] Only set createThreadPressed in ComposeThread when dispatching action<commit_after>// @flow

import type { AppState } from '../redux/redux-setup';
import type { LoadingStatus } from 'lib/types/loading-types';
import { loadingStatusPropType } from 'lib/types/loading-types';
import {
  type ThreadInfo,
  threadInfoPropType,
  type ThreadType,
  threadTypes,
  threadTypePropType,
  type NewThreadRequest,
  type NewThreadResult,
} from 'lib/types/thread-types';
import {
  type AccountUserInfo,
  accountUserInfoPropType,
  type UserInfo,
  userInfoPropType,
} from 'lib/types/user-types';
import type { DispatchActionPromise } from 'lib/utils/action-utils';
import type { UserSearchResult } from 'lib/types/search-types';
import type { ChatNavigationProp } from './chat.react';
import type { NavigationRoute } from '../navigation/route-names';

import * as React from 'react';
import PropTypes from 'prop-types';
import { View, Text, Alert } from 'react-native';
import invariant from 'invariant';
import _flow from 'lodash/fp/flow';
import _filter from 'lodash/fp/filter';
import _sortBy from 'lodash/fp/sortBy';
import { createSelector } from 'reselect';

import { connect } from 'lib/utils/redux-utils';
import { newThreadActionTypes, newThread } from 'lib/actions/thread-actions';
import { searchUsersActionTypes, searchUsers } from 'lib/actions/user-actions';
import { createLoadingStatusSelector } from 'lib/selectors/loading-selectors';
import {
  userInfoSelectorForOtherMembersOfThread,
  userSearchIndexForOtherMembersOfThread,
} from 'lib/selectors/user-selectors';
import SearchIndex from 'lib/shared/search-index';
import {
  threadInChatList,
  userIsMember,
  threadInfoFromRawThreadInfo,
} from 'lib/shared/thread-utils';
import { getUserSearchResults } from 'lib/shared/search-utils';
import { registerFetchKey } from 'lib/reducers/loading-reducer';
import { threadInfoSelector } from 'lib/selectors/thread-selectors';

import TagInput from '../components/tag-input.react';
import UserList from '../components/user-list.react';
import ThreadList from '../components/thread-list.react';
import LinkButton from '../components/link-button.react';
import { MessageListRouteName } from '../navigation/route-names';
import ThreadVisibility from '../components/thread-visibility.react';
import {
  type Colors,
  colorsPropType,
  colorsSelector,
  styleSelector,
} from '../themes/colors';

const tagInputProps = {
  placeholder: 'username',
  autoFocus: true,
  returnKeyType: 'go',
};

export type ComposeThreadParams = {|
  threadType?: ThreadType,
  parentThreadInfo?: ThreadInfo,
|};

type Props = {|
  navigation: ChatNavigationProp<'ComposeThread'>,
  route: NavigationRoute<'ComposeThread'>,
  // Redux state
  parentThreadInfo: ?ThreadInfo,
  loadingStatus: LoadingStatus,
  otherUserInfos: { [id: string]: AccountUserInfo },
  userSearchIndex: SearchIndex,
  threadInfos: { [id: string]: ThreadInfo },
  colors: Colors,
  styles: typeof styles,
  viewerID: ?string,
  userInfos: { [id: string]: UserInfo },
  // Redux dispatch functions
  dispatchActionPromise: DispatchActionPromise,
  // async functions that hit server APIs
  newThread: (request: NewThreadRequest) => Promise<NewThreadResult>,
  searchUsers: (usernamePrefix: string) => Promise<UserSearchResult>,
|};
type State = {|
  usernameInputText: string,
  userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
|};
type PropsAndState = {| ...Props, ...State |};
class ComposeThread extends React.PureComponent<Props, State> {
  static propTypes = {
    navigation: PropTypes.shape({
      setParams: PropTypes.func.isRequired,
      setOptions: PropTypes.func.isRequired,
      navigate: PropTypes.func.isRequired,
      pushNewThread: PropTypes.func.isRequired,
    }).isRequired,
    route: PropTypes.shape({
      key: PropTypes.string.isRequired,
      params: PropTypes.shape({
        threadType: threadTypePropType,
        parentThreadInfo: threadInfoPropType,
      }).isRequired,
    }).isRequired,
    parentThreadInfo: threadInfoPropType,
    loadingStatus: loadingStatusPropType.isRequired,
    otherUserInfos: PropTypes.objectOf(accountUserInfoPropType).isRequired,
    userSearchIndex: PropTypes.instanceOf(SearchIndex).isRequired,
    threadInfos: PropTypes.objectOf(threadInfoPropType).isRequired,
    colors: colorsPropType.isRequired,
    styles: PropTypes.objectOf(PropTypes.object).isRequired,
    viewerID: PropTypes.string,
    userInfos: PropTypes.objectOf(userInfoPropType).isRequired,
    dispatchActionPromise: PropTypes.func.isRequired,
    newThread: PropTypes.func.isRequired,
    searchUsers: PropTypes.func.isRequired,
  };
  state = {
    usernameInputText: '',
    userInfoInputArray: [],
  };
  tagInput: ?TagInput<AccountUserInfo>;
  createThreadPressed = false;

  constructor(props: Props) {
    super(props);
    this.setLinkButton(true);
  }

  setLinkButton(enabled: boolean) {
    this.props.navigation.setOptions({
      headerRight: () => (
        <LinkButton
          text="Create"
          onPress={this.onPressCreateThread}
          disabled={!enabled}
        />
      ),
    });
  }

  componentDidMount() {
    this.searchUsers('');
  }

  componentDidUpdate(prevProps: Props) {
    const oldReduxParentThreadInfo = prevProps.parentThreadInfo;
    const newReduxParentThreadInfo = this.props.parentThreadInfo;
    if (
      newReduxParentThreadInfo &&
      newReduxParentThreadInfo !== oldReduxParentThreadInfo
    ) {
      this.props.navigation.setParams({
        parentThreadInfo: newReduxParentThreadInfo,
      });
    }
  }

  static getParentThreadInfo(props: {
    route: NavigationRoute<'ComposeThread'>,
  }): ?ThreadInfo {
    return props.route.params.parentThreadInfo;
  }

  userSearchResultsSelector = createSelector(
    (propsAndState: PropsAndState) => propsAndState.usernameInputText,
    (propsAndState: PropsAndState) => propsAndState.otherUserInfos,
    (propsAndState: PropsAndState) => propsAndState.userSearchIndex,
    (propsAndState: PropsAndState) => propsAndState.userInfoInputArray,
    (propsAndState: PropsAndState) =>
      ComposeThread.getParentThreadInfo(propsAndState),
    (
      text: string,
      userInfos: { [id: string]: AccountUserInfo },
      searchIndex: SearchIndex,
      userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
      parentThreadInfo: ?ThreadInfo,
    ) =>
      getUserSearchResults(
        text,
        userInfos,
        searchIndex,
        userInfoInputArray.map(userInfo => userInfo.id),
        parentThreadInfo,
      ),
  );

  get userSearchResults() {
    return this.userSearchResultsSelector({ ...this.props, ...this.state });
  }

  existingThreadsSelector = createSelector(
    (propsAndState: PropsAndState) =>
      ComposeThread.getParentThreadInfo(propsAndState),
    (propsAndState: PropsAndState) => propsAndState.threadInfos,
    (propsAndState: PropsAndState) => propsAndState.userInfoInputArray,
    (
      parentThreadInfo: ?ThreadInfo,
      threadInfos: { [id: string]: ThreadInfo },
      userInfoInputArray: $ReadOnlyArray<AccountUserInfo>,
    ) => {
      const userIDs = userInfoInputArray.map(userInfo => userInfo.id);
      if (userIDs.length === 0) {
        return [];
      }
      return _flow(
        _filter(
          (threadInfo: ThreadInfo) =>
            threadInChatList(threadInfo) &&
            (!parentThreadInfo ||
              threadInfo.parentThreadID === parentThreadInfo.id) &&
            userIDs.every(userID => userIsMember(threadInfo, userID)),
        ),
        _sortBy(
          ([
            'members.length',
            (threadInfo: ThreadInfo) => (threadInfo.name ? 1 : 0),
          ]: $ReadOnlyArray<string | ((threadInfo: ThreadInfo) => mixed)>),
        ),
      )(threadInfos);
    },
  );

  get existingThreads() {
    return this.existingThreadsSelector({ ...this.props, ...this.state });
  }

  render() {
    let existingThreadsSection = null;
    const { existingThreads, userSearchResults } = this;
    if (existingThreads.length > 0) {
      existingThreadsSection = (
        <View style={this.props.styles.existingThreads}>
          <View style={this.props.styles.existingThreadsRow}>
            <Text style={this.props.styles.existingThreadsLabel}>
              Existing threads
            </Text>
          </View>
          <View style={this.props.styles.existingThreadList}>
            <ThreadList
              threadInfos={existingThreads}
              onSelect={this.onSelectExistingThread}
              itemTextStyle={this.props.styles.listItem}
            />
          </View>
        </View>
      );
    }
    let parentThreadRow = null;
    const parentThreadInfo = ComposeThread.getParentThreadInfo(this.props);
    if (parentThreadInfo) {
      const threadType = this.props.route.params.threadType;
      invariant(
        threadType !== undefined && threadType !== null,
        `no threadType provided for ${parentThreadInfo.id}`,
      );
      const threadVisibilityColor = this.props.colors.modalForegroundLabel;
      parentThreadRow = (
        <View style={this.props.styles.parentThreadRow}>
          <ThreadVisibility
            threadType={threadType}
            color={threadVisibilityColor}
          />
          <Text style={this.props.styles.parentThreadLabel}>within</Text>
          <Text style={this.props.styles.parentThreadName}>
            {parentThreadInfo.uiName}
          </Text>
        </View>
      );
    }
    const inputProps = {
      ...tagInputProps,
      onSubmitEditing: this.onPressCreateThread,
    };
    return (
      <View style={this.props.styles.container}>
        {parentThreadRow}
        <View style={this.props.styles.userSelectionRow}>
          <Text style={this.props.styles.tagInputLabel}>To: </Text>
          <View style={this.props.styles.tagInputContainer}>
            <TagInput
              value={this.state.userInfoInputArray}
              onChange={this.onChangeTagInput}
              text={this.state.usernameInputText}
              onChangeText={this.setUsernameInputText}
              labelExtractor={this.tagDataLabelExtractor}
              inputProps={inputProps}
              innerRef={this.tagInputRef}
            />
          </View>
        </View>
        <View style={this.props.styles.userList}>
          <UserList
            userInfos={userSearchResults}
            onSelect={this.onUserSelect}
            itemTextStyle={this.props.styles.listItem}
          />
        </View>
        {existingThreadsSection}
      </View>
    );
  }

  tagInputRef = (tagInput: ?TagInput<AccountUserInfo>) => {
    this.tagInput = tagInput;
  };

  onChangeTagInput = (userInfoInputArray: $ReadOnlyArray<AccountUserInfo>) => {
    this.setState({ userInfoInputArray });
  };

  tagDataLabelExtractor = (userInfo: AccountUserInfo) => userInfo.username;

  setUsernameInputText = (text: string) => {
    this.searchUsers(text);
    this.setState({ usernameInputText: text });
  };

  searchUsers(usernamePrefix: string) {
    this.props.dispatchActionPromise(
      searchUsersActionTypes,
      this.props.searchUsers(usernamePrefix),
    );
  }

  onUserSelect = (userID: string) => {
    for (let existingUserInfo of this.state.userInfoInputArray) {
      if (userID === existingUserInfo.id) {
        return;
      }
    }
    const userInfoInputArray = [
      ...this.state.userInfoInputArray,
      this.props.otherUserInfos[userID],
    ];
    this.setState({
      userInfoInputArray,
      usernameInputText: '',
    });
  };

  onPressCreateThread = () => {
    if (this.createThreadPressed) {
      return;
    }
    if (this.state.userInfoInputArray.length === 0) {
      Alert.alert(
        'Chatting to yourself?',
        'Are you sure you want to create a thread containing only yourself?',
        [
          { text: 'Cancel', style: 'cancel' },
          { text: 'Confirm', onPress: this.dispatchNewChatThreadAction },
        ],
      );
    } else {
      this.dispatchNewChatThreadAction();
    }
  };

  dispatchNewChatThreadAction = async () => {
    this.createThreadPressed = true;
    const promise = this.newChatThreadAction();
    this.props.dispatchActionPromise(newThreadActionTypes, promise);
    const { newThreadInfo: rawThreadInfo } = await promise;
    const threadInfo = threadInfoFromRawThreadInfo(
      rawThreadInfo,
      this.props.viewerID,
      this.props.userInfos,
    );
    this.props.navigation.pushNewThread(threadInfo);
  };

  async newChatThreadAction() {
    this.setLinkButton(false);
    try {
      const threadTypeParam = this.props.route.params.threadType;
      const threadType = threadTypeParam
        ? threadTypeParam
        : threadTypes.CHAT_SECRET;
      const initialMemberIDs = this.state.userInfoInputArray.map(
        (userInfo: AccountUserInfo) => userInfo.id,
      );
      const parentThreadInfo = ComposeThread.getParentThreadInfo(this.props);
      return await this.props.newThread({
        type: threadType,
        parentThreadID: parentThreadInfo ? parentThreadInfo.id : null,
        initialMemberIDs,
        color: parentThreadInfo ? parentThreadInfo.color : null,
      });
    } catch (e) {
      this.createThreadPressed = false;
      this.setLinkButton(true);
      Alert.alert(
        'Unknown error',
        'Uhh... try again?',
        [{ text: 'OK', onPress: this.onUnknownErrorAlertAcknowledged }],
        { cancelable: false },
      );
      throw e;
    }
  }

  onErrorAcknowledged = () => {
    invariant(this.tagInput, 'tagInput should be set');
    this.tagInput.focus();
  };

  onUnknownErrorAlertAcknowledged = () => {
    this.setState({ usernameInputText: '' }, this.onErrorAcknowledged);
  };

  onSelectExistingThread = (threadID: string) => {
    const threadInfo = this.props.threadInfos[threadID];
    this.props.navigation.navigate({
      name: MessageListRouteName,
      params: { threadInfo },
      key: `${MessageListRouteName}${threadInfo.id}`,
    });
  };
}

const styles = {
  container: {
    flex: 1,
  },
  existingThreadList: {
    backgroundColor: 'modalBackground',
    flex: 1,
    paddingRight: 12,
  },
  existingThreads: {
    flex: 1,
  },
  existingThreadsLabel: {
    color: 'modalForegroundSecondaryLabel',
    fontSize: 16,
    paddingLeft: 12,
    textAlign: 'center',
  },
  existingThreadsRow: {
    backgroundColor: 'modalForeground',
    borderBottomWidth: 1,
    borderColor: 'modalForegroundBorder',
    borderTopWidth: 1,
    paddingVertical: 6,
  },
  listItem: {
    color: 'modalForegroundLabel',
  },
  parentThreadLabel: {
    color: 'modalSubtextLabel',
    fontSize: 16,
    paddingLeft: 6,
  },
  parentThreadName: {
    color: 'modalForegroundLabel',
    fontSize: 16,
    paddingLeft: 6,
  },
  parentThreadRow: {
    alignItems: 'center',
    backgroundColor: 'modalSubtext',
    flexDirection: 'row',
    paddingLeft: 12,
    paddingVertical: 6,
  },
  tagInputContainer: {
    flex: 1,
    marginLeft: 8,
    paddingRight: 12,
  },
  tagInputLabel: {
    color: 'modalForegroundSecondaryLabel',
    fontSize: 16,
    paddingLeft: 12,
  },
  userList: {
    backgroundColor: 'modalBackground',
    flex: 1,
    paddingLeft: 35,
    paddingRight: 12,
  },
  userSelectionRow: {
    alignItems: 'center',
    backgroundColor: 'modalForeground',
    borderBottomWidth: 1,
    borderColor: 'modalForegroundBorder',
    flexDirection: 'row',
    paddingVertical: 6,
  },
};
const stylesSelector = styleSelector(styles);

const loadingStatusSelector = createLoadingStatusSelector(newThreadActionTypes);
registerFetchKey(searchUsersActionTypes);

export default connect(
  (
    state: AppState,
    ownProps: {
      route: NavigationRoute<'ComposeThread'>,
    },
  ) => {
    let reduxParentThreadInfo = null;
    const parentThreadInfo = ownProps.route.params.parentThreadInfo;
    if (parentThreadInfo) {
      reduxParentThreadInfo = threadInfoSelector(state)[parentThreadInfo.id];
    }
    return {
      parentThreadInfo: reduxParentThreadInfo,
      loadingStatus: loadingStatusSelector(state),
      otherUserInfos: userInfoSelectorForOtherMembersOfThread((null: ?string))(
        state,
      ),
      userSearchIndex: userSearchIndexForOtherMembersOfThread(null)(state),
      threadInfos: threadInfoSelector(state),
      colors: colorsSelector(state),
      styles: stylesSelector(state),
      viewerID: state.currentUserInfo && state.currentUserInfo.id,
      userInfos: state.userInfos,
    };
  },
  { newThread, searchUsers },
)(ComposeThread);
<|endoftext|>


<commit_before>import React from 'react';
import bukovelAPI from './data-access/bukovelAPI';
import CardNumberInput from './components/CardNumberInput';
import TestCardNumber from './components/TestCardNumber';
import LiftsTable from './components/LiftsTable';
import Paper from 'material-ui/Paper';
import CircularProgress from 'material-ui/CircularProgress';
import baseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';

class App extends React.Component {
  constructor() {
    super();

    this.state = {
      cardInfo: {
        lifts: []
      },
      cardNumber: '',
      isDataLoads: false
    };
    this.handleCardNumberChange = this.handleCardNumberChange.bind(this);
  }

  getChildContext() {
    return {
      muiTheme: getMuiTheme(baseTheme)
    };
  }

  handleCardNumberChange(event) {
    if (event.isValid) {
      let spinnerTimeOut = setTimeout(() => {
        this.setState({
          isDataLoads: true
        });
      }, 100);
      bukovelAPI
        .getCardBalance(event.text)
        .then(data => {
          clearTimeout(spinnerTimeOut);
          this.setState({
            isDataLoads: false,
            cardInfo: data
          });
        });
    }
  }

  renderTable() {
    return this.state.isDataLoads ? <CircularProgress size={1.5} /> : (
      <Paper zDepth={2}>
        <LiftsTable lifts={this.state.cardInfo.lifts} />
      </Paper>
    );
  }

  render() {
    return (

          <CardNumberInput
            id='cardnumber'
            className='buka-cardnumber__input'
            onChange={this.handleCardNumberChange}
            value={this.state.cardNumber} />
          <TestCardNumber />

        {this.renderTable()}

    );
  }
}

App.childContextTypes = {
  muiTheme: React.PropTypes.object.isRequired
};

export default App;<commit_msg>Add AppBar.<commit_after>import React from 'react';
import bukovelAPI from './data-access/bukovelAPI';
import CardNumberInput from './components/CardNumberInput';
import TestCardNumber from './components/TestCardNumber';
import LiftsTable from './components/LiftsTable';
import AppBar from 'material-ui/AppBar';
import Paper from 'material-ui/Paper';
import CircularProgress from 'material-ui/CircularProgress';
import baseTheme from 'material-ui/styles/baseThemes/lightBaseTheme';
import getMuiTheme from 'material-ui/styles/getMuiTheme';

class App extends React.Component {
  constructor() {
    super();

    this.state = {
      cardInfo: {
        lifts: []
      },
      cardNumber: '',
      isDataLoads: false
    };
    this.handleCardNumberChange = this.handleCardNumberChange.bind(this);
  }

  getChildContext() {
    return {
      muiTheme: getMuiTheme(baseTheme)
    };
  }

  handleCardNumberChange(event) {
    if (event.isValid) {
      let spinnerTimeOut = setTimeout(() => {
        this.setState({
          isDataLoads: true
        });
      }, 100);
      bukovelAPI
        .getCardBalance(event.text)
        .then(data => {
          clearTimeout(spinnerTimeOut);
          this.setState({
            isDataLoads: false,
            cardInfo: data
          });
        });
    }
  }

  renderTable() {
    return this.state.isDataLoads ? <CircularProgress size={1.5} /> : (
      <Paper zDepth={2}>
        <LiftsTable lifts={this.state.cardInfo.lifts} />
      </Paper>
    );
  }

  render() {
    return (

        <AppBar title='Buka' />

          <CardNumberInput
            id='cardnumber'
            className='buka-cardnumber__input'
            onChange={this.handleCardNumberChange}
            value={this.state.cardNumber} />
          <TestCardNumber />

        {this.renderTable()}

    );
  }
}

App.childContextTypes = {
  muiTheme: React.PropTypes.object.isRequired
};

export default App;<|endoftext|>