<commit_before>import commonConfig from 'common/config.js';
import { randomUUID } from 'crypto';
import humanizeDuration from 'humanize-duration';
import yup from 'yup';
import config from '../config.js';
import { db, firestore, mail } from './clients.js';

const ROLES = {
  agency: 'agency',
  contractor: 'contractor',
  public: 'reporter',
  admin: 'admin',
};
const NO_REPLY_EMAIL = 'noreply@utah.gov';

function getTrackingSettings() {
  if (process.env.ENVIRONMENT === 'development') {
    return {
      clickTracking: {
        enable: false,
      },
    };
  }

  return null;
}

export const EXPIRED_APPROVAL = 'EXPIRED_APPROVAL';

// TODO: clean up after 0.0.0 is gone
export const registerSchema_0_0_0 = yup.object().shape({
  organization: yup
    .object()
    .shape({
      name: yup.string().required(),
      org_type: yup.string().required(),
    })
    .nullable(),
  user: yup
    .object()
    .shape({
      organization_id: yup.number(),
      role: yup.string().required(),
      auth_provider: yup.string().required(),
      auth_id: yup.string().required(),
      email: yup.string().email().required(),
      first_name: yup.string().required(),
      last_name: yup.string().required(),
      phone: yup.string().required(),
    })
    .required(),
});

export const registerSchema = yup.object().shape({
  organization: yup
    .object()
    .shape({
      name: yup.string().required(),
      org_type: yup.string().required(),
      id: yup.number().required(),
    })
    .nullable(),
  user: yup
    .object()
    .shape({
      role: yup.string().required(),
      auth_provider: yup.string().required(),
      auth_id: yup.string().required(),
      email: yup.string().email().required(),
      first_name: yup.string().required(),
      last_name: yup.string().required(),
      phone: yup.string().required(),
    })
    .required(),
});

export const loginSchema = yup.object().shape({
  auth_id: yup.string().required(),
  auth_provider: yup.string().required(),
  email: yup.string().email().required(),
  first_name: yup.string().required(),
  last_name: yup.string().required(),
});

export async function isExistingUser({ auth_provider, auth_id }) {
  const rows = await db.select('*').from('users').where({ auth_provider, auth_id });

  return rows.length > 0;
}

async function ensureOrganization(organization, transaction) {
  let orgId = organization ? organization.id : null;
  if (organization && organization.id === commonConfig.otherOrg.id) {
    const orgInsertResult = await transaction('organizations').insert(
      {
        name: organization.name,
        org_type: organization.org_type,
      },
      'id'
    );
    orgId = orgInsertResult[0];
  }

  return orgId;
}

export async function registerUser_0_0_0(organization, user) {
  let approved = null;
  await db.transaction(async (transaction) => {
    const now = new Date();

    let orgInsertResult;
    if (organization) {
      orgInsertResult = await transaction('organizations').insert(organization, 'id');
    }
    let approved_date = null;
    if (user.role === ROLES.public) {
      approved = true;
      approved_date = now;
    }

    await transaction('users').insert({
      ...user,
      registered_date: now,
      last_logged_in: now,
      organization_id: organization ? orgInsertResult[0] : null,
      approved,
      approved_date,
    });
  });

  if (!approved) {
    sendApprovalEmail(user, organization);
  }
}

export async function registerUser(organization, user) {
  let approved = null;
  await db.transaction(async (transaction) => {
    const now = new Date();

    const orgId = await ensureOrganization(organization, transaction);

    let approved_date = null;
    if (user.role === ROLES.public) {
      approved = true;
      approved_date = now;
    }

    await transaction('users').insert({
      ...user,
      registered_date: now,
      last_logged_in: now,
      organization_id: orgId,
      approved,
      approved_date,
    });
  });

  if (!approved) {
    sendApprovalEmail(user, organization);
  }
}

const APPROVAL_DOC_TYPE = 'approvals';
export async function sendApprovalEmail(user, organization) {
  const guid = randomUUID();

  const approvalExpiration = Date.now() + config.APPROVAL_EXPIRATION_PERIOD;
  const document = firestore.doc(`${APPROVAL_DOC_TYPE}/${guid}`);
  await document.set({
    ...user,
    approvalExpiration,
  });

  const data = {
    user,
    organization,
    guid,
    api: process.env.API,
    environment: process.env.ENVIRONMENT,
  };

  const email = {
    to: process.env.ADMIN_EMAIL,
    from: NO_REPLY_EMAIL,
    templateId: 'd-021d5c287a1d4295a7ade35724bd2994', // roadkill-new-user
    dynamicTemplateData: data,
    trackingSettings: getTrackingSettings(),
  };

  try {
    await mail.send(email);
  } catch (error) {
    console.error(error);
  }
}

async function sendApproveRejectNotificationEmail(user, rejected) {
  const data = {
    user,
    rejected,
    environment: process.env.ENVIRONMENT,
  };

  const email = {
    to: process.env.ADMIN_EMAIL,
    from: NO_REPLY_EMAIL,
    templateId: 'd-ae24b5a55c9e461298a1bfc14e0b383e',
    dynamicTemplateData: data,
  };

  if (process.env.ENVIRONMENT === 'development') {
    mail.trackingSettings = {
      clickTracking: {
        enable: false,
      },
    };
  }

  try {
    await mail.send(email);
  } catch (error) {
    console.error(error);
  }
}

async function getUserFromGUID(guid) {
  const document = firestore.doc(`${APPROVAL_DOC_TYPE}/${guid}`);
  const snapshot = await document.get();

  if (!snapshot.exists) {
    const error = new Error('invalid approval guid!');
    error.code = 'INVALID_GUID';

    throw error;
  }

  return snapshot.data();
}

export function checkExpiration(user) {
  if (!user.approvalExpiration || user.approvalExpiration < Date.now()) {
    const error = new Error(
      `It has been more than ${humanizeDuration(config.APPROVAL_EXPIRATION_PERIOD)} since ${user.first_name} ${
        user.last_name
      } (${user.email}) registered. Their approval has expired. Approval will need to be done in the database directly.`
    );
    error.code = EXPIRED_APPROVAL;

    throw error;
  }
}

export async function approveUser(guid, role) {
  const user = await getUserFromGUID(guid);
  const userInfo = `${user.first_name} ${user.last_name} (${user.email})`;

  checkExpiration(user);

  await db('users').where({ auth_id: user.auth_id, auth_provider: user.auth_provider }).update({
    role,
    approved_date: new Date(),
    approved: true,
  });

  await sendApproveRejectNotificationEmail({ ...user, role }, false);

  return `${userInfo} has been approved as: ${role}`;
}

export async function deleteUser(userId) {
  await db('users').where({ id: userId }).update({
    organization_id: null,
    auth_id: 'deleted',
    email: 'deleted',
    first_name: 'deleted',
    last_name: 'deleted',
    phone: 'deleted',
  });
}

export async function rejectUser(guid) {
  const user = await getUserFromGUID(guid);

  checkExpiration(user);

  await db('users').where({ auth_id: user.auth_id, auth_provider: user.auth_provider }).update({
    approved_date: new Date(),
    approved: false,
  });

  await sendApproveRejectNotificationEmail(user, true);

  return `${user.first_name} ${user.last_name} (${user.email}) has been rejected`;
}

export async function updateUser({ auth_id, auth_provider, email, first_name, last_name }) {
  await db('users').where({ auth_id, auth_provider }).update({
    email,
    first_name,
    last_name,
    last_logged_in: new Date(),
  });
}

export async function getUser(auth_id, auth_provider) {
  const rows = await db('users').select('*').where({ auth_provider, auth_id });

  return rows[0];
}

// TODO: clean up after 0.0.0
export async function getProfile_0_0_0(userId) {
  const profileData = await db('users as u')
    .leftJoin('organizations as o', 'u.organization_id', 'o.id')
    .columns(
      'u.auth_provider',
      'u.email',
      'u.first_name',
      'u.last_name',
      'u.phone',
      'u.role',
      'u.approved',
      'u.registered_date',
      { organization: 'o.name' }
    )
    .where({ 'u.id': userId })
    .first();

  const reportsSubmitted = await db('report_infos').count('report_id').where({ user_id: userId });

  return {
    ...profileData,
    reports_submitted: reportsSubmitted[0].count,
  };
}

export async function getProfile(userId) {
  const profileData = await db('users as u')
    .leftJoin('organizations as o', 'u.organization_id', 'o.id')
    .columns(
      'u.auth_provider',
      'u.email',
      'u.first_name',
      'u.last_name',
      'u.phone',
      'u.role',
      'u.approved',
      'u.registered_date',
      { organization_name: 'o.name' },
      'u.organization_id'
    )
    .where({ 'u.id': userId })
    .first();

  const reportsSubmitted = await db('report_infos').count('report_id').where({ user_id: userId });

  return {
    ...profileData,
    reports_submitted: reportsSubmitted[0].count,
  };
}

// TODO: clean up after 0.0.0
export async function updateProfile_0_0_0(userId, profile, organizationId) {
  const { phone, organization } = profile;

  await db('users').where({ id: userId }).update({
    phone,
  });

  await db('organizations').where({ id: organizationId }).update({
    name: organization,
  });
}

export async function updateProfile(userId, profile) {
  const { phone, organization_id, organization_name, organization_type } = profile;

  await db.transaction(async (transaction) => {
    const orgId = await ensureOrganization(
      { id: organization_id, name: organization_name, org_type: organization_type },
      transaction
    );

    await transaction('users').where({ id: userId }).update({
      organization_id: orgId,
      phone,
    });
  });
}
<commit_msg>feat(back): send user approve/reject notification emails<commit_after>import commonConfig from 'common/config.js';
import { randomUUID } from 'crypto';
import humanizeDuration from 'humanize-duration';
import yup from 'yup';
import config from '../config.js';
import { db, firestore, mail } from './clients.js';

const ROLES = {
  agency: 'agency',
  contractor: 'contractor',
  public: 'reporter',
  admin: 'admin',
};
const NO_REPLY_EMAIL = 'noreply@utah.gov';

function getTrackingSettings() {
  if (process.env.ENVIRONMENT === 'development') {
    return {
      clickTracking: {
        enable: false,
      },
    };
  }

  return null;
}

export const EXPIRED_APPROVAL = 'EXPIRED_APPROVAL';

// TODO: clean up after 0.0.0 is gone
export const registerSchema_0_0_0 = yup.object().shape({
  organization: yup
    .object()
    .shape({
      name: yup.string().required(),
      org_type: yup.string().required(),
    })
    .nullable(),
  user: yup
    .object()
    .shape({
      organization_id: yup.number(),
      role: yup.string().required(),
      auth_provider: yup.string().required(),
      auth_id: yup.string().required(),
      email: yup.string().email().required(),
      first_name: yup.string().required(),
      last_name: yup.string().required(),
      phone: yup.string().required(),
    })
    .required(),
});

export const registerSchema = yup.object().shape({
  organization: yup
    .object()
    .shape({
      name: yup.string().required(),
      org_type: yup.string().required(),
      id: yup.number().required(),
    })
    .nullable(),
  user: yup
    .object()
    .shape({
      role: yup.string().required(),
      auth_provider: yup.string().required(),
      auth_id: yup.string().required(),
      email: yup.string().email().required(),
      first_name: yup.string().required(),
      last_name: yup.string().required(),
      phone: yup.string().required(),
    })
    .required(),
});

export const loginSchema = yup.object().shape({
  auth_id: yup.string().required(),
  auth_provider: yup.string().required(),
  email: yup.string().email().required(),
  first_name: yup.string().required(),
  last_name: yup.string().required(),
});

export async function isExistingUser({ auth_provider, auth_id }) {
  const rows = await db.select('*').from('users').where({ auth_provider, auth_id });

  return rows.length > 0;
}

async function ensureOrganization(organization, transaction) {
  let orgId = organization ? organization.id : null;
  if (organization && organization.id === commonConfig.otherOrg.id) {
    const orgInsertResult = await transaction('organizations').insert(
      {
        name: organization.name,
        org_type: organization.org_type,
      },
      'id'
    );
    orgId = orgInsertResult[0];
  }

  return orgId;
}

export async function registerUser_0_0_0(organization, user) {
  let approved = null;
  await db.transaction(async (transaction) => {
    const now = new Date();

    let orgInsertResult;
    if (organization) {
      orgInsertResult = await transaction('organizations').insert(organization, 'id');
    }
    let approved_date = null;
    if (user.role === ROLES.public) {
      approved = true;
      approved_date = now;
    }

    await transaction('users').insert({
      ...user,
      registered_date: now,
      last_logged_in: now,
      organization_id: organization ? orgInsertResult[0] : null,
      approved,
      approved_date,
    });
  });

  if (!approved) {
    sendApprovalEmail(user, organization);
  }
}

export async function registerUser(organization, user) {
  let approved = null;
  await db.transaction(async (transaction) => {
    const now = new Date();

    const orgId = await ensureOrganization(organization, transaction);

    let approved_date = null;
    if (user.role === ROLES.public) {
      approved = true;
      approved_date = now;
    }

    await transaction('users').insert({
      ...user,
      registered_date: now,
      last_logged_in: now,
      organization_id: orgId,
      approved,
      approved_date,
    });
  });

  if (!approved) {
    sendApprovalEmail(user, organization);
  }
}

const APPROVAL_DOC_TYPE = 'approvals';
export async function sendApprovalEmail(user, organization) {
  const guid = randomUUID();

  const approvalExpiration = Date.now() + config.APPROVAL_EXPIRATION_PERIOD;
  const document = firestore.doc(`${APPROVAL_DOC_TYPE}/${guid}`);
  await document.set({
    ...user,
    approvalExpiration,
  });

  const data = {
    user,
    organization,
    guid,
    api: process.env.API,
    environment: process.env.ENVIRONMENT,
  };

  const email = {
    to: process.env.ADMIN_EMAIL,
    from: NO_REPLY_EMAIL,
    templateId: 'd-021d5c287a1d4295a7ade35724bd2994', // roadkill-new-user
    dynamicTemplateData: data,
    trackingSettings: getTrackingSettings(),
  };

  try {
    await mail.send(email);
  } catch (error) {
    console.error(error);
  }
}

async function sendApproveRejectAdminNotificationEmail(user, rejected) {
  const data = {
    user,
    rejected,
    environment: process.env.ENVIRONMENT,
  };

  const email = {
    to: process.env.ADMIN_EMAIL,
    from: NO_REPLY_EMAIL,
    templateId: 'd-ae24b5a55c9e461298a1bfc14e0b383e',
    dynamicTemplateData: data,
    trackingSettings: getTrackingSettings(),
  };

  try {
    await mail.send(email);
  } catch (error) {
    console.error(error);
  }
}

async function sendApproveRejectUserNotificationEmail(user, rejected) {
  const approveMessage = `Congratulations, you have been approved as ${user.role} in the Wildlife-Vehicle Collision Reporter app.`;
  const rejectMessage = `Unfortunately, your registration for the Wildlife-Vehicle Collision Reporter app has been declined. You may send an email to ${process.env.ADMIN_EMAIL} if you believe that this was a mistake.`;
  const email = {
    to: user.email,
    from: NO_REPLY_EMAIL,
    subject: rejected ? 'Your WVCR registration request was denied' : 'Your WVCR registration request was approved',
    text: rejected ? rejectMessage : approveMessage,
    trackingSettings: getTrackingSettings(),
  };

  try {
    await mail.send(email);
  } catch (error) {
    console.error(error);
  }
}

async function getUserFromGUID(guid) {
  const document = firestore.doc(`${APPROVAL_DOC_TYPE}/${guid}`);
  const snapshot = await document.get();

  if (!snapshot.exists) {
    const error = new Error('invalid approval guid!');
    error.code = 'INVALID_GUID';

    throw error;
  }

  return snapshot.data();
}

export function checkExpiration(user) {
  if (!user.approvalExpiration || user.approvalExpiration < Date.now()) {
    const error = new Error(
      `It has been more than ${humanizeDuration(config.APPROVAL_EXPIRATION_PERIOD)} since ${user.first_name} ${
        user.last_name
      } (${user.email}) registered. Their approval has expired. Approval will need to be done in the database directly.`
    );
    error.code = EXPIRED_APPROVAL;

    throw error;
  }
}

export async function approveUser(guid, role) {
  const user = await getUserFromGUID(guid);
  const userInfo = `${user.first_name} ${user.last_name} (${user.email})`;

  checkExpiration(user);

  await db('users').where({ auth_id: user.auth_id, auth_provider: user.auth_provider }).update({
    role,
    approved_date: new Date(),
    approved: true,
  });

  await sendApproveRejectAdminNotificationEmail({ ...user, role }, false);
  await sendApproveRejectUserNotificationEmail({ ...user, role }, false);

  return `${userInfo} has been approved as: ${role}`;
}

export async function deleteUser(userId) {
  await db('users').where({ id: userId }).update({
    organization_id: null,
    auth_id: 'deleted',
    email: 'deleted',
    first_name: 'deleted',
    last_name: 'deleted',
    phone: 'deleted',
  });
}

export async function rejectUser(guid) {
  const user = await getUserFromGUID(guid);

  checkExpiration(user);

  await db('users').where({ auth_id: user.auth_id, auth_provider: user.auth_provider }).update({
    approved_date: new Date(),
    approved: false,
  });

  await sendApproveRejectAdminNotificationEmail(user, true);
  await sendApproveRejectUserNotificationEmail(user, true);

  return `${user.first_name} ${user.last_name} (${user.email}) has been rejected`;
}

export async function updateUser({ auth_id, auth_provider, email, first_name, last_name }) {
  await db('users').where({ auth_id, auth_provider }).update({
    email,
    first_name,
    last_name,
    last_logged_in: new Date(),
  });
}

export async function getUser(auth_id, auth_provider) {
  const rows = await db('users').select('*').where({ auth_provider, auth_id });

  return rows[0];
}

// TODO: clean up after 0.0.0
export async function getProfile_0_0_0(userId) {
  const profileData = await db('users as u')
    .leftJoin('organizations as o', 'u.organization_id', 'o.id')
    .columns(
      'u.auth_provider',
      'u.email',
      'u.first_name',
      'u.last_name',
      'u.phone',
      'u.role',
      'u.approved',
      'u.registered_date',
      { organization: 'o.name' }
    )
    .where({ 'u.id': userId })
    .first();

  const reportsSubmitted = await db('report_infos').count('report_id').where({ user_id: userId });

  return {
    ...profileData,
    reports_submitted: reportsSubmitted[0].count,
  };
}

export async function getProfile(userId) {
  const profileData = await db('users as u')
    .leftJoin('organizations as o', 'u.organization_id', 'o.id')
    .columns(
      'u.auth_provider',
      'u.email',
      'u.first_name',
      'u.last_name',
      'u.phone',
      'u.role',
      'u.approved',
      'u.registered_date',
      { organization_name: 'o.name' },
      'u.organization_id'
    )
    .where({ 'u.id': userId })
    .first();

  const reportsSubmitted = await db('report_infos').count('report_id').where({ user_id: userId });

  return {
    ...profileData,
    reports_submitted: reportsSubmitted[0].count,
  };
}

// TODO: clean up after 0.0.0
export async function updateProfile_0_0_0(userId, profile, organizationId) {
  const { phone, organization } = profile;

  await db('users').where({ id: userId }).update({
    phone,
  });

  await db('organizations').where({ id: organizationId }).update({
    name: organization,
  });
}

export async function updateProfile(userId, profile) {
  const { phone, organization_id, organization_name, organization_type } = profile;

  await db.transaction(async (transaction) => {
    const orgId = await ensureOrganization(
      { id: organization_id, name: organization_name, org_type: organization_type },
      transaction
    );

    await transaction('users').where({ id: userId }).update({
      organization_id: orgId,
      phone,
    });
  });
}
<|endoftext|>


<commit_before>/*
 Copyright (c) 2016 Dmitry Brant.
 http://dmitrybrant.com

 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 parseFormat(reader)
{
    return parseJpgStructure(reader);
}

function parsePngStructure(reader, offset) {
    var results = new ResultNode("PNG structure");
    try {
        var stream = new DataStream(reader);
        if (offset !== undefined) {
            stream.skip(offset);
        }
        var chunkType;
        var chunkLength;

        stream.skip(8);

        while (!stream.eof()) {
            chunkLength = stream.readUIntBe();
            chunkType = stream.readAsciiString(4);

            var node = results.add(chunkType, chunkLength.toString() + " bytes");

            // read chunk data
            if (chunkLength > 0) {

                var position = stream.position;

                if (chunkType == "tEXt" && chunkLength < 65536) {
                    var chunkStr = reader.getAsciiStringAt(position, chunkLength).replace("\0", " - ");
                    node.add("Contents", chunkStr);
                }

                stream.skip(chunkLength);
            }

            // skip crc
            stream.skip(4);

            // is this the ending chunk?
            if (chunkType == "IEND") {
                break;
            }
        }
    } catch(e) {
        console.log("Error while reading PNG: " + e);
    }
    return results;
}<commit_msg>Fix .png<commit_after>/*
 Copyright (c) 2016 Dmitry Brant.
 http://dmitrybrant.com

 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 parseFormat(reader)
{
    return parsePngStructure(reader);
}

function parsePngStructure(reader, offset) {
    var results = new ResultNode("PNG structure");
    try {
        var stream = new DataStream(reader);
        if (offset !== undefined) {
            stream.skip(offset);
        }
        var chunkType;
        var chunkLength;

        stream.skip(8);

        while (!stream.eof()) {
            chunkLength = stream.readUIntBe();
            chunkType = stream.readAsciiString(4);

            var node = results.add(chunkType, chunkLength.toString() + " bytes");

            // read chunk data
            if (chunkLength > 0) {

                var position = stream.position;

                if (chunkType == "tEXt" && chunkLength < 65536) {
                    var chunkStr = reader.getAsciiStringAt(position, chunkLength).replace("\0", " - ");
                    node.add("Contents", chunkStr);
                }

                stream.skip(chunkLength);
            }

            // skip crc
            stream.skip(4);

            // is this the ending chunk?
            if (chunkType == "IEND") {
                break;
            }
        }
    } catch(e) {
        console.log("Error while reading PNG: " + e);
    }
    return results;
}<|endoftext|>


<commit_before>$(document).ready(function(){
	//Prep callapse menu
	 $('#collapse_navbar').collapse({'toggle': false});
	//Load content
	$(window).hashchange();
});	//executed after the page has loaded

//AJAX wrapper function
jQuery.ajaxWrapper = function(resource, type, secure, data, notification, ajaxOptions) {
	var alert;
	if (type == 'POST') {
		var data = JSON.stringify(data);
		var contenttype = 'application/json';
	} else {
		var data = '';
		var contenttype = 'application/x-www-form-urlencoded; charset=UTF-8'; //is actually only true for POST requests, but this is default, see http://api.jquery.com/jquery.ajax/
	}
	$.ajax($.extend({
		type: type,
		contentType: contenttype,
		//url: "http://127.0.0.1/api/"+resource, //local
		url: "http://www.settlepad.com/api/"+resource, //web
		dataType: "json",
		//data: {data: data},
		data: data,
		beforeSend: function(jqXHR, settings){
			var timestamp = Math.round((new Date()).getTime() / 1000);
			jqXHR.setRequestHeader('X-TIME', timestamp);
			if (secure) {
				if(type =='POST') {
					var hash = CryptoJS.HmacSHA256(localStorage.getItem('user_series')+timestamp+data, localStorage.getItem('user_token')).toString();
				} else {
					var hash = CryptoJS.HmacSHA256(localStorage.getItem('user_series')+timestamp, localStorage.getItem('user_token')).toString();
				}
				jqXHR.setRequestHeader('X-HASH', hash);
				jqXHR.setRequestHeader('X-USER-ID', localStorage.getItem('user_id'));
				jqXHR.setRequestHeader('X-SERIES', localStorage.getItem('user_series'));
			}
			if (notification) {
				alert = $.bootstrapGrowl("Loading...");
			}
		},
		complete: function(jqXHR, textStatus){
			if (notification) {
				alert.alert("close");
			}

			if (textStatus == 'error') {
				try {
					var error_data = $.parseJSON(jqXHR.responseText);
					$.bootstrapGrowl(error_data.error.text, {'delay':2000, 'type':'danger'});
				 } catch (e) {
						$.bootstrapGrowl('Unknown error!', {'delay':2000, 'type':'danger'});
				}
			}
		}
  }, ajaxOptions));
}

//Bind an event to window.onhashchange that, when the hash changes, gets the hash and adds the class "selected" to any matching nav link.
$(window).hashchange( function(){
	var hash = location.hash.slice(1);
	var hash_split = hash.split("/");

	menu_item = hash_split[0];
	if (menu_item =='connections' || menu_item == 'profile') menu_item = 'settings';

	$("li[id^='menu_']").removeClass('active');
	$('#menu_'+menu_item).addClass('active');

	$("li[id^='navbar_menu_']").removeClass('active');
	$('#navbar_menu_'+menu_item).addClass('active');
	$('#collapse_navbar').collapse('hide');

	//Reset
	if (hash == 'reset') {
		Object.keys(localStorage).forEach(function(key){
			localStorage.removeItem(key); //kill all localstorage items
		});
		$("#content").html('');
		document.location.hash = '';
	}

	if (hash_split[0] == 'validate') validate_email(hash_split[1],hash_split[2]);

	if (localStorage.getItem('user_id') === null) {
		//Not logged in
		$('#loginEmail').val("");
		$('#loginPassword').val("");
		$('#username_top').html("");
		$('#username_left').html("");
		$('#loginModal').modal({keyboard: false, backdrop: 'static'});
	} else {
		//Logged in
		$('#username_top').html(localStorage.getItem('user_name'));
		$('#username_left').html(localStorage.getItem('user_name'));
		contacts_get_if_needed(); //Check whether contacts should be obtained, and if so, do so

		if (hash_split[0] == 'transactions') {
			search = '';
			transactions_init();
		} else if (hash == 'send') {
			send_load();
		} else if (hash == 'balance') {
			balance_currencies();
		} else if (hash_split[0] == 'balance') {
			balance_currency(hash_split[1]);
		} else if (hash == 'settings') {
			settings();
		} else if (hash == 'connections') {
			contacts_get(true);
		} else if (hash_split[0] == 'connection'){
			connection_load(hash_split[1]);
		} else if (hash == 'logout') {
			$.ajaxWrapper(
				'logout', //resource
				'POST', //type
				true, //secure
				{}, //data,
				false, //notification
				{} //ajax options
			);

			//Clear local credentials independent of success of logout
			Object.keys(localStorage).forEach(function(key){
				if (/^user_/.test(key)) localStorage.removeItem(key); //kill all localstorage items starting with user_
			});
			$("#content").html('');
			document.location.hash = '';
		} else {
			//default
			document.location.hash = 'balance';
		}
	}
})

//Catch the login form submit
$('#loginForm').submit(function() {
	//do login
	$.ajaxWrapper(
		'login', //resource
		'POST', //type
		false, //secure
		{user: $('#loginEmail').val(), password: $('#loginPassword').val(), provider: 'password'}, //data,
		false, //notification
		{
			success: function(data){
				localStorage.setItem('user_series', data.series);
				localStorage.setItem('user_token', data.token);
				localStorage.setItem('user_id', data.user_id);
				localStorage.setItem('user_name', data.user_name);
				localStorage.setItem('user_default_currency', data.default_currency);
				localStorage.setItem('user_identifiers', JSON.stringify(data.identifiers));
				$.bootstrapGrowl('Logged in', {'delay':2000, 'type':'success'});
				$('#loginModal').modal('hide');
				$(window).hashchange();
			},
			error: function(xhr, errorType, exception) {
				//revert
				pwd_incorrect = false;
				var error_data = $.parseJSON(xhr.responseText);
				if (error_data.error.code == 'incorrect_credentials') pwd_incorrect = true;
				if (pwd_incorrect) {
					$('#loginModal').modal('hide');
					//$('#loginModal').on('hidden.bs.modal', function (e) {
						$('#requestPasswordResetModal').modal({keyboard: false, backdrop: 'static'});
						$('#requestPasswordResetModal').on('hidden.bs.modal', function (e) {
							$('#loginModal').modal({keyboard: false, backdrop: 'static'});
						});
					//});

				} else {
					$.bootstrapGrowl(error_data.error.text, {'delay':2000, 'type':'danger'});
				}
			}
		} //ajax options
		);
	return false;
});

function change_password_submit() {
	$.ajaxWrapper(
		'register/reset_password', //resource
		'POST', //type
		false, //secure
		{identifier: $('#loginEmail').val(), token: $('#resetToken').val(), password: $('#resetPassword').val()}, //data,
		true, //notification
		{
			success: function(data){
				$.bootstrapGrowl("Password successfully changed. Please login with your new password.", {'delay':2000, 'type':'success'});
				$('#resetPasswordModal').modal('hide');

			}
		} //ajax options
	);
}

function change_password_form(sender) {
	$(sender).unbind();
	$(sender).modal('hide');
	$('#resetPasswordModal').modal({keyboard: false, backdrop: 'static'});
	$('#resetPasswordModal').on('hidden.bs.modal', function (e) {
		$('#loginModal').modal({keyboard: false, backdrop: 'static'});
	});
}

function request_reset() {
	$.ajaxWrapper(
		'register/request_reset_password', //resource
		'POST', //type
		false, //secure
		{identifier: $('#loginEmail').val()}, //data,
		true, //notification
		{
			success: function(data){
				$.bootstrapGrowl("Email with token sent", {'delay':2000, 'type':'success'});
				change_password_form('#requestPasswordResetModal');
			}
		} //ajax options
	);
}

//currencies
	var currencies = {
		'AFN':'Afghani',
		'ALL':'Lek',
		'DZD':'Algerian Dinar',
		'USD':'US Dollar',
		'EUR':'Euro',
		'AOA':'Kwanza',
		'XCD':'East Caribbean Dollar',
		'ARS':'Argentine Peso',
		'AMD':'Armenian Dram',
		'AWG':'Aruban Florin',
		'AUD':'Australian Dollar',
		'AZN':'Azerbaijanian Manat',
		'BSD':'Bahamian Dollar',
		'BHD':'Bahraini Dinar',
		'BDT':'Taka',
		'BBD':'Barbados Dollar',
		'BYR':'Belarussian Ruble',
		'BZD':'Belize Dollar',
		'XOF':'CFA Franc BCEAO',
		'BMD':'Bermudian Dollar',
		'BTN':'Ngultrum',
		'BOB':'Boliviano',
		'BAM':'Convertible Mark',
		'BWP':'Pula',
		'NOK':'Norwegian Krone',
		'BRL':'Brazilian Real',
		'BND':'Brunei Dollar',
		'BGN':'Bulgarian Lev',
		'BIF':'Burundi Franc',
		'KHR':'Riel',
		'XAF':'CFA Franc BEAC',
		'CAD':'Canadian Dollar',
		'CVE':'Cape Verde Escudo',
		'KYD':'Cayman Islands Dollar',
		'CLP':'Chilean Peso',
		'CNY':'Yuan Renminbi',
		'COP':'Colombian Peso',
		'KMF':'Comoro Franc',
		'CDF':'Congolais Franc',
		'NZD':'New Zealand Dollar',
		'CRC':'Costa Rican Colon',
		'HRK':'Croatian Kuna',
		'CUP':'Cuban Peso',
		'CUC':'Peso Convertible',
		'ANG':'Netherlands Antillean Guilder',
		'CZK':'Czech Koruna',
		'DKK':'Danish Krone',
		'DJF':'Djibouti Franc',
		'DOP':'Dominican Peso',
		'EGP':'Egyptian Pound',
		'ERN':'Nakfa',
		'ETB':'Ethiopian Birr',
		'FKP':'Falkland Islands Pound',
		'FJD':'Fiji Dollar',
		'XPF':'CFP Franc',
		'GMD':'Dalasi',
		'GEL':'Lari',
		'GHS':'Ghana Cedi',
		'GIP':'Gibraltar Pound',
		'GTQ':'Quetzal',
		'GBP':'Pound Sterling',
		'GNF':'Guinea Franc',
		'GYD':'Guyana Dollar',
		'HTG':'Gourde',
		'HNL':'Lempira',
		'HKD':'Hong Kong Dollar',
		'HUF':'Forint',
		'ISK':'Iceland Krona',
		'INR':'Indian Rupee',
		'IDR':'Rupiah',
		'IRR':'Iranian Rial',
		'IQD':'Iraqi Dinar',
		'ILS':'New Israeli Sheqel',
		'JMD':'Jamaican Dollar',
		'JPY':'Yen',
		'JOD':'Jordanian Dinar',
		'KZT':'Tenge',
		'KES':'Kenyan Shilling',
		'KPW':'North Korean Won',
		'KRW':'Won',
		'KWD':'Kuwaiti Dinar',
		'KGS':'Som',
		'LAK':'Kip',
		'LVL':'Latvian Lats',
		'LBP':'Lebanese Pound',
		'LSL':'Loti',
		'LRD':'Liberian Dollar',
		'LYD':'Libyan Dinar',
		'CHF':'Swiss Franc',
		'LTL':'Lithuanian Litas',
		'MOP':'Pataca',
		'MKD':'Denar',
		'MGA':'Malagasy Ariary',
		'MWK':'Kwacha',
		'MYR':'Malaysian Ringgit',
		'MVR':'Rufiyaa',
		'MRO':'Ouguiya',
		'MUR':'Mauritius Rupee',
		'MXN':'Mexican Peso',
		'MDL':'Moldovan Leu',
		'MNT':'Tugrik',
		'MAD':'Moroccan Dirham',
		'MZN':'Mozambique Metical',
		'MMK':'Kyat',
		'NAD':'Namibia Dollar',
		'NPR':'Nepalese Rupee',
		'NIO':'Cordoba Oro',
		'NGN':'Naira',
		'OMR':'Rial Omani',
		'PKR':'Pakistan Rupee',
		'PAB':'Balboa',
		'PGK':'Kina',
		'PYG':'Guarani',
		'PEN':'Nuevo Sol',
		'PHP':'Philippine Peso',
		'PLN':'Zloty',
		'QAR':'Qatari Rial',
		'RON':'New Romanian Leu',
		'RUB':'Russian Ruble',
		'RWF':'Rwanda Franc',
		'SHP':'Saint Helena Pound',
		'WST':'Tala',
		'STD':'Dobra',
		'SAR':'Saudi Riyal',
		'RSD':'Serbian Dinar',
		'SCR':'Seychelles Rupee',
		'SLL':'Leone',
		'SGD':'Singapore Dollar',
		'SBD':'Solomon Islands Dollar',
		'SOS':'Somali Shilling',
		'ZAR':'Rand',
		'SSP':'South Sudanese Pound',
		'LKR':'Sri Lanka Rupee',
		'SDG':'Sudanese Pound',
		'SRD':'Surinam Dollar',
		'SZL':'Lilangeni',
		'SEK':'Swedish Krona',
		'SYP':'Syrian Pound',
		'TWD':'New Taiwan Dollar',
		'TJS':'Somoni',
		'TZS':'Tanzanian Shilling',
		'THB':'Baht',
		'TOP':'Paʻanga',
		'TTD':'Trinidad and Tobago Dollar',
		'TND':'Tunisian Dinar',
		'TRY':'Turkish Lira',
		'TMT':'Turkmenistan New Manat',
		'UGX':'Uganda Shilling',
		'UAH':'Hryvnia',
		'AED':'UAE Dirham',
		'UYU':'Peso Uruguayo',
		'UZS':'Uzbekistan Sum',
		'VUV':'Vatu',
		'VEF':'Bolivar Fuerte',
		'VND':'Dong',
		'YER':'Yemeni Rial',
		'ZMW':'New Zambian Kwacha',
		'ZWL':'Zimbabwe Dollar'
	};
<commit_msg>fix: focus on email input field<commit_after>$(document).ready(function(){
	//Prep callapse menu
	 $('#collapse_navbar').collapse({'toggle': false});
	//Load content
	$(window).hashchange();
});	//executed after the page has loaded

//AJAX wrapper function
jQuery.ajaxWrapper = function(resource, type, secure, data, notification, ajaxOptions) {
	var alert;
	if (type == 'POST') {
		var data = JSON.stringify(data);
		var contenttype = 'application/json';
	} else {
		var data = '';
		var contenttype = 'application/x-www-form-urlencoded; charset=UTF-8'; //is actually only true for POST requests, but this is default, see http://api.jquery.com/jquery.ajax/
	}
	$.ajax($.extend({
		type: type,
		contentType: contenttype,
		//url: "http://127.0.0.1/api/"+resource, //local
		url: "http://www.settlepad.com/api/"+resource, //web
		dataType: "json",
		//data: {data: data},
		data: data,
		beforeSend: function(jqXHR, settings){
			var timestamp = Math.round((new Date()).getTime() / 1000);
			jqXHR.setRequestHeader('X-TIME', timestamp);
			if (secure) {
				if(type =='POST') {
					var hash = CryptoJS.HmacSHA256(localStorage.getItem('user_series')+timestamp+data, localStorage.getItem('user_token')).toString();
				} else {
					var hash = CryptoJS.HmacSHA256(localStorage.getItem('user_series')+timestamp, localStorage.getItem('user_token')).toString();
				}
				jqXHR.setRequestHeader('X-HASH', hash);
				jqXHR.setRequestHeader('X-USER-ID', localStorage.getItem('user_id'));
				jqXHR.setRequestHeader('X-SERIES', localStorage.getItem('user_series'));
			}
			if (notification) {
				alert = $.bootstrapGrowl("Loading...");
			}
		},
		complete: function(jqXHR, textStatus){
			if (notification) {
				alert.alert("close");
			}

			if (textStatus == 'error') {
				try {
					var error_data = $.parseJSON(jqXHR.responseText);
					$.bootstrapGrowl(error_data.error.text, {'delay':2000, 'type':'danger'});
				 } catch (e) {
						$.bootstrapGrowl('Unknown error!', {'delay':2000, 'type':'danger'});
				}
			}
		}
  }, ajaxOptions));
}

//Bind an event to window.onhashchange that, when the hash changes, gets the hash and adds the class "selected" to any matching nav link.
$(window).hashchange( function(){
	var hash = location.hash.slice(1);
	var hash_split = hash.split("/");

	menu_item = hash_split[0];
	if (menu_item =='connections' || menu_item == 'profile') menu_item = 'settings';

	$("li[id^='menu_']").removeClass('active');
	$('#menu_'+menu_item).addClass('active');

	$("li[id^='navbar_menu_']").removeClass('active');
	$('#navbar_menu_'+menu_item).addClass('active');
	$('#collapse_navbar').collapse('hide');

	//Reset
	if (hash == 'reset') {
		Object.keys(localStorage).forEach(function(key){
			localStorage.removeItem(key); //kill all localstorage items
		});
		$("#content").html('');
		document.location.hash = '';
	}

	if (hash_split[0] == 'validate') validate_email(hash_split[1],hash_split[2]);

	if (localStorage.getItem('user_id') === null) {
		//Not logged in
		$('#loginEmail').val("");
		$('#loginPassword').val("");
		$('#username_top').html("");
		$('#username_left').html("");
		$('#loginModal').modal({keyboard: false, backdrop: 'static'});
		$('#loginEmail').focus();
	} else {
		//Logged in
		$('#username_top').html(localStorage.getItem('user_name'));
		$('#username_left').html(localStorage.getItem('user_name'));
		contacts_get_if_needed(); //Check whether contacts should be obtained, and if so, do so

		if (hash_split[0] == 'transactions') {
			search = '';
			transactions_init();
		} else if (hash == 'send') {
			send_load();
		} else if (hash == 'balance') {
			balance_currencies();
		} else if (hash_split[0] == 'balance') {
			balance_currency(hash_split[1]);
		} else if (hash == 'settings') {
			settings();
		} else if (hash == 'connections') {
			contacts_get(true);
		} else if (hash_split[0] == 'connection'){
			connection_load(hash_split[1]);
		} else if (hash == 'logout') {
			$.ajaxWrapper(
				'logout', //resource
				'POST', //type
				true, //secure
				{}, //data,
				false, //notification
				{} //ajax options
			);

			//Clear local credentials independent of success of logout
			Object.keys(localStorage).forEach(function(key){
				if (/^user_/.test(key)) localStorage.removeItem(key); //kill all localstorage items starting with user_
			});
			$("#content").html('');
			document.location.hash = '';
		} else {
			//default
			document.location.hash = 'balance';
		}
	}
})

//Catch the login form submit
$('#loginForm').submit(function() {
	//do login
	$.ajaxWrapper(
		'login', //resource
		'POST', //type
		false, //secure
		{user: $('#loginEmail').val(), password: $('#loginPassword').val(), provider: 'password'}, //data,
		false, //notification
		{
			success: function(data){
				localStorage.setItem('user_series', data.series);
				localStorage.setItem('user_token', data.token);
				localStorage.setItem('user_id', data.user_id);
				localStorage.setItem('user_name', data.user_name);
				localStorage.setItem('user_default_currency', data.default_currency);
				localStorage.setItem('user_identifiers', JSON.stringify(data.identifiers));
				$.bootstrapGrowl('Logged in', {'delay':2000, 'type':'success'});
				$('#loginModal').modal('hide');
				$(window).hashchange();
			},
			error: function(xhr, errorType, exception) {
				//revert
				pwd_incorrect = false;
				var error_data = $.parseJSON(xhr.responseText);
				if (error_data.error.code == 'incorrect_credentials') pwd_incorrect = true;
				if (pwd_incorrect) {
					$('#loginModal').modal('hide');
					//$('#loginModal').on('hidden.bs.modal', function (e) {
						$('#requestPasswordResetModal').modal({keyboard: false, backdrop: 'static'});
						$('#requestPasswordResetModal').on('hidden.bs.modal', function (e) {
							$('#loginModal').modal({keyboard: false, backdrop: 'static'});
						});
					//});

				} else {
					$.bootstrapGrowl(error_data.error.text, {'delay':2000, 'type':'danger'});
				}
			}
		} //ajax options
		);
	return false;
});

function change_password_submit() {
	$.ajaxWrapper(
		'register/reset_password', //resource
		'POST', //type
		false, //secure
		{identifier: $('#loginEmail').val(), token: $('#resetToken').val(), password: $('#resetPassword').val()}, //data,
		true, //notification
		{
			success: function(data){
				$.bootstrapGrowl("Password successfully changed. Please login with your new password.", {'delay':2000, 'type':'success'});
				$('#resetPasswordModal').modal('hide');

			}
		} //ajax options
	);
}

function change_password_form(sender) {
	$(sender).unbind();
	$(sender).modal('hide');
	$('#resetPasswordModal').modal({keyboard: false, backdrop: 'static'});
	$('#resetPasswordModal').on('hidden.bs.modal', function (e) {
		$('#loginModal').modal({keyboard: false, backdrop: 'static'});
	});
}

function request_reset() {
	$.ajaxWrapper(
		'register/request_reset_password', //resource
		'POST', //type
		false, //secure
		{identifier: $('#loginEmail').val()}, //data,
		true, //notification
		{
			success: function(data){
				$.bootstrapGrowl("Email with token sent", {'delay':2000, 'type':'success'});
				change_password_form('#requestPasswordResetModal');
			}
		} //ajax options
	);
}

//currencies
	var currencies = {
		'AFN':'Afghani',
		'ALL':'Lek',
		'DZD':'Algerian Dinar',
		'USD':'US Dollar',
		'EUR':'Euro',
		'AOA':'Kwanza',
		'XCD':'East Caribbean Dollar',
		'ARS':'Argentine Peso',
		'AMD':'Armenian Dram',
		'AWG':'Aruban Florin',
		'AUD':'Australian Dollar',
		'AZN':'Azerbaijanian Manat',
		'BSD':'Bahamian Dollar',
		'BHD':'Bahraini Dinar',
		'BDT':'Taka',
		'BBD':'Barbados Dollar',
		'BYR':'Belarussian Ruble',
		'BZD':'Belize Dollar',
		'XOF':'CFA Franc BCEAO',
		'BMD':'Bermudian Dollar',
		'BTN':'Ngultrum',
		'BOB':'Boliviano',
		'BAM':'Convertible Mark',
		'BWP':'Pula',
		'NOK':'Norwegian Krone',
		'BRL':'Brazilian Real',
		'BND':'Brunei Dollar',
		'BGN':'Bulgarian Lev',
		'BIF':'Burundi Franc',
		'KHR':'Riel',
		'XAF':'CFA Franc BEAC',
		'CAD':'Canadian Dollar',
		'CVE':'Cape Verde Escudo',
		'KYD':'Cayman Islands Dollar',
		'CLP':'Chilean Peso',
		'CNY':'Yuan Renminbi',
		'COP':'Colombian Peso',
		'KMF':'Comoro Franc',
		'CDF':'Congolais Franc',
		'NZD':'New Zealand Dollar',
		'CRC':'Costa Rican Colon',
		'HRK':'Croatian Kuna',
		'CUP':'Cuban Peso',
		'CUC':'Peso Convertible',
		'ANG':'Netherlands Antillean Guilder',
		'CZK':'Czech Koruna',
		'DKK':'Danish Krone',
		'DJF':'Djibouti Franc',
		'DOP':'Dominican Peso',
		'EGP':'Egyptian Pound',
		'ERN':'Nakfa',
		'ETB':'Ethiopian Birr',
		'FKP':'Falkland Islands Pound',
		'FJD':'Fiji Dollar',
		'XPF':'CFP Franc',
		'GMD':'Dalasi',
		'GEL':'Lari',
		'GHS':'Ghana Cedi',
		'GIP':'Gibraltar Pound',
		'GTQ':'Quetzal',
		'GBP':'Pound Sterling',
		'GNF':'Guinea Franc',
		'GYD':'Guyana Dollar',
		'HTG':'Gourde',
		'HNL':'Lempira',
		'HKD':'Hong Kong Dollar',
		'HUF':'Forint',
		'ISK':'Iceland Krona',
		'INR':'Indian Rupee',
		'IDR':'Rupiah',
		'IRR':'Iranian Rial',
		'IQD':'Iraqi Dinar',
		'ILS':'New Israeli Sheqel',
		'JMD':'Jamaican Dollar',
		'JPY':'Yen',
		'JOD':'Jordanian Dinar',
		'KZT':'Tenge',
		'KES':'Kenyan Shilling',
		'KPW':'North Korean Won',
		'KRW':'Won',
		'KWD':'Kuwaiti Dinar',
		'KGS':'Som',
		'LAK':'Kip',
		'LVL':'Latvian Lats',
		'LBP':'Lebanese Pound',
		'LSL':'Loti',
		'LRD':'Liberian Dollar',
		'LYD':'Libyan Dinar',
		'CHF':'Swiss Franc',
		'LTL':'Lithuanian Litas',
		'MOP':'Pataca',
		'MKD':'Denar',
		'MGA':'Malagasy Ariary',
		'MWK':'Kwacha',
		'MYR':'Malaysian Ringgit',
		'MVR':'Rufiyaa',
		'MRO':'Ouguiya',
		'MUR':'Mauritius Rupee',
		'MXN':'Mexican Peso',
		'MDL':'Moldovan Leu',
		'MNT':'Tugrik',
		'MAD':'Moroccan Dirham',
		'MZN':'Mozambique Metical',
		'MMK':'Kyat',
		'NAD':'Namibia Dollar',
		'NPR':'Nepalese Rupee',
		'NIO':'Cordoba Oro',
		'NGN':'Naira',
		'OMR':'Rial Omani',
		'PKR':'Pakistan Rupee',
		'PAB':'Balboa',
		'PGK':'Kina',
		'PYG':'Guarani',
		'PEN':'Nuevo Sol',
		'PHP':'Philippine Peso',
		'PLN':'Zloty',
		'QAR':'Qatari Rial',
		'RON':'New Romanian Leu',
		'RUB':'Russian Ruble',
		'RWF':'Rwanda Franc',
		'SHP':'Saint Helena Pound',
		'WST':'Tala',
		'STD':'Dobra',
		'SAR':'Saudi Riyal',
		'RSD':'Serbian Dinar',
		'SCR':'Seychelles Rupee',
		'SLL':'Leone',
		'SGD':'Singapore Dollar',
		'SBD':'Solomon Islands Dollar',
		'SOS':'Somali Shilling',
		'ZAR':'Rand',
		'SSP':'South Sudanese Pound',
		'LKR':'Sri Lanka Rupee',
		'SDG':'Sudanese Pound',
		'SRD':'Surinam Dollar',
		'SZL':'Lilangeni',
		'SEK':'Swedish Krona',
		'SYP':'Syrian Pound',
		'TWD':'New Taiwan Dollar',
		'TJS':'Somoni',
		'TZS':'Tanzanian Shilling',
		'THB':'Baht',
		'TOP':'Paʻanga',
		'TTD':'Trinidad and Tobago Dollar',
		'TND':'Tunisian Dinar',
		'TRY':'Turkish Lira',
		'TMT':'Turkmenistan New Manat',
		'UGX':'Uganda Shilling',
		'UAH':'Hryvnia',
		'AED':'UAE Dirham',
		'UYU':'Peso Uruguayo',
		'UZS':'Uzbekistan Sum',
		'VUV':'Vatu',
		'VEF':'Bolivar Fuerte',
		'VND':'Dong',
		'YER':'Yemeni Rial',
		'ZMW':'New Zambian Kwacha',
		'ZWL':'Zimbabwe Dollar'
	};
<|endoftext|>


<commit_before>
import YouTubeApiClient from "sf4yt-gapi-client/es2015/YouTubeApiClient"
import Account from "../../model/Account"
import AccountState from "../../model/AccountState"
import Playlist from "../../model/Playlist"
import Subscription from "../../model/Subscription"
import SubscriptionState from "../../model/SubscriptionState"
import SubscriptionType from "../../model/SubscriptionType"

const PRIVATE = Object.freeze({
  apiClient: Symbol("apiClient")
})

export default class Client {
  constructor(apiClient: YouTubeApiClient) {
    this[PRIVATE.apiClient] = apiClient
  }

  async getAccountInfo(accountId: string, channelId: ?string = null): Account {
    let accountData = await this[PRIVATE.apiClient].getAccountInfo(channelId)
    return new Account({
      id: accountId,
      channelId: accountData.id,
      title: accountData.title,
      state: AccountState.ACTIVE,
      lastError: null,
      watchHistoryPlaylistId: accountData.playlistIds.watchHistory,
      watchLaterPlaylistId: accountData.playlistIds.watchLater
    })
  }

  async getSubscriptions(account: Account, authorized: boolean = false):
      Array<Subscription> {
    let apiClient = this[PRIVATE.apiClient]
    let channels = await apiClient.getSubscribedChannels(
      account.channelId,
      authorized
    )
    let channelIds = channels.map(channel => channel.id)

    let uploadPlaylistIds = await apiClient.getUploadsPlaylistIds(
      channelIds
    )
    let playlistIdsMap = new Map()
    for (let playlistId of uploadPlaylistIds) {
      playlistIdsMap.set(playlistId.id, playlistId.uploadsPlaylistId)
    }

    for (let channel of channels) {
      channel.uploadsPlaylistId = playlistIdsMap.get(channel.id)
    }

    return channels.map((channel) => {
      return new Subscription({
        type: SubscriptionType.CHANNEL,
        playlistId: channel.uploadsPlaylistId,
        channelId: channel.id,
        state: SubscriptionState.ACTIVE,
        lastError: null,
        accountId: account.id,
        isIncognito: 0
      })
    })
  }

  async resolveIncognitoSubscription(url: string): Subscription {
    let validator = /^https:\/\/www\.youtube\.com\/(channel\/|user\/|playlist\?(.+&)?list=|watch\?(.+&)?list=).+$/
    if (!validator.test(url)) {
      throw new Error("Invalid user, channel or playlist URL")
    }

    let apiClient = this[PRIVATE.apiClient]
    let channelId = null
    let subscriptionType = SubscriptionType.PLAYLIST
    let userMatcher = /^https:\/\/www\.youtube\.com\/user\/([^/]+)$/
    let channelMatcher = /^https:\/\/www\.youtube\.com\/channel\/([^/]+)$/
    if (userMatcher.test(url)) {
      let username = decodeURIComponent(userMatcher.exec(url)[1])
      channelId = await apiClient.getUserChannelId(username)
    } else if (channelMatcher.test(url)) {
      channelId = decodeURIComponent(channelMatcher.exec(url)[1])
    }

    let playlistMatcher = /^https:\/\/www\.youtube\.com\/(?:playlist|watch)\?(?:.+&)?list=([^&]+)(?:&.+)?$/
    let playlistId
    if (channelId) {
      subscriptionType = SubscriptionType.CHANNEL
      playlistId = await apiClient.getUploadsPlaylistId(channelId)
    } else {
      playlistId = decodeURIComponent(playlistMatcher.exec(url)[1])
      let playlistInfo = await apiClient.getPlaylistInfo(playlistId)
      channelId = playlistInfo.channelId
    }

    return new Subscription({
      type: subscriptionType,
      playlistId,
      channelId,
      state: SubscriptionState.ACTIVE,
      lastError: null,
      accountId: null,
      isIncognito: 1
    })
  }

  async getPlaylistsWithNewContent(playlists: Array<Playlist>):
      Array<Playlist> {
    let playlistsMap = new Map()
    for (let playlist of playlists) {
      playlistsMap.set(playlist.id, playlist)
    }

    let playlistIds = Array.from(playlistsMap.keys())
    let apiClient = this[PRIVATE.apiClient]
    let videoCounts = await apiClient.getPlaylistVideoCounts(playlistIds)

    let updated = []
    for (let videoCount of videoCounts) {
      let playlist = playlistsMap.get(videoCount.id)
      if (playlist.videoCount !== videoCount.videoCount) {
        playlist.videoCount = videoCount.videoCount
        updated.push(playlist)
      }
    }

    return updated
  }
}
<commit_msg>implemented fetching new videos from a playlist<commit_after>
import YouTubeApiClient from "sf4yt-gapi-client/es2015/YouTubeApiClient"
import Account from "../../model/Account"
import AccountState from "../../model/AccountState"
import Playlist from "../../model/Playlist"
import Subscription from "../../model/Subscription"
import SubscriptionState from "../../model/SubscriptionState"
import SubscriptionType from "../../model/SubscriptionType"
import Video from "../../model/Video"

const PRIVATE = Object.freeze({
  apiClient: Symbol("apiClient")
})

export default class Client {
  constructor(apiClient: YouTubeApiClient) {
    this[PRIVATE.apiClient] = apiClient
  }

  async getAccountInfo(accountId: string, channelId: ?string = null): Account {
    let accountData = await this[PRIVATE.apiClient].getAccountInfo(channelId)
    return new Account({
      id: accountId,
      channelId: accountData.id,
      title: accountData.title,
      state: AccountState.ACTIVE,
      lastError: null,
      watchHistoryPlaylistId: accountData.playlistIds.watchHistory,
      watchLaterPlaylistId: accountData.playlistIds.watchLater
    })
  }

  async getSubscriptions(account: Account, authorized: boolean = false):
      Array<Subscription> {
    let apiClient = this[PRIVATE.apiClient]
    let channels = await apiClient.getSubscribedChannels(
      account.channelId,
      authorized
    )
    let channelIds = channels.map(channel => channel.id)

    let uploadPlaylistIds = await apiClient.getUploadsPlaylistIds(
      channelIds
    )
    let playlistIdsMap = new Map()
    for (let playlistId of uploadPlaylistIds) {
      playlistIdsMap.set(playlistId.id, playlistId.uploadsPlaylistId)
    }

    for (let channel of channels) {
      channel.uploadsPlaylistId = playlistIdsMap.get(channel.id)
    }

    return channels.map((channel) => {
      return new Subscription({
        type: SubscriptionType.CHANNEL,
        playlistId: channel.uploadsPlaylistId,
        channelId: channel.id,
        state: SubscriptionState.ACTIVE,
        lastError: null,
        accountId: account.id,
        isIncognito: 0
      })
    })
  }

  async resolveIncognitoSubscription(url: string): Subscription {
    let validator = /^https:\/\/www\.youtube\.com\/(channel\/|user\/|playlist\?(.+&)?list=|watch\?(.+&)?list=).+$/
    if (!validator.test(url)) {
      throw new Error("Invalid user, channel or playlist URL")
    }

    let apiClient = this[PRIVATE.apiClient]
    let channelId = null
    let subscriptionType = SubscriptionType.PLAYLIST
    let userMatcher = /^https:\/\/www\.youtube\.com\/user\/([^/]+)$/
    let channelMatcher = /^https:\/\/www\.youtube\.com\/channel\/([^/]+)$/
    if (userMatcher.test(url)) {
      let username = decodeURIComponent(userMatcher.exec(url)[1])
      channelId = await apiClient.getUserChannelId(username)
    } else if (channelMatcher.test(url)) {
      channelId = decodeURIComponent(channelMatcher.exec(url)[1])
    }

    let playlistMatcher = /^https:\/\/www\.youtube\.com\/(?:playlist|watch)\?(?:.+&)?list=([^&]+)(?:&.+)?$/
    let playlistId
    if (channelId) {
      subscriptionType = SubscriptionType.CHANNEL
      playlistId = await apiClient.getUploadsPlaylistId(channelId)
    } else {
      playlistId = decodeURIComponent(playlistMatcher.exec(url)[1])
      let playlistInfo = await apiClient.getPlaylistInfo(playlistId)
      channelId = playlistInfo.channelId
    }

    return new Subscription({
      type: subscriptionType,
      playlistId,
      channelId,
      state: SubscriptionState.ACTIVE,
      lastError: null,
      accountId: null,
      isIncognito: 1
    })
  }

  async getPlaylistsWithNewContent(playlists: Array<Playlist>):
      Array<Playlist> {
    let playlistsMap = new Map()
    for (let playlist of playlists) {
      playlistsMap.set(playlist.id, playlist)
    }

    let playlistIds = Array.from(playlistsMap.keys())
    let apiClient = this[PRIVATE.apiClient]
    let videoCounts = await apiClient.getPlaylistVideoCounts(playlistIds)

    let updated = []
    for (let videoCount of videoCounts) {
      let playlist = playlistsMap.get(videoCount.id)
      if (playlist.videoCount !== videoCount.videoCount) {
        playlist.videoCount = videoCount.videoCount
        updated.push(playlist)
      }
    }

    return updated
  }

  async getNewPlaylistVideos(playlist: Playlist, knownVideos: Array<Video>,
      authorized: boolean = false): Array<Video> {
    if (knownVideos.length === playlist.videoCount) {
      return []
    }

    let knownVideoIds = new Set(knownVideos.map(video => video.id))
    let newVideosCount = playlist.videoCount - knownVideos.length

    let apiClient = this[PRIVATE.apiClient]
    let videos = await apiClient.getPlaylistVideos(playlist.id, (videos) => {
      for (let video of videos) {
        let videoId = video.resourceId.videoId
        if (knownVideoIds.has(videoId)) {
          newVideosCount--
        }
        if (!newVideosCount) {
          break
        }
      }

      return !!newVideosCount
    })
    let newVideos = videos.filter(video => !knownVideoIds.has(video.id))
    let videoEntities = new Map()
    for (let video of newVideos) {
      videoEntities.set(video.id, new Video(Object.assign({}, video, {
        duration: -1,
        viewCount: -1,
        accountIds: playlist.accountIds.slice(),
        incognitoSubscriptionIds: playlist.incognitoSubscriptionIds.slice(),
        watched: 0
      })))
    }

    let newVideoIds = Array.from(videoEntities.keys())
    let metadata = await apiClient.getVideosMetaData(newVideoIds)
    for (let data of metadata) {
      let entity = videoEntities.get(data.id)
      entity.duration = data.duration
      entity.viewCount = data.viewCount
    }

    return Array.from(videoEntities.values())
  }
}
<|endoftext|>


<commit_before>/* eslint-disable no-eval */

const LiteralType = require('./types/LiteralType').LiteralType
const ArrayType = require('./types/ArrayType').ArrayType
const ObjectType = require('./types/ObjectType').ObjectType
const AnyType = require('./types/AnyType').AnyType
const PolymorphicType = require('./types/PolymorphicType').PolymorphicType
const RepeatedType = require('./types/RepeatedType').RepeatedType

/*
 * Number -> LiteralType(Number)
 * [Number, [String, Number]] -> ArrayType(LiteralType(Number), ArrayType ...)
 * ^^ so it's recursive
 */
exports.wrapType = function (constructor) {
  if (constructor.constructor === Array) {
    return new ArrayType(...constructor.map(exports.wrapType))
  } else if (constructor.constructor === Object) {
    let obj = {}

    for (var prop in constructor) {
      if (constructor.hasOwnProperty(prop)) {
        obj[prop] = exports.wrapType(constructor[prop])
      }
    }

    return new ObjectType(obj)
  } else {
    return new LiteralType(constructor)
  }
}

/*
 * 5 -> LiteralType(Number)
 * [1, [2, 3]] -> ArrayType(LiteralType(Number), ArrayType(LiteralType(Number), LiteralType(Number)))
 * ^^ so it's recursive
 */
exports.wrapValue = function (val) {
  const constructor = val.constructor

  if (constructor === Array) {
    return new ArrayType(...val.map(exports.wrapValue))
  } else if (constructor === Object) {
    let obj = {}

    for (var prop in val) {
      if (val.hasOwnProperty(prop)) {
        obj[prop] = exports.wrapValue(val[prop])
      }
    }

    return new ObjectType(obj)
  } else {
    return new LiteralType(constructor)
  }
}

/*
 * Takes an AST and returns a JS value representing it
 */
exports.getType = function (AST, aliases) {
  const type = AST.type // e.g. 'Identifier', 'ArrayExpression', etc...

  switch (type) {
    case 'Identifier':
      for (let alias of aliases) {
        if (alias.name === AST.name) {
          return exports.getType(alias.AST, aliases)
        }
      }

      if (AST.name === 'Any') {
        return new AnyType()
      }

      return exports.wrapType(eval(AST.name))
    case 'ArrayExpression':
      let arr = AST.elements.map((elem) => exports.getType(elem, aliases))

      return new ArrayType(...arr)
    case 'ObjectExpression':
      let obj = {}

      for (let prop of AST.properties) {
        assert(prop.key.type === 'Identifier', 'Parse', 'Expected an identifier for every key')

        obj[prop.key.name] = exports.getType(prop.value, aliases)
      }
      return new ObjectType(obj)
    case 'CallExpression':
      assert(AST.callee.type === 'Identifier', 'Parse', 'Expected an identifier for the callee\'s name')
      if (AST.callee.name === 'Any') {
        assert(AST.arguments.length > 0, 'Parse', 'Expected at least one constraint for \'Any\'')
        return new PolymorphicType(...AST.arguments.map((type) => exports.getType(type, aliases)))
      } else if (AST.callee.name === 'Repeat') {
        assert(AST.arguments.length > 0 && AST.arguments.length < 3, 'Parse', 'Expected one or two arguments to \'Repeat\'')

        const constraint = exports.getType(AST.arguments[0], aliases)

        if (AST.arguments.length === 2) {
          const amountArg = AST.arguments[1]
          assert(amountArg.type === 'Literal', 'Parse', 'Expected a literal as the second argument to \'Repeat\'')
            .and(parseInt(amountArg.value) === amountArg.value, 'Parse', 'Expected an integer as the second argument to \'Repeat\'')

          const amount = parseInt(amountArg.value)
          const arr = []

          for (let i = 0; i < amount; i++) {
            arr.push(constraint)
          }

          return new ArrayType(...arr)
        } else {
          return new RepeatedType(constraint)
        }
      } else {
        throw new Error(`Invalid constraint function call, ${AST.callee.name}`)
      }
    default:
      throw new Error(`Invalid constraint type, ${type}!`)
  }
}

/* eslint-enable no-eval */
<commit_msg>Require assert<commit_after>/* eslint-disable no-eval */

const LiteralType = require('./types/LiteralType').LiteralType
const ArrayType = require('./types/ArrayType').ArrayType
const ObjectType = require('./types/ObjectType').ObjectType
const AnyType = require('./types/AnyType').AnyType
const PolymorphicType = require('./types/PolymorphicType').PolymorphicType
const RepeatedType = require('./types/RepeatedType').RepeatedType

const assert = require('./errors').assert

/*
 * Number -> LiteralType(Number)
 * [Number, [String, Number]] -> ArrayType(LiteralType(Number), ArrayType ...)
 * ^^ so it's recursive
 */
exports.wrapType = function (constructor) {
  if (constructor.constructor === Array) {
    return new ArrayType(...constructor.map(exports.wrapType))
  } else if (constructor.constructor === Object) {
    let obj = {}

    for (var prop in constructor) {
      if (constructor.hasOwnProperty(prop)) {
        obj[prop] = exports.wrapType(constructor[prop])
      }
    }

    return new ObjectType(obj)
  } else {
    return new LiteralType(constructor)
  }
}

/*
 * 5 -> LiteralType(Number)
 * [1, [2, 3]] -> ArrayType(LiteralType(Number), ArrayType(LiteralType(Number), LiteralType(Number)))
 * ^^ so it's recursive
 */
exports.wrapValue = function (val) {
  const constructor = val.constructor

  if (constructor === Array) {
    return new ArrayType(...val.map(exports.wrapValue))
  } else if (constructor === Object) {
    let obj = {}

    for (var prop in val) {
      if (val.hasOwnProperty(prop)) {
        obj[prop] = exports.wrapValue(val[prop])
      }
    }

    return new ObjectType(obj)
  } else {
    return new LiteralType(constructor)
  }
}

/*
 * Takes an AST and returns a JS value representing it
 */
exports.getType = function (AST, aliases) {
  const type = AST.type // e.g. 'Identifier', 'ArrayExpression', etc...

  switch (type) {
    case 'Identifier':
      for (let alias of aliases) {
        if (alias.name === AST.name) {
          return exports.getType(alias.AST, aliases)
        }
      }

      if (AST.name === 'Any') {
        return new AnyType()
      }

      return exports.wrapType(eval(AST.name))
    case 'ArrayExpression':
      let arr = AST.elements.map((elem) => exports.getType(elem, aliases))

      return new ArrayType(...arr)
    case 'ObjectExpression':
      let obj = {}

      for (let prop of AST.properties) {
        assert(prop.key.type === 'Identifier', 'Parse', 'Expected an identifier for every key')

        obj[prop.key.name] = exports.getType(prop.value, aliases)
      }
      return new ObjectType(obj)
    case 'CallExpression':
      assert(AST.callee.type === 'Identifier', 'Parse', 'Expected an identifier for the callee\'s name')
      if (AST.callee.name === 'Any') {
        assert(AST.arguments.length > 0, 'Parse', 'Expected at least one constraint for \'Any\'')
        return new PolymorphicType(...AST.arguments.map((type) => exports.getType(type, aliases)))
      } else if (AST.callee.name === 'Repeat') {
        assert(AST.arguments.length > 0 && AST.arguments.length < 3, 'Parse', 'Expected one or two arguments to \'Repeat\'')

        const constraint = exports.getType(AST.arguments[0], aliases)

        if (AST.arguments.length === 2) {
          const amountArg = AST.arguments[1]
          assert(amountArg.type === 'Literal', 'Parse', 'Expected a literal as the second argument to \'Repeat\'')
            .and(parseInt(amountArg.value) === amountArg.value, 'Parse', 'Expected an integer as the second argument to \'Repeat\'')

          const amount = parseInt(amountArg.value)
          const arr = []

          for (let i = 0; i < amount; i++) {
            arr.push(constraint)
          }

          return new ArrayType(...arr)
        } else {
          return new RepeatedType(constraint)
        }
      } else {
        throw new Error(`Invalid constraint function call, ${AST.callee.name}`)
      }
    default:
      throw new Error(`Invalid constraint type, ${type}!`)
  }
}

/* eslint-enable no-eval */
<|endoftext|>


<commit_before>
require('./base');
import {
	Box,
	Button,
	Card,
	Checkbox, CssBaseline,
	Icon,
	IconButton,
	Input,
	Link,
	List,
	ListItem,
	MuiThemeProvider,
	Typography
} from '@material-ui/core';
var React = require('react');
var Rx = require('./rx').default;

var {testHost} = require('./xenaQuery');
var _ = require('./underscore_ext').default;
var {servers: {localHub}, serverNames} = require('./defaultServers');
var {parseServer, getHubParams} = require('./hubParams');
import nav from './nav';
import XTypography, {XTypographyVariants} from './views/XTypography';
import {xenaColor} from './xenaColor';
import {xenaTheme} from './xenaTheme';
var {encodeObject} = require('./util').default;

// Styles
var styles = require('./hubPage.module.css');
var sxHubItem = {
	gap: 16,
	'&:hover': {
		backgroundColor: xenaColor.BLACK_3
	},
};
var sxStatusContainer = {
	borderRadius: 24,
	minWidth: 100,
};

var RETURN = 13;

var getName = h => _.get(serverNames, h, h);

var getStatus = (user, ping) =>
	user ? (ping === true ? 'connected' : 'selected') : '';

var isStatusConnected = statusStr => statusStr === 'connected';

var reqStatus = (ping) =>
	ping == null ? ' (connecting...)' :
				(ping ? '' : ' (not running)');

var checkHost = host => testHost(host).take(1).map(v => ({[host]: v}));

var Hub = class extends React.Component {
	static displayName = 'Hub';

	state = {
		ping: {}
	};

	onNavigate = (page, params) => {
		this.props.callback(['navigate', page, params]);
	};

	componentDidMount() {
		// XXX Use a connector to get rid of selector, here.
		// Or use a sub-component.
		var {state, selector} = this.props,
			allHosts = _.keys(selector(state));

		nav({activeLink: 'hub', onNavigate: this.onNavigate});
		this.ping = new Rx.Subject();

		this.sub = this.ping.switchMapTo(Rx.Observable.from(allHosts.map(checkHost)).mergeAll())
			.subscribe(this.updatePing);

		this.ping.next();
	}

	onStartup = () => {
		this.ping.next();
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	UNSAFE_componentWillReceiveProps(newProps) {//eslint-disable-line camelcase
		var {ping} = this.state,
			{state, selector} = newProps,
			servers = selector(state),
			old = _.omit(ping, _.keys(servers));

		this.setState({ping: _.omit(ping, old)});

		_.difference(_.keys(servers), _.keys(ping))
			.forEach(h => checkHost(h).subscribe(this.updatePing));
	}

	updatePing = (h) => {
		this.setState({ping: {...this.state.ping, ...h}});
	};

	onKeyDown = (ev) => {
		if (ev.keyCode === RETURN) {
			ev.preventDefault();
			this.onAdd();
		}
	};

	onSelect = (ev) => {
		var {checked} = ev.target,
			host = ev.target.getAttribute('data-host');
		this.props.callback([checked ? 'enable-host' : 'disable-host', host, 'user']);
	};

	onAdd = () => {
		var target = this.newHost,
			value = target.value.trim();
		if (value !== '') {
			this.props.callback(['add-host', parseServer(value)]);
			target.value = '';
		}
	};

	onRemove = (ev) => {
		var host = ev.currentTarget.getAttribute('data-host');
		this.props.callback(['remove-host', host]);
	};

	setAddHubInputRef = ref => this.newHost = ref;

	render() {
		var {state, selector, badge} = this.props,
			hubParams = getHubParams(state),
			{ping} = this.state,
			servers = selector(state),
			hostList = _.mapObject(servers, (s, h) => ({
				selected: s.user,
				host: h,
				name: getName(h),
				statusStr: getStatus(s.user, ping[h]),
				reqStatus: reqStatus(ping[h])
			}));
		return (

				<XTypography component='h1' variant={XTypographyVariants.MD_HEADLINE}>Data Hubs</XTypography>
				<Card elevation={2}>
					<List disablePadding>
						{_.values(hostList).map(h => (
						<Box component={ListItem} divider key={h.host} sx={sxHubItem}>
							<Checkbox checked={h.selected || false} inputProps={{'data-host': h.host}} onChange={this.onSelect}/>
							<Box
								bgcolor={isStatusConnected(h.statusStr) ? 'secondary.main' : xenaColor.BLACK_6}
								color={isStatusConnected(h.statusStr) ? 'primary.contrastText' : undefined}
								component='span'
								sx={sxStatusContainer}>
								<Typography align='center' className={styles.status} display='block' variant='caption'>{h.statusStr}</Typography>
							</Box>
							<Box flex={1}>
								<Link href={`../datapages/?${encodeObject({
									host: h.host,
									addHub: hubParams.addHub,
									removeHub: hubParams.removeHub.filter( hub => hub !== h.host)})}`} variant='body1'>
									{h.name}
								</Link>
								<Typography display='inline' variant='body1'>{h.host === localHub ? badge : h.reqStatus}</Typography>
							</Box>
							<IconButton data-host={h.host} edge='end' onClick={this.onRemove}>
								<Icon className={styles.remove}>close</Icon>
							</IconButton>
						</Box>
						))}
						<Box component={ListItem} sx={{gap: 16}}>
							<Box sx={{marginLeft: 150, width: 400}}>
								<Input fullWidth inputRef={this.setAddHubInputRef} onKeyDown={this.onKeyDown} placeholder='Add Hub' type='text'/>
							</Box>
							<Button onClick={this.onAdd}>Add</Button>
						</Box>
					</List>
				</Card>

);
	}
};

var selector = state => state.spreadsheet.servers;

class ThemedHub extends React.Component {
	render() {
		return (
		<MuiThemeProvider theme={xenaTheme}>
			<CssBaseline/>
			<Hub {...this.props} selector={selector}/>
		</MuiThemeProvider>);
	}
}

export default ThemedHub;
<commit_msg>Convert 'Hub' and 'Hub Page' components to MUI. Removed theme provider from hub page component. #508.<commit_after>
require('./base');
import {
	Box,
	Button,
	Card,
	Checkbox,
	Icon,
	IconButton,
	Input,
	Link,
	List,
	ListItem,
	Typography
} from '@material-ui/core';
var React = require('react');
var Rx = require('./rx').default;

var {testHost} = require('./xenaQuery');
var _ = require('./underscore_ext').default;
var {servers: {localHub}, serverNames} = require('./defaultServers');
var {parseServer, getHubParams} = require('./hubParams');
import nav from './nav';
import XTypography, {XTypographyVariants} from './views/XTypography';
import {xenaColor} from './xenaColor';
var {encodeObject} = require('./util').default;

// Styles
var styles = require('./hubPage.module.css');
var sxHubItem = {
	gap: 16,
	'&:hover': {
		backgroundColor: xenaColor.BLACK_3
	},
};
var sxStatusContainer = {
	borderRadius: 24,
	minWidth: 100,
};

var RETURN = 13;

var getName = h => _.get(serverNames, h, h);

var getStatus = (user, ping) =>
	user ? (ping === true ? 'connected' : 'selected') : '';

var isStatusConnected = statusStr => statusStr === 'connected';

var reqStatus = (ping) =>
	ping == null ? ' (connecting...)' :
				(ping ? '' : ' (not running)');

var checkHost = host => testHost(host).take(1).map(v => ({[host]: v}));

var Hub = class extends React.Component {
	static displayName = 'Hub';

	state = {
		ping: {}
	};

	onNavigate = (page, params) => {
		this.props.callback(['navigate', page, params]);
	};

	componentDidMount() {
		// XXX Use a connector to get rid of selector, here.
		// Or use a sub-component.
		var {state, selector} = this.props,
			allHosts = _.keys(selector(state));

		nav({activeLink: 'hub', onNavigate: this.onNavigate});
		this.ping = new Rx.Subject();

		this.sub = this.ping.switchMapTo(Rx.Observable.from(allHosts.map(checkHost)).mergeAll())
			.subscribe(this.updatePing);

		this.ping.next();
	}

	onStartup = () => {
		this.ping.next();
	}

	componentWillUnmount() {
		this.sub.unsubscribe();
	}

	UNSAFE_componentWillReceiveProps(newProps) {//eslint-disable-line camelcase
		var {ping} = this.state,
			{state, selector} = newProps,
			servers = selector(state),
			old = _.omit(ping, _.keys(servers));

		this.setState({ping: _.omit(ping, old)});

		_.difference(_.keys(servers), _.keys(ping))
			.forEach(h => checkHost(h).subscribe(this.updatePing));
	}

	updatePing = (h) => {
		this.setState({ping: {...this.state.ping, ...h}});
	};

	onKeyDown = (ev) => {
		if (ev.keyCode === RETURN) {
			ev.preventDefault();
			this.onAdd();
		}
	};

	onSelect = (ev) => {
		var {checked} = ev.target,
			host = ev.target.getAttribute('data-host');
		this.props.callback([checked ? 'enable-host' : 'disable-host', host, 'user']);
	};

	onAdd = () => {
		var target = this.newHost,
			value = target.value.trim();
		if (value !== '') {
			this.props.callback(['add-host', parseServer(value)]);
			target.value = '';
		}
	};

	onRemove = (ev) => {
		var host = ev.currentTarget.getAttribute('data-host');
		this.props.callback(['remove-host', host]);
	};

	setAddHubInputRef = ref => this.newHost = ref;

	render() {
		var {state, selector, badge} = this.props,
			hubParams = getHubParams(state),
			{ping} = this.state,
			servers = selector(state),
			hostList = _.mapObject(servers, (s, h) => ({
				selected: s.user,
				host: h,
				name: getName(h),
				statusStr: getStatus(s.user, ping[h]),
				reqStatus: reqStatus(ping[h])
			}));
		return (

				<XTypography component='h1' variant={XTypographyVariants.MD_HEADLINE}>Data Hubs</XTypography>
				<Card elevation={2}>
					<List disablePadding>
						{_.values(hostList).map(h => (
						<Box component={ListItem} divider key={h.host} sx={sxHubItem}>
							<Checkbox checked={h.selected || false} inputProps={{'data-host': h.host}} onChange={this.onSelect}/>
							<Box
								bgcolor={isStatusConnected(h.statusStr) ? 'secondary.main' : xenaColor.BLACK_6}
								color={isStatusConnected(h.statusStr) ? 'primary.contrastText' : undefined}
								component='span'
								sx={sxStatusContainer}>
								<Typography align='center' className={styles.status} display='block' variant='caption'>{h.statusStr}</Typography>
							</Box>
							<Box flex={1}>
								<Link href={`../datapages/?${encodeObject({
									host: h.host,
									addHub: hubParams.addHub,
									removeHub: hubParams.removeHub.filter( hub => hub !== h.host)})}`} variant='body1'>
									{h.name}
								</Link>
								<Typography display='inline' variant='body1'>{h.host === localHub ? badge : h.reqStatus}</Typography>
							</Box>
							<IconButton data-host={h.host} edge='end' onClick={this.onRemove}>
								<Icon className={styles.remove}>close</Icon>
							</IconButton>
						</Box>
						))}
						<Box component={ListItem} sx={{gap: 16}}>
							<Box sx={{marginLeft: 150, width: 400}}>
								<Input fullWidth inputRef={this.setAddHubInputRef} onKeyDown={this.onKeyDown} placeholder='Add Hub' type='text'/>
							</Box>
							<Button onClick={this.onAdd}>Add</Button>
						</Box>
					</List>
				</Card>

);
	}
};

var selector = state => state.spreadsheet.servers;

class HubPage extends React.Component {
	render() {
		return (
			<Hub {...this.props} selector={selector}/>
		);
	}
}

export default HubPage;
<|endoftext|>