<commit_before>import Framebuffer from "./Framebuffer.js";
import GLTextureCube from "gl-texture-cube";

export default class TextureCube extends GLTextureCube {
  constructor(gl, sources, format = gl.RGBA, type = gl.UNSIGNED_BYTE) {
    let tmpSources = sources;
    let useFramebuffers = sources[0].handle && sources[0].handle instanceof WebGLFramebuffer;

    if(useFramebuffers) {
      let tmpCanvas = document.createElement("canvas");
      tmpCanvas.width = 1;
      tmpCanvas.height = 1;
      tmpSources = new Array(6).fill(tmpCanvas);
    }
    super(gl, {
      pos: {
        x: tmpSources[0],
        y: tmpSources[1],
        z: tmpSources[2],
      },
      neg: {
        x: tmpSources[3],
        y: tmpSources[4],
        z: tmpSources[5]
      }
    }, format, type);

    if(useFramebuffers) {
      sources[0].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, format, 0, 0, sources[0].width * .5, sources[0].height * .5, 0);
      sources[1].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, format, 0, 0, sources[1].width * .5, sources[1].height * .5, 0);
      sources[2].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, format, 0, 0, sources[2].width * .5, sources[2].height * .5, 0);
      sources[3].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, format, 0, 0, sources[3].width * .5, sources[3].height * .5, 0);
      sources[4].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, format, 0, 0, sources[4].width * .5, sources[4].height * .5, 0);
      sources[5].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, format, 0, 0, sources[5].width * .5, sources[5].height * .5, 0);
    }
  }
}
<commit_msg>update CubeTexture to match new FrameBuffer<commit_after>import Framebuffer from "./Framebuffer.js";
import GLTextureCube from "gl-texture-cube";

export default class TextureCube extends GLTextureCube {
  constructor(gl, sources, format = gl.RGBA, type = gl.UNSIGNED_BYTE) {
    let tmpSources = sources;
    let useFramebuffers = sources[0] instanceof Framebuffer;

    if(useFramebuffers) {
      let tmpCanvas = document.createElement("canvas");
      tmpCanvas.width = 1;
      tmpCanvas.height = 1;
      tmpSources = new Array(6).fill(tmpCanvas);
    }
    super(gl, {
      pos: {
        x: tmpSources[0],
        y: tmpSources[1],
        z: tmpSources[2],
      },
      neg: {
        x: tmpSources[3],
        y: tmpSources[4],
        z: tmpSources[5]
      }
    }, format, type);

    if(useFramebuffers) {
      sources[0].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_X, 0, format, 0, 0, sources[0].width * .5, sources[0].height * .5, 0);
      sources[1].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Y, 0, format, 0, 0, sources[1].width * .5, sources[1].height * .5, 0);
      sources[2].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_POSITIVE_Z, 0, format, 0, 0, sources[2].width * .5, sources[2].height * .5, 0);
      sources[3].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_X, 0, format, 0, 0, sources[3].width * .5, sources[3].height * .5, 0);
      sources[4].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, format, 0, 0, sources[4].width * .5, sources[4].height * .5, 0);
      sources[5].bind();
      gl.copyTexImage2D(gl.TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, format, 0, 0, sources[5].width * .5, sources[5].height * .5, 0);
    }
  }
}
<|endoftext|>


<commit_before>import React from 'react';

import {
  HOLY_SHOCK_HEAL_SPELL_ID, LIGHT_OF_DAWN_CAST_SPELL_ID, JUDGMENT_CAST_SPELL_ID, BESTOW_FAITH_SPELL_ID, LIGHT_OF_THE_MARTYR_SPELL_ID, TYRS_DELIVERANCE_CAST_SPELL_ID, AVENGING_WRATH_SPELL_ID, HOLY_AVENGER_SPELL_ID, AURA_MASTERY_SPELL_ID, CRUSADER_STRIKE_SPELL_ID, HOLY_PRISM_CAST_SPELL_ID, LIGHTS_HAMMER_CAST_SPELL_ID, FLASH_OF_LIGHT_SPELL_ID, HOLY_LIGHT_SPELL_ID, ARCANE_TORRENT_SPELL_ID, RULE_OF_LAW_SPELL_ID,
  JUDGMENT_OF_LIGHT_SPELL_ID, CRUSADERS_MIGHT_SPELL_ID,
} from './Parser/Constants';

const SPELL_CATEGORY = {
  ROTATIONAL: 'Rotational Spell',
  COOLDOWNS: 'Cooldown',
  OTHERS: 'Spell',
};

const CPM_ABILITIES = [
  {
    spellId: HOLY_SHOCK_HEAL_SPELL_ID,
    icon: 'spell_holy_searinglight',
    name: 'Holy Shock',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCasts: castCount => castCount.hits,
    getCooldown: haste => 9 / (1 + haste),
  },
  {
    spellId: LIGHT_OF_DAWN_CAST_SPELL_ID,
    icon: 'spell_paladin_lightofdawn',
    name: 'Light of Dawn',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12 / (1 + haste),
  },
  {
    spellId: JUDGMENT_CAST_SPELL_ID,
    icon: 'spell_holy_righteousfury',
    name: 'Judgment',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12 / (1 + haste),
    isActive: combatant => combatant.lv90Talent === JUDGMENT_OF_LIGHT_SPELL_ID,
    recommendedCastEfficiency: 0.85, // this rarely overheals, so keeping this on cooldown is pretty much always best
  },
  {
    spellId: BESTOW_FAITH_SPELL_ID,
    icon: 'ability_paladin_blessedmending',
    name: 'Bestow Faith',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12,
    isActive: combatant => combatant.lv15Talent === BESTOW_FAITH_SPELL_ID,
    recommendedCastEfficiency: 0.65,
  },
  {
    spellId: LIGHTS_HAMMER_CAST_SPELL_ID,
    icon: 'spell_paladin_lightshammer',
    name: 'Light\'s Hammer',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 60,
    isActive: combatant => combatant.lv15Talent === LIGHTS_HAMMER_CAST_SPELL_ID,
  },
  {
    spellId: CRUSADER_STRIKE_SPELL_ID,
    icon: 'spell_holy_crusaderstrike',
    name: 'Crusader Strike',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 4.5 / (1 + haste) / 2,
    isActive: combatant => combatant.lv15Talent === CRUSADERS_MIGHT_SPELL_ID,
  },
  {
    spellId: HOLY_PRISM_CAST_SPELL_ID,
    icon: 'spell_paladin_holyprism',
    name: 'Holy Prism',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 20,
    isActive: combatant => combatant.lv75Talent === HOLY_PRISM_CAST_SPELL_ID,
  },
  {
    spellId: TYRS_DELIVERANCE_CAST_SPELL_ID,
    icon: 'inv_mace_2h_artifactsilverhand_d_01',
    name: 'Tyr\'s Deliverance',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
  },
  {
    spellId: AVENGING_WRATH_SPELL_ID,
    icon: 'spell_holy_avenginewrath',
    name: 'Avenging Wrath',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 120,
  },
  {
    spellId: HOLY_AVENGER_SPELL_ID,
    icon: 'ability_paladin_holyavenger',
    name: 'Holy Avenger',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
    isActive: combatant => combatant.lv75Talent === HOLY_AVENGER_SPELL_ID,
  },
  {
    spellId: AURA_MASTERY_SPELL_ID,
    icon: 'spell_holy_auramastery',
    name: 'Aura Mastery',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 180,
  },
  {
    spellId: ARCANE_TORRENT_SPELL_ID,
    icon: 'spell_shadow_teleport',
    name: 'Arcane Torrent',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
    hideWithZeroCasts: true,
  },
  {
    spellId: RULE_OF_LAW_SPELL_ID,
    icon: 'ability_paladin_longarmofthelaw',
    name: 'Rule of Law',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    charges: 2,
    isActive: combatant => combatant.lv30Talent === RULE_OF_LAW_SPELL_ID,
  },
  {
    spellId: LIGHT_OF_THE_MARTYR_SPELL_ID,
    icon: 'ability_paladin_lightofthemartyr',
    name: 'Light of the Martyr',
    category: SPELL_CATEGORY.OTHERS,
    getCooldown: haste => null,
  },
  {
    spellId: FLASH_OF_LIGHT_SPELL_ID,
    icon: 'spell_holy_flashheal',
    name: 'Filler Flash of Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => (castCount.casts || 0) - (castCount.withIol || 0),
    getCooldown: haste => null,
  },
  {
    spellId: FLASH_OF_LIGHT_SPELL_ID,
    icon: 'spell_holy_flashheal',
    name: 'Infusion of Light Flash of Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => castCount.withIol || 0,
    getCooldown: haste => null,
  },
  {
    spellId: HOLY_LIGHT_SPELL_ID,
    icon: 'spell_holy_surgeoflight',
    name: 'Filler Holy Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => (castCount.casts || 0) - (castCount.withIol || 0),
    getCooldown: haste => null,
  },
  {
    spellId: HOLY_LIGHT_SPELL_ID,
    icon: 'spell_holy_surgeoflight',
    name: 'Infusion of Light Holy Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => castCount.withIol || 0,
    getCooldown: haste => null,
  },
];

const CastEfficiency = ({ parser }) => {
  const fightDuration = (parser.finished ? parser.fight.end_time : parser.currentTimestamp) - parser.fight.start_time;
  const minutes = fightDuration / 1000 / 60;

  const castCounter = parser.modules.castCounter;
  const getCastCount = spellId => castCounter.casts[spellId] || {};

  const selectedCombatant = parser.selectedCombatant;

  if (!selectedCombatant) {
    return null;
  }

  const hastePercentage = selectedCombatant ? selectedCombatant.hastePercentage : 0;

  return (

      {Object.keys(SPELL_CATEGORY).map((key) => {
        return (

              {SPELL_CATEGORY[key]}
              <dfn data-tip="Casts Per Minute">CPM</dfn>
              <dfn data-tip="The max possible casts is a super simplified calculation based on the Haste you get from your gear alone. Haste increasers like Holy Avenger, Bloodlust and from boss abilities are not taken into consideration, so this is always lower than actually possible for abilities affected by Haste.">Cast efficiency</dfn>

            {CPM_ABILITIES
              .filter(ability => ability.category === SPELL_CATEGORY[key])
              .filter(ability => !ability.isActive || ability.isActive(selectedCombatant))
              .map((ability) => {
                const castCount = getCastCount(ability.spellId);
                const casts = (ability.getCasts ? ability.getCasts(castCount) : castCount.casts) || 0;
                if (ability.hideWithZeroCasts && casts === 0) {
                  return null;
                }
                const cpm = casts/minutes;

                const cooldown = ability.getCooldown(hastePercentage);
                // By dividing the fight duration by the cooldown we get the max amount of casts during this particular fight, we round this up because you would be able to cast once at the start of the fight and once at the end since abilities always start off cooldown (e.g. fight is 100 seconds long, you could cast 2 Holy Avengers with a 90 sec cooldown). Good players should be able to reasonably predict this and maximize their casts.
                const maxCasts = Math.ceil(fightDuration / 1000 / cooldown) + (ability.charges ? ability.charges - 1 : 0);
                const maxCpm = cooldown === null ? null : maxCasts / minutes;
                const castEfficiency = cpm / maxCpm;

                const canBeImproved = castEfficiency < (ability.recommendedCastEfficiency || 0.8);

                return (

                      <img src={`./img/icons/${ability.icon}.jpg`} alt={ability.name} /> {ability.name}

                      {cpm.toFixed(2)}

                      {casts}{maxCasts === Infinity ? '' : `/${Math.floor(maxCasts)}`} casts

                      {maxCpm === null ? '' : (
                          <div
                            className="performance-bar"
                            style={{ width: `${castEfficiency * 100}%`, backgroundColor: canBeImproved ? '#ff8000' : '#70b570' }}
                          >

                        )}

                      {maxCpm === null ? '' : `${(castEfficiency * 100).toFixed(2)}%`}

                      {canBeImproved && 'Can be improved.'}

                );
              })}

        );
      })}

  );
};
CastEfficiency.propTypes = {
  parser: React.PropTypes.object.isRequired,
};

export default CastEfficiency;
<commit_msg>Wowhead links<commit_after>import React from 'react';

import {
  HOLY_SHOCK_HEAL_SPELL_ID, LIGHT_OF_DAWN_CAST_SPELL_ID, JUDGMENT_CAST_SPELL_ID, BESTOW_FAITH_SPELL_ID, LIGHT_OF_THE_MARTYR_SPELL_ID, TYRS_DELIVERANCE_CAST_SPELL_ID, AVENGING_WRATH_SPELL_ID, HOLY_AVENGER_SPELL_ID, AURA_MASTERY_SPELL_ID, CRUSADER_STRIKE_SPELL_ID, HOLY_PRISM_CAST_SPELL_ID, LIGHTS_HAMMER_CAST_SPELL_ID, FLASH_OF_LIGHT_SPELL_ID, HOLY_LIGHT_SPELL_ID, ARCANE_TORRENT_SPELL_ID, RULE_OF_LAW_SPELL_ID,
  JUDGMENT_OF_LIGHT_SPELL_ID, CRUSADERS_MIGHT_SPELL_ID,
} from './Parser/Constants';

const SPELL_CATEGORY = {
  ROTATIONAL: 'Rotational Spell',
  COOLDOWNS: 'Cooldown',
  OTHERS: 'Spell',
};

const CPM_ABILITIES = [
  {
    spellId: HOLY_SHOCK_HEAL_SPELL_ID,
    icon: 'spell_holy_searinglight',
    name: 'Holy Shock',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCasts: castCount => castCount.hits,
    getCooldown: haste => 9 / (1 + haste),
  },
  {
    spellId: LIGHT_OF_DAWN_CAST_SPELL_ID,
    icon: 'spell_paladin_lightofdawn',
    name: 'Light of Dawn',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12 / (1 + haste),
  },
  {
    spellId: JUDGMENT_CAST_SPELL_ID,
    icon: 'spell_holy_righteousfury',
    name: 'Judgment',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12 / (1 + haste),
    isActive: combatant => combatant.lv90Talent === JUDGMENT_OF_LIGHT_SPELL_ID,
    recommendedCastEfficiency: 0.85, // this rarely overheals, so keeping this on cooldown is pretty much always best
  },
  {
    spellId: BESTOW_FAITH_SPELL_ID,
    icon: 'ability_paladin_blessedmending',
    name: 'Bestow Faith',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 12,
    isActive: combatant => combatant.lv15Talent === BESTOW_FAITH_SPELL_ID,
    recommendedCastEfficiency: 0.65,
  },
  {
    spellId: LIGHTS_HAMMER_CAST_SPELL_ID,
    icon: 'spell_paladin_lightshammer',
    name: 'Light\'s Hammer',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 60,
    isActive: combatant => combatant.lv15Talent === LIGHTS_HAMMER_CAST_SPELL_ID,
  },
  {
    spellId: CRUSADER_STRIKE_SPELL_ID,
    icon: 'spell_holy_crusaderstrike',
    name: 'Crusader Strike',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 4.5 / (1 + haste) / 2,
    isActive: combatant => combatant.lv15Talent === CRUSADERS_MIGHT_SPELL_ID,
  },
  {
    spellId: HOLY_PRISM_CAST_SPELL_ID,
    icon: 'spell_paladin_holyprism',
    name: 'Holy Prism',
    category: SPELL_CATEGORY.ROTATIONAL,
    getCooldown: haste => 20,
    isActive: combatant => combatant.lv75Talent === HOLY_PRISM_CAST_SPELL_ID,
  },
  {
    spellId: TYRS_DELIVERANCE_CAST_SPELL_ID,
    icon: 'inv_mace_2h_artifactsilverhand_d_01',
    name: 'Tyr\'s Deliverance',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
  },
  {
    spellId: AVENGING_WRATH_SPELL_ID,
    icon: 'spell_holy_avenginewrath',
    name: 'Avenging Wrath',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 120,
  },
  {
    spellId: HOLY_AVENGER_SPELL_ID,
    icon: 'ability_paladin_holyavenger',
    name: 'Holy Avenger',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
    isActive: combatant => combatant.lv75Talent === HOLY_AVENGER_SPELL_ID,
  },
  {
    spellId: AURA_MASTERY_SPELL_ID,
    icon: 'spell_holy_auramastery',
    name: 'Aura Mastery',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 180,
  },
  {
    spellId: ARCANE_TORRENT_SPELL_ID,
    icon: 'spell_shadow_teleport',
    name: 'Arcane Torrent',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 90,
    hideWithZeroCasts: true,
  },
  {
    spellId: RULE_OF_LAW_SPELL_ID,
    icon: 'ability_paladin_longarmofthelaw',
    name: 'Rule of Law',
    category: SPELL_CATEGORY.COOLDOWNS,
    getCooldown: haste => 30,
    charges: 2,
    isActive: combatant => combatant.lv30Talent === RULE_OF_LAW_SPELL_ID,
  },
  {
    spellId: LIGHT_OF_THE_MARTYR_SPELL_ID,
    icon: 'ability_paladin_lightofthemartyr',
    name: 'Light of the Martyr',
    category: SPELL_CATEGORY.OTHERS,
    getCooldown: haste => null,
  },
  {
    spellId: FLASH_OF_LIGHT_SPELL_ID,
    icon: 'spell_holy_flashheal',
    name: 'Filler Flash of Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => (castCount.casts || 0) - (castCount.withIol || 0),
    getCooldown: haste => null,
  },
  {
    spellId: FLASH_OF_LIGHT_SPELL_ID,
    icon: 'spell_holy_flashheal',
    name: 'Infusion of Light Flash of Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => castCount.withIol || 0,
    getCooldown: haste => null,
  },
  {
    spellId: HOLY_LIGHT_SPELL_ID,
    icon: 'spell_holy_surgeoflight',
    name: 'Filler Holy Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => (castCount.casts || 0) - (castCount.withIol || 0),
    getCooldown: haste => null,
  },
  {
    spellId: HOLY_LIGHT_SPELL_ID,
    icon: 'spell_holy_surgeoflight',
    name: 'Infusion of Light Holy Light',
    category: SPELL_CATEGORY.OTHERS,
    getCasts: castCount => castCount.withIol || 0,
    getCooldown: haste => null,
  },
];

const CastEfficiency = ({ parser }) => {
  const fightDuration = (parser.finished ? parser.fight.end_time : parser.currentTimestamp) - parser.fight.start_time;
  const minutes = fightDuration / 1000 / 60;

  const castCounter = parser.modules.castCounter;
  const getCastCount = spellId => castCounter.casts[spellId] || {};

  const selectedCombatant = parser.selectedCombatant;

  if (!selectedCombatant) {
    return null;
  }

  const hastePercentage = selectedCombatant ? selectedCombatant.hastePercentage : 0;

  return (

      {Object.keys(SPELL_CATEGORY).map((key) => {
        return (

              {SPELL_CATEGORY[key]}
              <dfn data-tip="Casts Per Minute">CPM</dfn>
              <dfn data-tip="The max possible casts is a super simplified calculation based on the Haste you get from your gear alone. Haste increasers like Holy Avenger, Bloodlust and from boss abilities are not taken into consideration, so this is always lower than actually possible for abilities affected by Haste.">Cast efficiency</dfn>

            {CPM_ABILITIES
              .filter(ability => ability.category === SPELL_CATEGORY[key])
              .filter(ability => !ability.isActive || ability.isActive(selectedCombatant))
              .map((ability) => {
                const castCount = getCastCount(ability.spellId);
                const casts = (ability.getCasts ? ability.getCasts(castCount) : castCount.casts) || 0;
                if (ability.hideWithZeroCasts && casts === 0) {
                  return null;
                }
                const cpm = casts/minutes;

                const cooldown = ability.getCooldown(hastePercentage);
                // By dividing the fight duration by the cooldown we get the max amount of casts during this particular fight, we round this up because you would be able to cast once at the start of the fight and once at the end since abilities always start off cooldown (e.g. fight is 100 seconds long, you could cast 2 Holy Avengers with a 90 sec cooldown). Good players should be able to reasonably predict this and maximize their casts.
                const maxCasts = Math.ceil(fightDuration / 1000 / cooldown) + (ability.charges ? ability.charges - 1 : 0);
                const maxCpm = cooldown === null ? null : maxCasts / minutes;
                const castEfficiency = cpm / maxCpm;

                const canBeImproved = castEfficiency < (ability.recommendedCastEfficiency || 0.8);

                return (

                      <a href={`http://www.wowhead.com/spell=${ability.spellId}`} target="_blank">
                        <img src={`./img/icons/${ability.icon}.jpg`} alt={ability.name} /> {ability.name}

                      {cpm.toFixed(2)}

                      {casts}{maxCasts === Infinity ? '' : `/${Math.floor(maxCasts)}`} casts

                      {maxCpm === null ? '' : (
                          <div
                            className="performance-bar"
                            style={{ width: `${castEfficiency * 100}%`, backgroundColor: canBeImproved ? '#ff8000' : '#70b570' }}
                          >

                        )}

                      {maxCpm === null ? '' : `${(castEfficiency * 100).toFixed(2)}%`}

                      {canBeImproved && 'Can be improved.'}

                );
              })}

        );
      })}

  );
};
CastEfficiency.propTypes = {
  parser: React.PropTypes.object.isRequired,
};

export default CastEfficiency;
<|endoftext|>


<commit_before>/*---------------------------------------------------------
 * OpenERP base_calendar
 *---------------------------------------------------------*/

openerp.base_calendar = function(openerp) {
QWeb.add_template('/base_calendar/static/src/xml/base_calendar.xml');
openerp.base.views.add('calendar', 'openerp.base_calendar.CalendarView');
openerp.base_calendar.CalendarView = openerp.base.Controller.extend({
// Dhtmlx scheduler ?
	init: function(view_manager, session, element_id, dataset, view_id){
		this._super(session, element_id);
		this.view_manager = view_manager;
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;
        this.domain = this.dataset.domain || [];
		this.context = this.dataset.context || {};
	},
	start: function() {
		this.rpc("/base_calendar/calendarview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
	},
    on_loaded: function(data) {
		this.calendar_fields = {};
		this.ids = this.dataset.ids;
		this.day_lenth = 8;
		this.color_values = [];
		this.info_fields = [];

        this.fields_view = data.fields_view;
        this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
		this.view_id = this.fields_view.view_id;

        this.date_start = this.fields_view.arch.attrs.date_start;
		this.date_delay = this.fields_view.arch.attrs.date_delay;
		this.date_stop = this.fields_view.arch.attrs.date_stop;

        this.colors = this.fields_view.arch.attrs.colors;
		this.day_length = this.fields_view.arch.attrs.day_length || 8;
		this.color_field = this.fields_view.arch.attrs.color;
		this.fields =  this.fields_view.fields;

        //* Calendar Fields *
		this.calendar_fields['date_start'] = {'name': this.date_start, 'kind': this.fields[this.date_start]['type']};

		if(this.date_delay)
			this.calendar_fields['date_delay'] = {'name': this.date_delay, 'kind': this.fields[this.date_delay]['type']};

		if(this.date_stop)
			this.calendar_fields['date_stop'] = {'name': this.date_stop, 'kind': this.fields[this.date_stop]['type']};

		//* ------- *

        for(var fld=0;fld<this.fields_view.arch.children.length;fld++) {
			this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
		}

        this.load_scheduler();
    },

    load_scheduler: function() {
        var self = this;
        this.dataset.read_slice([], 0, false, function(events) {
            if (self.session.locale_code) {
                $LAB.setOptions({AlwaysPreserveOrder: true})
                .script([
                    '/base_calendar/static/lib/dhtmlxScheduler/sources/locale_'+self.session.locale_code+'.js',
                    '/base_calendar/static/lib/dhtmlxScheduler/sources/locale_recurring_'+self.session.locale_code+'.js'
                ])
                .wait(function() {
                    self.schedule_events(events);
                });

            } else {
                self.schedule_events(events);
            }
        });
    },

    schedule_events: function(events) {
        this.$element.html(QWeb.render("CalendarView", {"fields_view": this.fields_view}));
        /*
		 * Initialize dhtmlx Schedular
		 */

        scheduler.clearAll();
        scheduler.config.api_date="%Y-%m-%d %H:%M:%S";
        if(this.fields[this.date_start]['type'] == 'time') {
            scheduler.config.xml_date="%H:%M:%S";
        } else {
            scheduler.config.xml_date="%Y-%m-%d %H:%M:%S";

        }

        scheduler.config.multi_day = true; //Multi day events are not rendered in daily and weekly views

        // Initialize Sceduler
		scheduler.init('openerp_scheduler',null,"month");

        //To parse Events we have to convert date Format
        var res_events = [];
        for(var e=0;e<events.length;e++) {
            var evt = events[e];
            if(!evt[this.date_start]) {
                this.notification.warn("Start date is not defined for event :", evt['id']);
                break;
            }

            if (this.fields[this.date_start]['type'] == 'date') {
                evt[this.date_start] = openerp.base.parse_date(evt[this.date_start]).set({hour: 9}).toString('yyyy-MM-dd HH:mm:ss')
            }
            if(this.date_stop && evt[this.date_stop] && this.fields[this.date_stop]['type'] == 'date') {
                evt[this.date_stop] = openerp.base.parse_date(evt[this.date_stop]).set({hour: 17}).toString('yyyy-MM-dd HH:mm:ss')
            }
            res_events.push(this.convert_event(evt))
        }
        scheduler.parse(res_events,"json");
        jQuery('#dhx_minical_icon').bind('click', this.mini_calendar);

        // Event Options Click,edit
        var self = this;
        scheduler.attachEvent(
            "onDblClick",
            function(event_id, event_object) {
                self.popup_event(event_id);
            }
        );

        scheduler.attachEvent(
            "onEventCreated",
            function(event_id, event_object) {
                //Replace default Lightbox with Popup Form of new Event

                scheduler.showLightbox = function(){
                    //Delete Newly created Event,Later we reload Scheduler
                    scheduler.deleteEvent(event_id)
                    self.popup_event();
                }
            }
        );
    },

    convert_event: function(event) {
        var res_text = '';
        var res_description = [];
        var start = event[this.date_start];
        var end = event[this.date_delay] || 1;
        var span = 0;
        if(this.info_fields) {
            var fld = event[this.info_fields[0]];
            if(typeof fld == 'object') {

                res_text = fld[fld.length -1];
            } else {
                res_text = fld
            }
            var sliced_info_fields = this.info_fields.slice(1);
            for(sl_fld in sliced_info_fields) {
                var slc_fld = event[sliced_info_fields[sl_fld]];
                if(typeof slc_fld == 'object') {
                  res_description.push(slc_fld[slc_fld.length - 1])
                } else {
                    if(slc_fld) {
                        res_description.push(slc_fld);
                    }
                }
            }
        }
        if(start && end){
            var n = 0;
            var h = end;
            if (end == this.day_length) {
                span = 1
            } else if(end > this.day_length) {
                n = end / this.day_length;
                h = end % this.day_length;
                n = parseInt(Math.floor(n));

                if(h > 0)
                    span = n + 1
                else
                    span = n
            }
            var end_date = openerp.base.parse_datetime(start);
            end = end_date.add({hours: h, minutes: n})
        }
        if(start && this.date_stop) {
            var tds = start = openerp.base.parse_datetime(start);
            var tde = ends = openerp.base.parse_datetime(event[this.date_stop]);
            if(event[this.date_stop] == undefined) {
                if(tds) {
                    end = (tds.getOrdinalNumber() + 60 * 60)
                }
            }

            if(tds && tde) {
                //time.mktime equivalent
                tds = (tds.getOrdinalNumber() / 1e3 >> 0) - (tds.getOrdinalNumber() < 0);
                tde = (tde.getOrdinalNumber() / 1e3 >> 0) - (tde.getOrdinalNumber() < 0);

            }
            if(tds >= tde) {
                 tde = tds + 60 * 60;
            }

            n = (tde - tds) / (60 * 60);
            if (n >= this.day_length) {
                span = Math.ceil(n / 24);
            }
        }

        return {
            'start_date': start.toString('yyyy-MM-dd HH:mm:ss'),
            'end_date': end.toString('yyyy-MM-dd HH:mm:ss'),
            'text': res_text,
            'id': event['id'],
            'title': res_description.join()
            }
    },

    mini_calendar: function() {

		if(scheduler.isCalendarVisible()) {
			scheduler.destroyCalendar();
		} else {
			scheduler.renderCalendar({
				position:"dhx_minical_icon",
            	date:scheduler._date,
            	navigation:true,
				handler:function(date,calendar){
	               scheduler.setCurrentView(date);
	               scheduler.destroyCalendar()
	            }
			});
		}
	},

    do_search: function(domains, contexts, groupbys) {
        var self = this;
        this.rpc('/base/session/eval_domain_and_context', {
            domains: domains,
            contexts: contexts,
            group_by_seq: groupbys
        }, function (results) {
            // TODO: handle non-empty results.group_by with read_group
            self.dataset.context = self.context = results.context;
            self.dataset.domain = self.domain = results.domain;
            self.dataset.read_slice(self.fields, 0, self.limit,function(events){
                self.schedule_events(events)
            });
        });
    },

	do_show: function () {
        this.$element.show();
    },

    do_hide: function () {
        this.$element.hide();
    },

    popup_event: function(event_id) {
        var action_manager = this.view_manager.session.action_manager;

        var action = {
    		"res_model": this.dataset.model,
    		"res_id": event_id,
            "views":[[false,"form"]],
            "type":"ir.actions.act_window",
            "view_type":"form",
            "view_mode":"form"
        }

        action.flags = {
    		search_view: false,
            sidebar : false,
            views_switcher : false,
            action_buttons : false,
            pager: false
            }
        var element_id = _.uniqueId("act_window_dialog");
        var dialog = jQuery('<div>', {
            'id': element_id
            }).dialog({
                modal: true,
                width: 'auto',
                height: 'auto'
            });

        var action_manager = new openerp.base.ActionManager(this.session, element_id);
        action_manager.start();
        action_manager.do_action(action);
    }
});

};

// DEBUG_RPC:rpc.request:('execute', 'addons-dsh-l10n_us', 1, '*', ('ir.filters', 'get_filters', u'res.partner'))
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
<commit_msg>[FIX] Popup selected event.<commit_after>/*---------------------------------------------------------
 * OpenERP base_calendar
 *---------------------------------------------------------*/

openerp.base_calendar = function(openerp) {
QWeb.add_template('/base_calendar/static/src/xml/base_calendar.xml');
openerp.base.views.add('calendar', 'openerp.base_calendar.CalendarView');
openerp.base_calendar.CalendarView = openerp.base.Controller.extend({
// Dhtmlx scheduler ?
	init: function(view_manager, session, element_id, dataset, view_id){
		this._super(session, element_id);
		this.view_manager = view_manager;
        this.dataset = dataset;
        this.model = dataset.model;
        this.view_id = view_id;
        this.domain = this.dataset.domain || [];
		this.context = this.dataset.context || {};
	},
	start: function() {
		this.rpc("/base_calendar/calendarview/load", {"model": this.model, "view_id": this.view_id}, this.on_loaded);
	},
    on_loaded: function(data) {
		this.calendar_fields = {};
		this.ids = this.dataset.ids;
		this.day_lenth = 8;
		this.color_values = [];
		this.info_fields = [];

        this.fields_view = data.fields_view;
        this.name = this.fields_view.name || this.fields_view.arch.attrs.string;
		this.view_id = this.fields_view.view_id;

        this.date_start = this.fields_view.arch.attrs.date_start;
		this.date_delay = this.fields_view.arch.attrs.date_delay;
		this.date_stop = this.fields_view.arch.attrs.date_stop;

        this.colors = this.fields_view.arch.attrs.colors;
		this.day_length = this.fields_view.arch.attrs.day_length || 8;
		this.color_field = this.fields_view.arch.attrs.color;
		this.fields =  this.fields_view.fields;

        //* Calendar Fields *
		this.calendar_fields['date_start'] = {'name': this.date_start, 'kind': this.fields[this.date_start]['type']};

		if(this.date_delay)
			this.calendar_fields['date_delay'] = {'name': this.date_delay, 'kind': this.fields[this.date_delay]['type']};

		if(this.date_stop)
			this.calendar_fields['date_stop'] = {'name': this.date_stop, 'kind': this.fields[this.date_stop]['type']};

		//* ------- *

        for(var fld=0;fld<this.fields_view.arch.children.length;fld++) {
			this.info_fields.push(this.fields_view.arch.children[fld].attrs.name);
		}

        this.load_scheduler();
    },

    load_scheduler: function() {
        var self = this;
        this.dataset.read_slice([], 0, false, function(events) {
            if (self.session.locale_code) {
                $LAB.setOptions({AlwaysPreserveOrder: true})
                .script([
                    '/base_calendar/static/lib/dhtmlxScheduler/sources/locale_'+self.session.locale_code+'.js',
                    '/base_calendar/static/lib/dhtmlxScheduler/sources/locale_recurring_'+self.session.locale_code+'.js'
                ])
                .wait(function() {
                    self.schedule_events(events);
                });

            } else {
                self.schedule_events(events);
            }
        });
    },

    schedule_events: function(events) {
        this.$element.html(QWeb.render("CalendarView", {"fields_view": this.fields_view}));
        /*
		 * Initialize dhtmlx Schedular
		 */

        scheduler.clearAll();
        scheduler.config.api_date="%Y-%m-%d %H:%M:%S";
        if(this.fields[this.date_start]['type'] == 'time') {
            scheduler.config.xml_date="%H:%M:%S";
        } else {
            scheduler.config.xml_date="%Y-%m-%d %H:%M:%S";

        }

        scheduler.config.multi_day = true; //Multi day events are not rendered in daily and weekly views

        // Initialize Sceduler
		scheduler.init('openerp_scheduler',null,"month");

        //To parse Events we have to convert date Format
        var res_events = [];
        for(var e=0;e<events.length;e++) {
            var evt = events[e];
            if(!evt[this.date_start]) {
                this.notification.warn("Start date is not defined for event :", evt['id']);
                break;
            }

            if (this.fields[this.date_start]['type'] == 'date') {
                evt[this.date_start] = openerp.base.parse_date(evt[this.date_start]).set({hour: 9}).toString('yyyy-MM-dd HH:mm:ss')
            }
            if(this.date_stop && evt[this.date_stop] && this.fields[this.date_stop]['type'] == 'date') {
                evt[this.date_stop] = openerp.base.parse_date(evt[this.date_stop]).set({hour: 17}).toString('yyyy-MM-dd HH:mm:ss')
            }
            res_events.push(this.convert_event(evt))
        }
        scheduler.parse(res_events,"json");
        jQuery('#dhx_minical_icon').bind('click', this.mini_calendar);

        // Event Options Click,edit
        var self = this;
        scheduler.attachEvent(
            "onDblClick",
            function(event_id, event_object) {
                self.popup_event(event_id);
            }
        );

        scheduler.attachEvent(
            "onEventCreated",
            function(event_id, event_object) {
                //Replace default Lightbox with Popup Form of new Event

                scheduler.showLightbox = function(){
                    //Delete Newly created Event,Later we reload Scheduler
                    scheduler.deleteEvent(event_id)
                    self.popup_event();
                }
            }
        );
    },

    convert_event: function(event) {
        var res_text = '';
        var res_description = [];
        var start = event[this.date_start];
        var end = event[this.date_delay] || 1;
        var span = 0;
        if(this.info_fields) {
            var fld = event[this.info_fields[0]];
            if(typeof fld == 'object') {

                res_text = fld[fld.length -1];
            } else {
                res_text = fld
            }
            var sliced_info_fields = this.info_fields.slice(1);
            for(sl_fld in sliced_info_fields) {
                var slc_fld = event[sliced_info_fields[sl_fld]];
                if(typeof slc_fld == 'object') {
                  res_description.push(slc_fld[slc_fld.length - 1])
                } else {
                    if(slc_fld) {
                        res_description.push(slc_fld);
                    }
                }
            }
        }
        if(start && end){
            var n = 0;
            var h = end;
            if (end == this.day_length) {
                span = 1
            } else if(end > this.day_length) {
                n = end / this.day_length;
                h = end % this.day_length;
                n = parseInt(Math.floor(n));

                if(h > 0)
                    span = n + 1
                else
                    span = n
            }
            var end_date = openerp.base.parse_datetime(start);
            end = end_date.add({hours: h, minutes: n})
        }
        if(start && this.date_stop) {
            var tds = start = openerp.base.parse_datetime(start);
            var tde = ends = openerp.base.parse_datetime(event[this.date_stop]);
            if(event[this.date_stop] == undefined) {
                if(tds) {
                    end = (tds.getOrdinalNumber() + 60 * 60)
                }
            }

            if(tds && tde) {
                //time.mktime equivalent
                tds = (tds.getOrdinalNumber() / 1e3 >> 0) - (tds.getOrdinalNumber() < 0);
                tde = (tde.getOrdinalNumber() / 1e3 >> 0) - (tde.getOrdinalNumber() < 0);

            }
            if(tds >= tde) {
                 tde = tds + 60 * 60;
            }

            n = (tde - tds) / (60 * 60);
            if (n >= this.day_length) {
                span = Math.ceil(n / 24);
            }
        }

        return {
            'start_date': start.toString('yyyy-MM-dd HH:mm:ss'),
            'end_date': end.toString('yyyy-MM-dd HH:mm:ss'),
            'text': res_text,
            'id': event['id'],
            'title': res_description.join()
            }
    },

    mini_calendar: function() {

		if(scheduler.isCalendarVisible()) {
			scheduler.destroyCalendar();
		} else {
			scheduler.renderCalendar({
				position:"dhx_minical_icon",
            	date:scheduler._date,
            	navigation:true,
				handler:function(date,calendar){
	               scheduler.setCurrentView(date);
	               scheduler.destroyCalendar()
	            }
			});
		}
	},

    do_search: function(domains, contexts, groupbys) {
        var self = this;
        this.rpc('/base/session/eval_domain_and_context', {
            domains: domains,
            contexts: contexts,
            group_by_seq: groupbys
        }, function (results) {
            // TODO: handle non-empty results.group_by with read_group
            self.dataset.context = self.context = results.context;
            self.dataset.domain = self.domain = results.domain;
            self.dataset.read_slice(self.fields, 0, self.limit,function(events){
                self.schedule_events(events)
            });
        });
    },

	do_show: function () {
        this.$element.show();
    },

    do_hide: function () {
        this.$element.hide();
    },

    popup_event: function(event_id) {

    	if(event_id) event_id = parseInt(event_id, 10);

        var action = {
    		"res_model": this.dataset.model,
    		"res_id": event_id,
            "views":[[false,"form"]],
            "type":"ir.actions.act_window",
            "view_type":"form",
            "view_mode":"form"
        }

        action.flags = {
    		search_view: false,
            sidebar : false,
            views_switcher : false,
            action_buttons : false,
            pager: false
            }
        var element_id = _.uniqueId("act_window_dialog");
        var dialog = jQuery('<div>', {
            'id': element_id
            }).dialog({
                modal: true,
                width: 'auto',
                height: 'auto'
            });

        var action_manager = new openerp.base.ActionManager(this.session, element_id);
        action_manager.start();
        action_manager.do_action(action);
    }
});

};

// DEBUG_RPC:rpc.request:('execute', 'addons-dsh-l10n_us', 1, '*', ('ir.filters', 'get_filters', u'res.partner'))
// vim:et fdc=0 fdl=0 foldnestmax=3 fdm=syntax:
<|endoftext|>


<commit_before>import {
  TIMELINE_UPDATE,
  TIMELINE_DELETE,
  TIMELINE_CLEAR,
  TIMELINE_EXPAND_SUCCESS,
  TIMELINE_EXPAND_REQUEST,
  TIMELINE_EXPAND_FAIL,
  TIMELINE_SCROLL_TOP,
  TIMELINE_CONNECT,
  TIMELINE_DISCONNECT,
} from 'flavours/glitch/actions/timelines';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
  ACCOUNT_UNFOLLOW_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap();

const initialTimeline = ImmutableMap({
  unread: 0,
  online: false,
  top: true,
  isLoading: false,
  hasMore: true,
  items: ImmutableList(),
});

const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    mMap.set('isLoading', false);
    mMap.set('isPartial', isPartial);

    if (!next && !isLoadingRecent) mMap.set('hasMore', false);

    if (!statuses.isEmpty()) {
      mMap.update('items', ImmutableList(), oldIds => {
        const newIds = statuses.map(status => status.get('id'));
        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);

        if (firstIndex < 0) {
          return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
        }

        return oldIds.take(firstIndex + 1).concat(
          isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
          oldIds.skip(lastIndex)
        );
      });
    }
  }));
};

const updateTimeline = (state, timeline, status) => {
  const top        = state.getIn([timeline, 'top']);
  const ids        = state.getIn([timeline, 'items'], ImmutableList());
  const includesId = ids.includes(status.get('id'));
  const unread     = state.getIn([timeline, 'unread'], 0);

  if (includesId) {
    return state;
  }

  let newIds = ids;

  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    if (!top) mMap.set('unread', unread + 1);
    if (top && ids.size > 40) newIds = newIds.take(20);
    mMap.set('items', newIds.unshift(status.get('id')));
  }));
};

const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
  state.keySeq().forEach(timeline => {
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
      state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
  });

  // Remove reblogs of deleted status
  references.forEach(ref => {
    state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
  });

  return state;
};

const clearTimeline = (state, timeline) => {
  return state.set(timeline, initialTimeline);
};

const filterTimelines = (state, relationship, statuses) => {
  let references;

  statuses.forEach(status => {
    if (status.get('account') !== relationship.id) {
      return;
    }

    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
    state      = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
  });

  return state;
};

const filterTimeline = (timeline, state, relationship, statuses) =>
  state.updateIn([timeline, 'items'], ImmutableList(), list =>
    list.filterNot(statusId =>
      statuses.getIn([statusId, 'account']) === relationship.id
    ));

const updateTop = (state, timeline, top) => {
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    if (top) mMap.set('unread', 0);
    mMap.set('top', top);
  }));
};

export default function timelines(state = initialState, action) {
  switch(action.type) {
  case TIMELINE_EXPAND_REQUEST:
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
  case TIMELINE_EXPAND_FAIL:
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
  case TIMELINE_EXPAND_SUCCESS:
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
  case TIMELINE_UPDATE:
    return updateTimeline(state, action.timeline, fromJS(action.status));
  case TIMELINE_DELETE:
    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
  case TIMELINE_CLEAR:
    return clearTimeline(state, action.timeline);
  case ACCOUNT_BLOCK_SUCCESS:
  case ACCOUNT_MUTE_SUCCESS:
    return filterTimelines(state, action.relationship, action.statuses);
  case ACCOUNT_UNFOLLOW_SUCCESS:
    return filterTimeline('home', state, action.relationship, action.statuses);
  case TIMELINE_SCROLL_TOP:
    return updateTop(state, action.timeline, action.top);
  case TIMELINE_CONNECT:
    return state.update(action.timeline, initialTimeline, map => map.set('online', true));
  case TIMELINE_DISCONNECT:
    return state.update(
      action.timeline,
      initialTimeline,
      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
    );
  default:
    return state;
  }
};
<commit_msg>[Glitch] Fix refreshing featured toots when the new collection is empty<commit_after>import {
  TIMELINE_UPDATE,
  TIMELINE_DELETE,
  TIMELINE_CLEAR,
  TIMELINE_EXPAND_SUCCESS,
  TIMELINE_EXPAND_REQUEST,
  TIMELINE_EXPAND_FAIL,
  TIMELINE_SCROLL_TOP,
  TIMELINE_CONNECT,
  TIMELINE_DISCONNECT,
} from 'flavours/glitch/actions/timelines';
import {
  ACCOUNT_BLOCK_SUCCESS,
  ACCOUNT_MUTE_SUCCESS,
  ACCOUNT_UNFOLLOW_SUCCESS,
} from 'flavours/glitch/actions/accounts';
import { Map as ImmutableMap, List as ImmutableList, fromJS } from 'immutable';
import compareId from 'flavours/glitch/util/compare_id';

const initialState = ImmutableMap();

const initialTimeline = ImmutableMap({
  unread: 0,
  online: false,
  top: true,
  isLoading: false,
  hasMore: true,
  items: ImmutableList(),
});

const expandNormalizedTimeline = (state, timeline, statuses, next, isPartial, isLoadingRecent) => {
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    mMap.set('isLoading', false);
    mMap.set('isPartial', isPartial);

    if (!next && !isLoadingRecent) mMap.set('hasMore', false);

    if (timeline.endsWith(':pinned')) {
      mMap.set('items', statuses.map(status => status.get('id')));
    } else if (!statuses.isEmpty()) {
      mMap.update('items', ImmutableList(), oldIds => {
        const newIds = statuses.map(status => status.get('id'));
        const lastIndex = oldIds.findLastIndex(id => id !== null && compareId(id, newIds.last()) >= 0) + 1;
        const firstIndex = oldIds.take(lastIndex).findLastIndex(id => id !== null && compareId(id, newIds.first()) > 0);

        if (firstIndex < 0) {
          return (isPartial ? newIds.unshift(null) : newIds).concat(oldIds.skip(lastIndex));
        }

        return oldIds.take(firstIndex + 1).concat(
          isPartial && oldIds.get(firstIndex) !== null ? newIds.unshift(null) : newIds,
          oldIds.skip(lastIndex)
        );
      });
    }
  }));
};

const updateTimeline = (state, timeline, status) => {
  const top        = state.getIn([timeline, 'top']);
  const ids        = state.getIn([timeline, 'items'], ImmutableList());
  const includesId = ids.includes(status.get('id'));
  const unread     = state.getIn([timeline, 'unread'], 0);

  if (includesId) {
    return state;
  }

  let newIds = ids;

  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    if (!top) mMap.set('unread', unread + 1);
    if (top && ids.size > 40) newIds = newIds.take(20);
    mMap.set('items', newIds.unshift(status.get('id')));
  }));
};

const deleteStatus = (state, id, accountId, references, exclude_account = null) => {
  state.keySeq().forEach(timeline => {
    if (exclude_account === null || (timeline !== `account:${exclude_account}` && !timeline.startsWith(`account:${exclude_account}:`)))
      state = state.updateIn([timeline, 'items'], list => list.filterNot(item => item === id));
  });

  // Remove reblogs of deleted status
  references.forEach(ref => {
    state = deleteStatus(state, ref[0], ref[1], [], exclude_account);
  });

  return state;
};

const clearTimeline = (state, timeline) => {
  return state.set(timeline, initialTimeline);
};

const filterTimelines = (state, relationship, statuses) => {
  let references;

  statuses.forEach(status => {
    if (status.get('account') !== relationship.id) {
      return;
    }

    references = statuses.filter(item => item.get('reblog') === status.get('id')).map(item => [item.get('id'), item.get('account')]);
    state      = deleteStatus(state, status.get('id'), status.get('account'), references, relationship.id);
  });

  return state;
};

const filterTimeline = (timeline, state, relationship, statuses) =>
  state.updateIn([timeline, 'items'], ImmutableList(), list =>
    list.filterNot(statusId =>
      statuses.getIn([statusId, 'account']) === relationship.id
    ));

const updateTop = (state, timeline, top) => {
  return state.update(timeline, initialTimeline, map => map.withMutations(mMap => {
    if (top) mMap.set('unread', 0);
    mMap.set('top', top);
  }));
};

export default function timelines(state = initialState, action) {
  switch(action.type) {
  case TIMELINE_EXPAND_REQUEST:
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', true));
  case TIMELINE_EXPAND_FAIL:
    return state.update(action.timeline, initialTimeline, map => map.set('isLoading', false));
  case TIMELINE_EXPAND_SUCCESS:
    return expandNormalizedTimeline(state, action.timeline, fromJS(action.statuses), action.next, action.partial, action.isLoadingRecent);
  case TIMELINE_UPDATE:
    return updateTimeline(state, action.timeline, fromJS(action.status));
  case TIMELINE_DELETE:
    return deleteStatus(state, action.id, action.accountId, action.references, action.reblogOf);
  case TIMELINE_CLEAR:
    return clearTimeline(state, action.timeline);
  case ACCOUNT_BLOCK_SUCCESS:
  case ACCOUNT_MUTE_SUCCESS:
    return filterTimelines(state, action.relationship, action.statuses);
  case ACCOUNT_UNFOLLOW_SUCCESS:
    return filterTimeline('home', state, action.relationship, action.statuses);
  case TIMELINE_SCROLL_TOP:
    return updateTop(state, action.timeline, action.top);
  case TIMELINE_CONNECT:
    return state.update(action.timeline, initialTimeline, map => map.set('online', true));
  case TIMELINE_DISCONNECT:
    return state.update(
      action.timeline,
      initialTimeline,
      map => map.set('online', false).update('items', items => items.first() ? items.unshift(null) : items)
    );
  default:
    return state;
  }
};
<|endoftext|>


<commit_before>// var mongoose = require('mongoose');
// // var File = mongoose.model('File');
// var User = mongoose.model('User');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require("fs"));

window.thisApp.controller('FileSelectorCtrl', function(AccountEditFactory, FileManagerFactory, $scope, $rootScope, $state){

 $scope.fileOptions = ['.bashrc', '.bash_profile', '.gitconfig', '.zshrc'];
 $scope.filePrefs = [];

  // retrieves current user's files
  $scope.retrieveAllFiles = function(){
    console.log('getting called');
    FileManagerFactory.getAllFiles()
    .then(function(files){
      console.log('files here:', files);
      $scope.files = files;
      $scope.$digest();
      console.log('updated user', $rootScope.currentUser);
    });
  }

 $scope.addFilePreference = function(filename){
    $scope.filePrefs = FileManagerFactory.addFilePrefs(filename, $scope.filePrefs);
 }

 //get file preferences from user upon signup
  $scope.addFilePrefToUser = function(){
    $rootScope.currentUser.filePreferences = $scope.filePrefs;
    AccountEditFactory.saveUserChanges();
    //child process that reads files from user
    $scope.getFileFromUser();
    //go to file manager state
    $state.go('loggedIn.fileManager');
  }

  $scope.filePaths = [];
  $scope.filePrefs = $rootScope.currentUser.filePreferences;
  $scope.getFileFromUser = function(){
    $scope.fileData = [];

    $scope.filePrefs.forEach(function(pref, ind){
      $scope.filePaths[ind] = process.env["HOME"]+ '/' + pref;
    })

    $scope.addFilesToUser();
  }

  $scope.addFilesToUser = function(){
     Promise.map($scope.filePaths, function(path, index){
      return Promise.all([fs.readFileAsync(path, "utf8"), path, $scope.filePrefs[index]])
    })
    .then(function(content){
      $scope.fileData = content;
      $scope.fileData.forEach(function(fileArray){
        FileManagerFactory.addFile(null, fileArray)
        .then(function(createdUser){
          $scope.retrieveAllFiles();
        })
      })
    })
  }

})<commit_msg>refactor in progress<commit_after>// var mongoose = require('mongoose');
// // var File = mongoose.model('File');
// var User = mongoose.model('User');
var Promise = require('bluebird');
var fs = Promise.promisifyAll(require("fs"));

window.thisApp.controller('FileSelectorCtrl', function(AccountEditFactory, FileManagerFactory, $scope, $rootScope, $state){

 $scope.fileOptions = ['.bashrc', '.bash_profile', '.gitconfig', '.zshrc'];
 $scope.filePrefs = [];

  // retrieves current user's files
  $scope.retrieveAllFiles = function(){
    console.log('getting called');
    FileManagerFactory.getAllFiles()
    .then(function(files){
      console.log('files here:', files);
      $scope.files = files;
      $scope.$digest();
      console.log('updated user', $rootScope.currentUser);
    });
  }

 $scope.addFilePreference = function(filename){
    $scope.filePrefs = FileManagerFactory.addFilePrefs(filename, $scope.filePrefs);
 }

 //get file preferences from user upon signup
  $scope.addFilePrefToUser = function(){
    $rootScope.currentUser.filePreferences = $scope.filePrefs;
    AccountEditFactory.saveUserChanges();
    //child process that reads files from user
    $scope.getFileFromUser();
    //go to file manager state
    $state.go('loggedIn.fileManager');
  }

  $scope.filePaths = [];
  $scope.filePrefs = $rootScope.currentUser.filePreferences;
  $scope.getFileFromUser = function(){
    $scope.fileData = [];

    $scope.filePrefs.forEach(function(pref, ind){
      $scope.filePaths[ind] = process.env["HOME"]+ '/' + pref;
    })

    $scope.addFilesToUser();
  }

  $scope.addFilesToUser = function(){
     Promise.map($scope.filePaths, function(path, index){
      return Promise.all([fs.readFileAsync(path, "utf8"), path, $scope.filePrefs[index]])
    })
    .then(function(content){
      $scope.fileData = content;
      $scope.fileData.forEach(function(fileArray){
        FileManagerFactory.addFile(null, fileArray)
        .then(function(createdUser){
          $scope.retrieveAllFiles();
        })
      })
    })
  }

})<|endoftext|>


<commit_before>import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import FileSaver from 'file-saver'
import gql from 'graphql-tag'
import JSZip from 'jszip'
import moment from 'moment'
import * as R from 'ramda'
import React, { memo } from 'react'

import { HoverableTooltip } from 'src/components/Tooltip'
import { IDButton, ActionButton } from 'src/components/buttons'
import { P, Label1 } from 'src/components/typography'
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
import { ReactComponent as PhoneIdInverseIcon } from 'src/styling/icons/ID/phone/white.svg'
import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg'
import { ReactComponent as CamIdInverseIcon } from 'src/styling/icons/ID/photo/white.svg'
import { ReactComponent as CamIdIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { URI } from 'src/utils/apollo'
import { toUnit, formatCryptoAddress } from 'src/utils/coin'
import { onlyFirstToUpper } from 'src/utils/string'

import CopyToClipboard from './CopyToClipboard'
import styles from './DetailsCard.styles'
import { getStatus, getStatusDetails } from './helper'

const useStyles = makeStyles(styles)
const MINUTES_OFFSET = 3
const TX_SUMMARY = gql`
  query txSummaryAndLogs(
    $txId: ID!
    $deviceId: ID!
    $limit: Int
    $from: DateTime
    $until: DateTime
    $txClass: String
  ) {
    serverLogsCsv(limit: $limit, from: $from, until: $until)
    machineLogsCsv(
      deviceId: $deviceId
      limit: $limit
      from: $from
      until: $until
    )
    transactionCsv(id: $txId, txClass: $txClass)
    txAssociatedDataCsv(id: $txId, txClass: $txClass)
  }
`

const formatAddress = (cryptoCode = '', address = '') =>
  formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')

const Label = ({ children }) => {
  const classes = useStyles()
  return <Label1 className={classes.label}>{children}</Label1>
}

const DetailsRow = ({ it: tx }) => {
  const classes = useStyles()

  const zip = new JSZip()

  const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
    onCompleted: data => createCsv(data)
  })

  const fiat = Number.parseFloat(tx.fiat)
  const crypto = toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode)
  const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2)
  const commission = Number(fiat * commissionPercentage).toFixed(2)
  const discount = tx.discount ? `-${tx.discount}%` : null
  const exchangeRate = BigNumber(fiat / crypto).toFormat(2)
  const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}`

  const customer = tx.customerIdCardData && {
    name: `${onlyFirstToUpper(
      tx.customerIdCardData.firstName
    )} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`,
    age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'),
    country: tx.customerIdCardData.country,
    idCardNumber: tx.customerIdCardData.documentNumber,
    idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format(
      'DD-MM-YYYY'
    )
  }

  const from = moment(tx.created)
    .subtract(MINUTES_OFFSET, 'm')
    .format()
  const until = moment(tx.created)
    .add(MINUTES_OFFSET, 'm')
    .format()

  const downloadRawLogs = ({ id: txId, deviceId, txClass }) => {
    fetchSummary({ variables: { txId, from, until, deviceId, txClass } })
  }

  const createCsv = async logs => {
    const zipFilename = `tx_${tx.id}_summary.zip`
    const filesNames = R.keys(logs)
    R.map(name => zip.file(name + '.csv', logs[name]), filesNames)
    const content = await zip.generateAsync({ type: 'blob' })
    FileSaver.saveAs(content, zipFilename)
  }

  const errorElements = (
    <>
      Transaction status
      {getStatus(tx)}
    </>
  )

  return (

          Direction

              {tx.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}

            {tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'}

          Available IDs
          <Box display="flex" flexDirection="row">
            {tx.customerPhone && (
              <IDButton
                className={classes.idButton}
                name="phone"
                Icon={PhoneIdIcon}
                InverseIcon={PhoneIdInverseIcon}>
                {tx.customerPhone}
              </IDButton>
            )}
            {tx.customerIdCardPhotoPath && !tx.customerIdCardData && (
              <IDButton
                popoverClassname={classes.clipboardPopover}
                className={classes.idButton}
                name="card"
                Icon={CardIdIcon}
                InverseIcon={CardIdInverseIcon}>
                <img
                  className={classes.idCardPhoto}
                  src={`${URI}/id-card-photo/${tx.customerIdCardPhotoPath}`}
                  alt=""
                />
              </IDButton>
            )}
            {tx.customerIdCardData && (
              <IDButton
                className={classes.idButton}
                name="card"
                Icon={CardIdIcon}
                InverseIcon={CardIdInverseIcon}>

                      Name

{customer.name}

                      Age

{customer.age}

                      Country

{customer.country}

                      ID number

{customer.idCardNumber}

                      Expiration date

{customer.idCardExpirationDate}

              </IDButton>
            )}
            {tx.customerFrontCameraPath && (
              <IDButton
                name="cam"
                Icon={CamIdIcon}
                InverseIcon={CamIdInverseIcon}>
                <img
                  src={`${URI}/front-camera-photo/${tx.customerFrontCameraPath}`}
                  alt=""
                />
              </IDButton>
            )}
          </Box>

          Exchange rate

{crypto > 0 ? displayExRate : '-'}

          Commission

            {`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}
            {discount && (

                <Label1 className={classes.chipLabel}>{discount}</Label1>

            )}

          Fixed fee

            {tx.txClass === 'cashIn'
              ? `${Number.parseFloat(tx.cashInFee)} ${tx.fiatCode}`
              : 'N/A'}

          Address

            <CopyToClipboard>
              {formatAddress(tx.cryptoCode, tx.toAddress)}
            </CopyToClipboard>

          Transaction ID

            {tx.txClass === 'cashOut' ? (
              'N/A'
            ) : (
              <CopyToClipboard>{tx.txHash}</CopyToClipboard>
            )}

          Session ID
          <CopyToClipboard>{tx.id}</CopyToClipboard>

          {getStatusDetails(tx) ? (
            <HoverableTooltip parentElements={errorElements} width={200}>

{getStatusDetails(tx)}

            </HoverableTooltip>
          ) : (
            errorElements
          )}

          {

 {
                downloadRawLogs(tx)
              }}>
              Other actions
              <ActionButton
                color="primary"
                Icon={Download}
                className={classes.downloadRawLogs}>
                {'Download raw logs'}
              </ActionButton>

          }

  )
}

export default memo(DetailsRow, (prev, next) => prev.id === next.id)
<commit_msg>refactor: remove syntactic sugar<commit_after>import { useLazyQuery } from '@apollo/react-hooks'
import { makeStyles, Box } from '@material-ui/core'
import BigNumber from 'bignumber.js'
import FileSaver from 'file-saver'
import gql from 'graphql-tag'
import JSZip from 'jszip'
import moment from 'moment'
import * as R from 'ramda'
import React, { memo } from 'react'

import { HoverableTooltip } from 'src/components/Tooltip'
import { IDButton, ActionButton } from 'src/components/buttons'
import { P, Label1 } from 'src/components/typography'
import { ReactComponent as CardIdInverseIcon } from 'src/styling/icons/ID/card/white.svg'
import { ReactComponent as CardIdIcon } from 'src/styling/icons/ID/card/zodiac.svg'
import { ReactComponent as PhoneIdInverseIcon } from 'src/styling/icons/ID/phone/white.svg'
import { ReactComponent as PhoneIdIcon } from 'src/styling/icons/ID/phone/zodiac.svg'
import { ReactComponent as CamIdInverseIcon } from 'src/styling/icons/ID/photo/white.svg'
import { ReactComponent as CamIdIcon } from 'src/styling/icons/ID/photo/zodiac.svg'
import { ReactComponent as Download } from 'src/styling/icons/button/download/zodiac.svg'
import { ReactComponent as TxInIcon } from 'src/styling/icons/direction/cash-in.svg'
import { ReactComponent as TxOutIcon } from 'src/styling/icons/direction/cash-out.svg'
import { URI } from 'src/utils/apollo'
import { toUnit, formatCryptoAddress } from 'src/utils/coin'
import { onlyFirstToUpper } from 'src/utils/string'

import CopyToClipboard from './CopyToClipboard'
import styles from './DetailsCard.styles'
import { getStatus, getStatusDetails } from './helper'

const useStyles = makeStyles(styles)
const MINUTES_OFFSET = 3
const TX_SUMMARY = gql`
  query txSummaryAndLogs(
    $txId: ID!
    $deviceId: ID!
    $limit: Int
    $from: DateTime
    $until: DateTime
    $txClass: String
  ) {
    serverLogsCsv(limit: $limit, from: $from, until: $until)
    machineLogsCsv(
      deviceId: $deviceId
      limit: $limit
      from: $from
      until: $until
    )
    transactionCsv(id: $txId, txClass: $txClass)
    txAssociatedDataCsv(id: $txId, txClass: $txClass)
  }
`

const formatAddress = (cryptoCode = '', address = '') =>
  formatCryptoAddress(cryptoCode, address).replace(/(.{5})/g, '$1 ')

const Label = ({ children }) => {
  const classes = useStyles()
  return <Label1 className={classes.label}>{children}</Label1>
}

const DetailsRow = ({ it: tx }) => {
  const classes = useStyles()

  const zip = new JSZip()

  const [fetchSummary] = useLazyQuery(TX_SUMMARY, {
    onCompleted: data => createCsv(data)
  })

  const fiat = Number.parseFloat(tx.fiat)
  const crypto = toUnit(new BigNumber(tx.cryptoAtoms), tx.cryptoCode)
  const commissionPercentage = Number.parseFloat(tx.commissionPercentage, 2)
  const commission = Number(fiat * commissionPercentage).toFixed(2)
  const discount = tx.discount ? `-${tx.discount}%` : null
  const exchangeRate = BigNumber(fiat / crypto).toFormat(2)
  const displayExRate = `1 ${tx.cryptoCode} = ${exchangeRate} ${tx.fiatCode}`

  const customer = tx.customerIdCardData && {
    name: `${onlyFirstToUpper(
      tx.customerIdCardData.firstName
    )} ${onlyFirstToUpper(tx.customerIdCardData.lastName)}`,
    age: moment().diff(moment(tx.customerIdCardData.dateOfBirth), 'years'),
    country: tx.customerIdCardData.country,
    idCardNumber: tx.customerIdCardData.documentNumber,
    idCardExpirationDate: moment(tx.customerIdCardData.expirationDate).format(
      'DD-MM-YYYY'
    )
  }

  const from = moment(tx.created)
    .subtract(MINUTES_OFFSET, 'm')
    .format()
  const until = moment(tx.created)
    .add(MINUTES_OFFSET, 'm')
    .format()

  const downloadRawLogs = ({ id: txId, deviceId, txClass }) => {
    fetchSummary({ variables: { txId, from, until, deviceId, txClass } })
  }

  const createCsv = async logs => {
    const zipFilename = `tx_${tx.id}_summary.zip`
    const filesNames = R.keys(logs)
    R.map(name => zip.file(name + '.csv', logs[name]), filesNames)
    const content = await zip.generateAsync({ type: 'blob' })
    FileSaver.saveAs(content, zipFilename)
  }

  const errorElements = (
    <>
      Transaction status
      {getStatus(tx)}
    </>
  )

  return (

          Direction

              {tx.txClass === 'cashOut' ? <TxOutIcon /> : <TxInIcon />}

            {tx.txClass === 'cashOut' ? 'Cash-out' : 'Cash-in'}

          Available IDs
          <Box display="flex" flexDirection="row">
            {tx.customerPhone && (
              <IDButton
                className={classes.idButton}
                name="phone"
                Icon={PhoneIdIcon}
                InverseIcon={PhoneIdInverseIcon}>
                {tx.customerPhone}
              </IDButton>
            )}
            {tx.customerIdCardPhotoPath && !tx.customerIdCardData && (
              <IDButton
                popoverClassname={classes.clipboardPopover}
                className={classes.idButton}
                name="card"
                Icon={CardIdIcon}
                InverseIcon={CardIdInverseIcon}>
                <img
                  className={classes.idCardPhoto}
                  src={`${URI}/id-card-photo/${tx.customerIdCardPhotoPath}`}
                  alt=""
                />
              </IDButton>
            )}
            {tx.customerIdCardData && (
              <IDButton
                className={classes.idButton}
                name="card"
                Icon={CardIdIcon}
                InverseIcon={CardIdInverseIcon}>

                      Name

{customer.name}

                      Age

{customer.age}

                      Country

{customer.country}

                      ID number

{customer.idCardNumber}

                      Expiration date

{customer.idCardExpirationDate}

              </IDButton>
            )}
            {tx.customerFrontCameraPath && (
              <IDButton
                name="cam"
                Icon={CamIdIcon}
                InverseIcon={CamIdInverseIcon}>
                <img
                  src={`${URI}/front-camera-photo/${tx.customerFrontCameraPath}`}
                  alt=""
                />
              </IDButton>
            )}
          </Box>

          Exchange rate

{crypto > 0 ? displayExRate : '-'}

          Commission

            {`${commission} ${tx.fiatCode} (${commissionPercentage * 100} %)`}
            {discount && (

                <Label1 className={classes.chipLabel}>{discount}</Label1>

            )}

          Fixed fee

            {tx.txClass === 'cashIn'
              ? `${Number.parseFloat(tx.cashInFee)} ${tx.fiatCode}`
              : 'N/A'}

          Address

            <CopyToClipboard>
              {formatAddress(tx.cryptoCode, tx.toAddress)}
            </CopyToClipboard>

          Transaction ID

            {tx.txClass === 'cashOut' ? (
              'N/A'
            ) : (
              <CopyToClipboard>{tx.txHash}</CopyToClipboard>
            )}

          Session ID
          <CopyToClipboard>{tx.id}</CopyToClipboard>

          {getStatusDetails(tx) ? (
            <HoverableTooltip parentElements={errorElements} width={200}>

{getStatusDetails(tx)}

            </HoverableTooltip>
          ) : (
            errorElements
          )}

          {

 downloadRawLogs(tx)}>
              Other actions
              <ActionButton
                color="primary"
                Icon={Download}
                className={classes.downloadRawLogs}>
                Download raw logs
              </ActionButton>

          }

  )
}

export default memo(DetailsRow, (prev, next) => prev.id === next.id)
<|endoftext|>


<commit_before>/*
 * Notes: the accounts array on an IsometricaUser is a list of objects with
 * an id (id of an account) and type (access type, can be one of 'owner' or 'member')
 */

module.exports = function(IsometricaUser) {

	IsometricaUser.observe('before save', function updateTimestamp(ctx, next) {

		//set the created/ updated timestamps

		if (ctx.instance) {
		  	//create
		  	ctx.instance.created = new Date();
		  	ctx.instance.updated = new Date();
		} else {
		  	//update
		  	ctx.data.updated = new Date();
		}

	  	next();

	});

};
<commit_msg>1.1 Added validations to the IsometricaUser model<commit_after>/*
 * Notes: the accounts array on an IsometricaUser is a list of objects with
 * an id (id of an account) and type (access type, can be one of 'owner' or 'member')
 */

module.exports = function(IsometricaUser) {

	IsometricaUser.observe('before save', function updateTimestamp(ctx, next) {

		//set the created/ updated timestamps

		if (ctx.instance) {
		  	//create
		  	ctx.instance.created = new Date();
		  	ctx.instance.updated = new Date();
		} else {
		  	//update
		  	ctx.data.updated = new Date();
		}

	  	next();

	});

	/**
	 * Model validations
	 *
	 * @note Password validation: min 6 chars long containing both letters and numbers
	 * @note Defining here rather than in the json file because the [docs say
	 * 		 so](http://docs.strongloop.com/display/public/LB/Model+definition+JSON+file;jsessionid=D25089B559F634FAEA33260B9C331A20#ModeldefinitionJSONfile-Validations)
	 * @author Steve Fortune
	 */
	IsometricaUser.validatesPresenceOf('firstName', 'lastName', 'email', 'password');
	IsometricaUser.validatesUniquenessOf('email', {
		message: 'A user is already registered with this email address.'
	});
	IsometricaUser.validatesFormatOf('email', {
		// @note This regex is copied straight from the Loopback documentation for validating
		// an email address here: http://docs.strongloop.com/display/public/LB/Validating+model+data.
		// In the future it might be worth registering a custom validator to do a more sophisticated
		// check.
		with: /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
		message: 'Email address should be in the format local-part@host'
	})
	IsometricaUser.validatesLengthOf('password', {
		min: 6,
		message: 'Password must be > 6 characters long'
	});
	IsometricaUser.validatesFormatOf('password', {
		with: /^(?=.*[a-zA-Z])(?=.*[0-9])/,
		message: 'Password must be > 6 characters long'
	});

};
<|endoftext|>


<commit_before>ReactionCore.Schemas.Coordinates = new SimpleSchema({
  x: {
    label: 'Longitude',
    type: Number,
    decimal: true
  },
  y: {
    label: 'Latitude',
    type: Number,
    decimal: true
  }
});

ReactionCore.Schemas.Location = new SimpleSchema({
  address1: {
    label: 'Address 1',
    type: String,
    optional: true
  },
  address2: {
    label: 'Address 2',
    type: String,
    optional: true
  },
  city: {
    type: String,
    label: 'City',
    optional: true
  },
  region: {
    label: 'State/Province/Region',
    type: String,
    optional: true
  },
  postal: {
    label: 'ZIP/Postal Code',
    type: String,
    optional: true
  },
  country: {
    type: String,
    label: 'Country',
    optional: true
  },
  coords: {
    type: ReactionCore.Schemas.Coordinates,
    optional: true
  },
  metafields: {
    type: [ReactionCore.Schemas.Metafield],
    optional: true
  }
});

ReactionCore.Schemas.ProductEvent = new SimpleSchema({
  _id: {
    type: String,
    defaultValue: Random.id()
  },
  createdAt: {
    type: Date,
    defaultValue: new Date()
  },
  title: {
    type: String
  },
  location: {
    type: ReactionCore.Schemas.Location,
    optional: true
  },
  description: {
    type: String,
    optional: true
  }
});

ReactionCore.Schemas.RentalProductVariant = new SimpleSchema([
  ReactionCore.Schemas.ProductVariant, {
    active: {
      type: Boolean,
      optional: true,
      defaultValue: true,
      index: 1
    },
    unavailableDates: {
      type: [Date],
      defaultValue: [],
      optional: true,
      index: 1
    },
    status: {
      type: String,
      optional: true
    },
    location: {
      label: 'Warehouse Storage Location',
      type: String,
      optional: true
    },
    currentLocation: {
      type: ReactionCore.Schemas.Location,
      optional: true
    },
    color: {
      type: String,
      optional: true
    },
    size: {
      type: String,
      optional: true
    },
    events: {
      type: [ReactionCore.Schemas.ProductEvent],
      optional: true,
      defaultValue: [
        {
          _id: Random.id(),
          createdAt: new Date(),
          title: 'Inbounded',
          description: 'Added to Inventory'
        }
      ]
    },
    pricePerDay: {
      label: 'Daily Rate',
      type: Number,
      defaultValue: 0.0,
      decimal: true,
      min: 0,
      optional: true
    },
    pricePerWeek: {
      label: 'Weekly Rate',
      type: Number,
      decimal: true,
      min: 0,
      optional: true
    }
  }
]);

ReactionCore.Schemas.RentalProduct = new SimpleSchema([
  ReactionCore.Schemas.Product, {
    variants: {
      type: [ReactionCore.Schemas.RentalProductVariant]
    },
    type: {
      type: String,
      defaultValue: 'rental'
    },
    cleaningBuffer: {
      type: Number,
      defaultValue: 0,
      optional: true
    },
    shopifyId: {
      type: String,
      optional: true
    }
  }
]);

// Update ProductVariant because it's checked against in core in certain methods.
ReactionCore.Schemas.ProductVariant = ReactionCore.Schemas.RentalProductVariant;
<commit_msg>Add productType to the schema. This is a big product category type that reflects Shopify's productType<commit_after>ReactionCore.Schemas.Coordinates = new SimpleSchema({
  x: {
    label: 'Longitude',
    type: Number,
    decimal: true
  },
  y: {
    label: 'Latitude',
    type: Number,
    decimal: true
  }
});

ReactionCore.Schemas.Location = new SimpleSchema({
  address1: {
    label: 'Address 1',
    type: String,
    optional: true
  },
  address2: {
    label: 'Address 2',
    type: String,
    optional: true
  },
  city: {
    type: String,
    label: 'City',
    optional: true
  },
  region: {
    label: 'State/Province/Region',
    type: String,
    optional: true
  },
  postal: {
    label: 'ZIP/Postal Code',
    type: String,
    optional: true
  },
  country: {
    type: String,
    label: 'Country',
    optional: true
  },
  coords: {
    type: ReactionCore.Schemas.Coordinates,
    optional: true
  },
  metafields: {
    type: [ReactionCore.Schemas.Metafield],
    optional: true
  }
});

ReactionCore.Schemas.ProductEvent = new SimpleSchema({
  _id: {
    type: String,
    defaultValue: Random.id()
  },
  createdAt: {
    type: Date,
    defaultValue: new Date()
  },
  title: {
    type: String
  },
  location: {
    type: ReactionCore.Schemas.Location,
    optional: true
  },
  description: {
    type: String,
    optional: true
  }
});

ReactionCore.Schemas.RentalProductVariant = new SimpleSchema([
  ReactionCore.Schemas.ProductVariant, {
    active: {
      type: Boolean,
      optional: true,
      defaultValue: true,
      index: 1
    },
    unavailableDates: {
      type: [Date],
      defaultValue: [],
      optional: true,
      index: 1
    },
    status: {
      type: String,
      optional: true
    },
    location: {
      label: 'Warehouse Storage Location',
      type: String,
      optional: true
    },
    currentLocation: {
      type: ReactionCore.Schemas.Location,
      optional: true
    },
    color: {
      type: String,
      optional: true
    },
    size: {
      type: String,
      optional: true
    },
    events: {
      type: [ReactionCore.Schemas.ProductEvent],
      optional: true,
      defaultValue: [
        {
          _id: Random.id(),
          createdAt: new Date(),
          title: 'Inbounded',
          description: 'Added to Inventory'
        }
      ]
    },
    pricePerDay: {
      label: 'Daily Rate',
      type: Number,
      defaultValue: 0.0,
      decimal: true,
      min: 0,
      optional: true
    },
    pricePerWeek: {
      label: 'Weekly Rate',
      type: Number,
      decimal: true,
      min: 0,
      optional: true
    }
  }
]);

ReactionCore.Schemas.RentalProduct = new SimpleSchema([
  ReactionCore.Schemas.Product, {
    variants: {
      type: [ReactionCore.Schemas.RentalProductVariant]
    },
    type: {
      type: String,
      defaultValue: 'rental'
    },
    productType: {
      type: String,
      index: 1,
      optional: true
    },
    cleaningBuffer: {
      type: Number,
      defaultValue: 0,
      optional: true
    },
    shopifyId: {
      type: String,
      optional: true
    }
  }
]);

// Update ProductVariant because it's checked against in core in certain methods.
ReactionCore.Schemas.ProductVariant = ReactionCore.Schemas.RentalProductVariant;
<|endoftext|>


<commit_before>/**
 * @author qiao / https://github.com/qiao
 * @author mrdoob / http://mrdoob.com
 * @author alteredq / http://alteredqualia.com/
 * @author WestLangley / http://github.com/WestLangley
 */

THREE.EditorControls = function ( object, domElement ) {

	domElement = ( domElement !== undefined ) ? domElement : document;

	// API

	this.enabled = true;
	this.center = new THREE.Vector3();
	this.panSpeed = 0.001;
	this.zoomSpeed = 0.001;
	this.rotationSpeed = 0.005;

	// internals

	var scope = this;
	var vector = new THREE.Vector3();

	var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
	var state = STATE.NONE;

	var center = this.center;
	var normalMatrix = new THREE.Matrix3();
	var pointer = new THREE.Vector2();
	var pointerOld = new THREE.Vector2();
	var spherical = new THREE.Spherical();

	// events

	var changeEvent = { type: 'change' };

	this.focus = function ( target ) {

		var box = new THREE.Box3().setFromObject( target );
		object.lookAt( center.copy( box.getCenter() ) );
		scope.dispatchEvent( changeEvent );

	};

	this.pan = function ( delta ) {

		var distance = object.position.distanceTo( center );

		delta.multiplyScalar( distance * scope.panSpeed );
		delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

		object.position.add( delta );
		center.add( delta );

		scope.dispatchEvent( changeEvent );

	};

	this.zoom = function ( delta ) {

		var distance = object.position.distanceTo( center );

		delta.multiplyScalar( distance * scope.zoomSpeed );

		if ( delta.length() > distance ) return;

		delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

		object.position.add( delta );

		scope.dispatchEvent( changeEvent );

	};

	this.rotate = function ( delta ) {

		vector.copy( object.position ).sub( center );

		spherical.setFromVector3( vector );

		spherical.theta += delta.x;
		spherical.phi += delta.y;

		spherical.makeSafe();

		vector.setFromSpherical( spherical );

		object.position.copy( center ).add( vector );

		object.lookAt( center );

		scope.dispatchEvent( changeEvent );

	};

	// mouse

	function onMouseDown( event ) {

		if ( scope.enabled === false ) return;

		if ( event.button === 0 ) {

			state = STATE.ROTATE;

		} else if ( event.button === 1 ) {

			state = STATE.ZOOM;

		} else if ( event.button === 2 ) {

			state = STATE.PAN;

		}

		pointerOld.set( event.clientX, event.clientY );

		domElement.addEventListener( 'mousemove', onMouseMove, false );
		domElement.addEventListener( 'mouseup', onMouseUp, false );
		domElement.addEventListener( 'mouseout', onMouseUp, false );
		domElement.addEventListener( 'dblclick', onMouseUp, false );

	}

	function onMouseMove( event ) {

		if ( scope.enabled === false ) return;

		pointer.set( event.clientX, event.clientY );

		var movementX = pointer.x - pointerOld.x;
		var movementY = pointer.y - pointerOld.y;

		if ( state === STATE.ROTATE ) {

			scope.rotate( new THREE.Vector3( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );

		} else if ( state === STATE.ZOOM ) {

			scope.zoom( new THREE.Vector3( 0, 0, movementY ) );

		} else if ( state === STATE.PAN ) {

			scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );

		}

		pointerOld.set( event.clientX, event.clientY );

	}

	function onMouseUp( event ) {

		domElement.removeEventListener( 'mousemove', onMouseMove, false );
		domElement.removeEventListener( 'mouseup', onMouseUp, false );
		domElement.removeEventListener( 'mouseout', onMouseUp, false );
		domElement.removeEventListener( 'dblclick', onMouseUp, false );

		state = STATE.NONE;

	}

	function onMouseWheel( event ) {

		event.preventDefault();

		// if ( scope.enabled === false ) return;

		scope.zoom( new THREE.Vector3( 0, 0, event.deltaY ) );

	}

	function contextmenu( event ) {

		event.preventDefault();

	}

	this.dispose = function() {

		domElement.removeEventListener( 'contextmenu', contextmenu, false );
		domElement.removeEventListener( 'mousedown', onMouseDown, false );
		domElement.removeEventListener( 'wheel', onMouseWheel, false );

		domElement.removeEventListener( 'mousemove', onMouseMove, false );
		domElement.removeEventListener( 'mouseup', onMouseUp, false );
		domElement.removeEventListener( 'mouseout', onMouseUp, false );
		domElement.removeEventListener( 'dblclick', onMouseUp, false );

		domElement.removeEventListener( 'touchstart', touchStart, false );
		domElement.removeEventListener( 'touchmove', touchMove, false );

	};

	domElement.addEventListener( 'contextmenu', contextmenu, false );
	domElement.addEventListener( 'mousedown', onMouseDown, false );
	domElement.addEventListener( 'wheel', onMouseWheel, false );

	// touch

	var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
	var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];

	var prevDistance = null;

	function touchStart( event ) {

		if ( scope.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				break;

			case 2:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
				prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
				break;

		}

		prevTouches[ 0 ].copy( touches[ 0 ] );
		prevTouches[ 1 ].copy( touches[ 1 ] );

	}

	function touchMove( event ) {

		if ( scope.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		function getClosest( touch, touches ) {

			var closest = touches[ 0 ];

			for ( var i in touches ) {

				if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ];

			}

			return closest;

		}

		switch ( event.touches.length ) {

			case 1:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
				break;

			case 2:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
				var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
				scope.zoom( new THREE.Vector3( 0, 0, prevDistance - distance ) );
				prevDistance = distance;

				var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
				var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
				offset0.x = - offset0.x;
				offset1.x = - offset1.x;

				scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );

				break;

		}

		prevTouches[ 0 ].copy( touches[ 0 ] );
		prevTouches[ 1 ].copy( touches[ 1 ] );

	}

	domElement.addEventListener( 'touchstart', touchStart, false );
	domElement.addEventListener( 'touchmove', touchMove, false );

};

THREE.EditorControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.EditorControls.prototype.constructor = THREE.EditorControls;
<commit_msg>Focus will frame target in view. Object will only translate to focus on target, will not rotate. Fixed a bug where empty's couldn't be focused on.<commit_after>/**
 * @author qiao / https://github.com/qiao
 * @author mrdoob / http://mrdoob.com
 * @author alteredq / http://alteredqualia.com/
 * @author WestLangley / http://github.com/WestLangley
 */

THREE.EditorControls = function ( object, domElement ) {

	domElement = ( domElement !== undefined ) ? domElement : document;

	// API

	this.enabled = true;
	this.center = new THREE.Vector3();
	this.panSpeed = 0.001;
	this.zoomSpeed = 0.001;
	this.rotationSpeed = 0.005;

	// internals

	var scope = this;
	var vector = new THREE.Vector3();

	var STATE = { NONE: - 1, ROTATE: 0, ZOOM: 1, PAN: 2 };
	var state = STATE.NONE;

	var center = this.center;
	var normalMatrix = new THREE.Matrix3();
	var pointer = new THREE.Vector2();
	var pointerOld = new THREE.Vector2();
	var spherical = new THREE.Spherical();

	// events

	var changeEvent = { type: 'change' };

	this.focus = function ( target ) {

		if ( target === undefined || !target.isObject3D ) {
			return;
		}

		var box = new THREE.Box3().setFromObject( target );

		var targetDistance;

		if ( box.isEmpty() ) {

			// Focusing on an empty such as a light.

			target.getWorldPosition( center );
			targetDistance = 0.5;

		} else {

			center.copy( box.getCenter() );
			targetDistance = box.getBoundingSphere().radius * 6;

		}

		var forwards = object.getWorldDirection().normalize();

		var targetDelta = forwards.multiplyScalar( -targetDistance );

		var targetWorldPosition = targetDelta.add( center );

		object.position.copy( targetWorldPosition );

		scope.dispatchEvent( changeEvent );

	};

	this.pan = function ( delta ) {

		var distance = object.position.distanceTo( center );

		delta.multiplyScalar( distance * scope.panSpeed );
		delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

		object.position.add( delta );
		center.add( delta );

		scope.dispatchEvent( changeEvent );

	};

	this.zoom = function ( delta ) {

		var distance = object.position.distanceTo( center );

		delta.multiplyScalar( distance * scope.zoomSpeed );

		if ( delta.length() > distance ) return;

		delta.applyMatrix3( normalMatrix.getNormalMatrix( object.matrix ) );

		object.position.add( delta );

		scope.dispatchEvent( changeEvent );

	};

	this.rotate = function ( delta ) {

		vector.copy( object.position ).sub( center );

		spherical.setFromVector3( vector );

		spherical.theta += delta.x;
		spherical.phi += delta.y;

		spherical.makeSafe();

		vector.setFromSpherical( spherical );

		object.position.copy( center ).add( vector );

		object.lookAt( center );

		scope.dispatchEvent( changeEvent );

	};

	// mouse

	function onMouseDown( event ) {

		if ( scope.enabled === false ) return;

		if ( event.button === 0 ) {

			state = STATE.ROTATE;

		} else if ( event.button === 1 ) {

			state = STATE.ZOOM;

		} else if ( event.button === 2 ) {

			state = STATE.PAN;

		}

		pointerOld.set( event.clientX, event.clientY );

		domElement.addEventListener( 'mousemove', onMouseMove, false );
		domElement.addEventListener( 'mouseup', onMouseUp, false );
		domElement.addEventListener( 'mouseout', onMouseUp, false );
		domElement.addEventListener( 'dblclick', onMouseUp, false );

	}

	function onMouseMove( event ) {

		if ( scope.enabled === false ) return;

		pointer.set( event.clientX, event.clientY );

		var movementX = pointer.x - pointerOld.x;
		var movementY = pointer.y - pointerOld.y;

		if ( state === STATE.ROTATE ) {

			scope.rotate( new THREE.Vector3( - movementX * scope.rotationSpeed, - movementY * scope.rotationSpeed, 0 ) );

		} else if ( state === STATE.ZOOM ) {

			scope.zoom( new THREE.Vector3( 0, 0, movementY ) );

		} else if ( state === STATE.PAN ) {

			scope.pan( new THREE.Vector3( - movementX, movementY, 0 ) );

		}

		pointerOld.set( event.clientX, event.clientY );

	}

	function onMouseUp( event ) {

		domElement.removeEventListener( 'mousemove', onMouseMove, false );
		domElement.removeEventListener( 'mouseup', onMouseUp, false );
		domElement.removeEventListener( 'mouseout', onMouseUp, false );
		domElement.removeEventListener( 'dblclick', onMouseUp, false );

		state = STATE.NONE;

	}

	function onMouseWheel( event ) {

		event.preventDefault();

		// if ( scope.enabled === false ) return;

		scope.zoom( new THREE.Vector3( 0, 0, event.deltaY ) );

	}

	function contextmenu( event ) {

		event.preventDefault();

	}

	this.dispose = function() {

		domElement.removeEventListener( 'contextmenu', contextmenu, false );
		domElement.removeEventListener( 'mousedown', onMouseDown, false );
		domElement.removeEventListener( 'wheel', onMouseWheel, false );

		domElement.removeEventListener( 'mousemove', onMouseMove, false );
		domElement.removeEventListener( 'mouseup', onMouseUp, false );
		domElement.removeEventListener( 'mouseout', onMouseUp, false );
		domElement.removeEventListener( 'dblclick', onMouseUp, false );

		domElement.removeEventListener( 'touchstart', touchStart, false );
		domElement.removeEventListener( 'touchmove', touchMove, false );

	};

	domElement.addEventListener( 'contextmenu', contextmenu, false );
	domElement.addEventListener( 'mousedown', onMouseDown, false );
	domElement.addEventListener( 'wheel', onMouseWheel, false );

	// touch

	var touches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];
	var prevTouches = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ];

	var prevDistance = null;

	function touchStart( event ) {

		if ( scope.enabled === false ) return;

		switch ( event.touches.length ) {

			case 1:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				break;

			case 2:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
				prevDistance = touches[ 0 ].distanceTo( touches[ 1 ] );
				break;

		}

		prevTouches[ 0 ].copy( touches[ 0 ] );
		prevTouches[ 1 ].copy( touches[ 1 ] );

	}

	function touchMove( event ) {

		if ( scope.enabled === false ) return;

		event.preventDefault();
		event.stopPropagation();

		function getClosest( touch, touches ) {

			var closest = touches[ 0 ];

			for ( var i in touches ) {

				if ( closest.distanceTo( touch ) > touches[ i ].distanceTo( touch ) ) closest = touches[ i ];

			}

			return closest;

		}

		switch ( event.touches.length ) {

			case 1:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				scope.rotate( touches[ 0 ].sub( getClosest( touches[ 0 ], prevTouches ) ).multiplyScalar( - scope.rotationSpeed ) );
				break;

			case 2:
				touches[ 0 ].set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY, 0 );
				touches[ 1 ].set( event.touches[ 1 ].pageX, event.touches[ 1 ].pageY, 0 );
				var distance = touches[ 0 ].distanceTo( touches[ 1 ] );
				scope.zoom( new THREE.Vector3( 0, 0, prevDistance - distance ) );
				prevDistance = distance;

				var offset0 = touches[ 0 ].clone().sub( getClosest( touches[ 0 ], prevTouches ) );
				var offset1 = touches[ 1 ].clone().sub( getClosest( touches[ 1 ], prevTouches ) );
				offset0.x = - offset0.x;
				offset1.x = - offset1.x;

				scope.pan( offset0.add( offset1 ).multiplyScalar( 0.5 ) );

				break;

		}

		prevTouches[ 0 ].copy( touches[ 0 ] );
		prevTouches[ 1 ].copy( touches[ 1 ] );

	}

	domElement.addEventListener( 'touchstart', touchStart, false );
	domElement.addEventListener( 'touchmove', touchMove, false );

};

THREE.EditorControls.prototype = Object.create( THREE.EventDispatcher.prototype );
THREE.EditorControls.prototype.constructor = THREE.EditorControls;
<|endoftext|>