<commit_before>elation.elements.registerType('vector3', {
  read(value) {
    if (value instanceof THREE.Vector3) {
      return value;
    } else if (elation.utils.isString(value) && value != 'null') {
      let vec3 = new THREE.Vector3();
      let arr = value.split(' ');
      vec3.fromArray(arr);
      return vec3;
    }
  },
  write(value) {
    if (value) {
      return value.toArray().join(' ');
    }
    return null;
  }
});
elation.elements.registerType('vector2', {
  read(value) {
    if (value instanceof THREE.Vector2) {
      return value;
    } else if (elation.utils.isString(value) && value != 'null') {
      let vec2 = new THREE.Vector2();
      let arr = value.split(' ');
      vec2.fromArray(arr);
      return vec2;
    }
  },
  write(value) {
    if (value) {
      return value.toArray().join(' ');
    }
    return null;
  }
});
elation.elements.registerType('euler', {
  read(value) {
    if (value instanceof THREE.Euler) {
      return value;
    } else if (elation.utils.isString(value)) {
      let euler = new THREE.Euler();
      let arr = value.split(' ');
      euler.fromArray(arr);
      return euler;
    }
  },
  write(value) {
    return value.toArray().join(' ');
  }
});
elation.elements.registerType('color', {
  read(value) {
    if (value instanceof THREE.Color) {
      return value;
    } else if (elation.utils.isString(value)) {
      let color = new THREE.Color();
      let arr = value.split(' ');
      color.fromArray(arr);
      return color;
    }
  },
  write(value) {
    return value.toArray().join(' ');
  }
});
elation.elements.define('janus-ui-editor-property', class extends elation.elements.ui.item {
  create() {
    super.create();
    this.defineAttributes({
      editing: { type: 'boolean', default: false },
      label: { type: 'string' },
      propertyname: { type: 'string' },
      value: { type: 'object' },
    });

    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
  }
  updateValue(value) {
    this.value = value;
  }
  render() {
  }
  focus() {
    this.editing = true;
  }
  resetChangeTimer() {
    if (this.changetimer) {
      clearTimeout(this.changetimer);
    }
    this.changetimer = setTimeout(() => {
      elation.events.fire({type: 'editorchange', element: this, data: this.value});
    }, 350);
  }
  handleInputFocus(ev) {
    this.editing = true;
    console.log('editing', this);
  }
  handleInputBlur(ev) {
    this.editing = false;
    console.log('stop editing', this);
  }
});

elation.elements.define('janus-ui-editor-property-integer', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'integer' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));
    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      if (this.value !== null && typeof this.value != 'undefined' && !isNaN(this.value)) {
        this.input.value = this.value.toString();
      } else {
        this.input.value = '';
      }
    }
  }
});
elation.elements.define('janus-ui-editor-property-float', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'float' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));
    this.input.addEventListener('input', (ev) => {
      this.value = this.input.value;
      this.resetChangeTimer();
    });
    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value;
    if (this.input) {
      if (this.value !== null && typeof this.value != 'undefined' && !isNaN(this.value)) {
        this.input.value = +this.value.toFixed(4);
      } else {
        this.input.value = '';
      }
    }
  }
});
elation.elements.define('janus-ui-editor-property-string', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'string' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    this.input.addEventListener('change', (ev) => this.handleInputChange(ev));
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));

    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value || '';
    if (this.input) {
      this.input.value = this.value;
    }
  }
  focus() {
    this.input.focus();
    this.input.select();
  }
  handleInputChange(ev) {
    this.value = this.input.value;
    //this.resetChangeTimer();
    elation.events.fire({type: 'editorchange', element: this, data: this.value});
  }
});
elation.elements.define('janus-ui-editor-property-vector3', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'vector3' },
      constraint: { type: 'string' },
    });

    this.inputs = [];
    let inputs = 'xyz'.split('');
    let i = 0;
    inputs.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
        if (this.inputs[0].value != this.value.x) this.value.x = +this.inputs[0].value;
        if (this.inputs[1].value != this.value.y) this.value.y = +this.inputs[1].value;
        if (this.inputs[2].value != this.value.z) this.value.z = +this.inputs[2].value;
        //elation.events.fire({type: 'editorchange', element: this})
        this.resetChangeTimer();
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.inputs && this.value instanceof THREE.Vector3) {
      this.inputs[0].value = +this.value.x.toFixed(4);
      this.inputs[1].value = +this.value.y.toFixed(4);
      this.inputs[2].value = +this.value.z.toFixed(4);
    }
  }
  render() {
  }
});
elation.elements.define('janus-ui-editor-property-vector2', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'vector2' },
      constraint: { type: 'string' },
    });

    this.inputs = [];
    let inputs = 'xy'.split('');
    let i = 0;
    inputs.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
        if (this.inputs[0].value != this.value.x) this.value.x = +this.inputs[0].value;
        if (this.inputs[1].value != this.value.y) this.value.y = +this.inputs[1].value;
        //elation.events.fire({type: 'editorchange', element: this})
        this.resetChangeTimer();
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.inputs && this.value instanceof THREE.Vector2) {
      this.inputs[0].value = +this.value.x.toFixed(4);
      this.inputs[1].value = +this.value.y.toFixed(4);
    }
  }
  render() {
  }
});
elation.elements.define('janus-ui-editor-property-euler', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'euler' },
    });

    this.elements = elation.elements.fromString(`
      <ui-input name="x" label="x"></ui-input>
      <ui-input name="y" label="y"></ui-input>
      <ui-input name="z" label="z"></ui-input>
    `, this);
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      this.elements.x.value = this.value.x * THREE.Math.RAD2DEG;
      this.elements.y.value = this.value.y * THREE.Math.RAD2DEG;
      this.elements.z.value = this.value.z * THREE.Math.RAD2DEG;
    }
  }
});
elation.elements.define('janus-ui-editor-property-color', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      label: { type: 'string' },
      value: { type: 'color' },
    });
/*
    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
*/
    this.inputs = [];
    let inputnames = 'rgb'.split('');
    let i = 0;
    inputnames.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
/*
        if (this.inputs[0].value != this.value.r) this.value.r = this.inputs[0].value / 255;
        if (this.inputs[1].value != this.value.g) this.value.g = this.inputs[1].value / 255;
        if (this.inputs[2].value != this.value.b) this.value.b = this.inputs[2].value / 255;
*/
        if (this.inputs[0].value != this.value.r ||
            this.inputs[1].value != this.value.g ||
            this.inputs[2].value != this.value.b) {
          this.value = new THREE.Color(this.inputs[0].value / 255, this.inputs[1].value / 255, this.inputs[2].value / 255);
          this.colorpicker.value = '#' + this.value.getHexString();
          this.resetChangeTimer();
        }
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));

    this.colorpicker = elation.elements.create('input', {
      type: 'color',
      name: 'color',
      append: this
    });
    elation.events.add(this.colorpicker, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.colorpicker, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
    this.colorpicker.addEventListener('input', (ev) => {
      this.updateValue(new THREE.Color(this.colorpicker.value));
      //this.resetChangeTimer();
      elation.events.fire({type: 'editorchange', element: this, data: this.value});
    });
  }
  createInputs(type='rgb') {
    super.createInputs(type);
  }
  updateValue(value) {
    this.value = value;
    if (this.colorpicker) {
      this.colorpicker.value = '#' + value.getHexString();
      this.inputs[0].value = (value.r * 255) | 0;
      this.inputs[1].value = (value.g * 255) | 0;
      this.inputs[2].value = (value.b * 255) | 0;
    }
  }
});
elation.elements.define('janus-ui-editor-property-boolean', class extends elation.elements.janus.ui.editor.property {
  create() {
    this.defineAttributes({
      label: { type: 'string' },
      value: { type: 'boolean' },
    });
    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
    this.elements = elation.elements.fromString(`
      <ui-toggle name="toggle"></ui-toggle>
    `, this);
    elation.events.add(this.elements.toggle, 'toggle', (ev) => {
      this.value = ev.target.checked;
      elation.events.fire({type: 'editorchange', element: this, data: this.value})
    });
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      this.elements.toggle.checked = value;
    }
  }
});
<commit_msg>Handle null values for color and vectors<commit_after>elation.elements.registerType('vector3', {
  read(value) {
    if (value instanceof THREE.Vector3) {
      return value;
    } else if (elation.utils.isString(value) && value != 'null') {
      let vec3 = new THREE.Vector3();
      let arr = value.split(' ');
      vec3.fromArray(arr);
      return vec3;
    }
  },
  write(value) {
    if (value) {
      return value.toArray().join(' ');
    }
    return null;
  }
});
elation.elements.registerType('vector2', {
  read(value) {
    if (value instanceof THREE.Vector2) {
      return value;
    } else if (elation.utils.isString(value) && value != 'null') {
      let vec2 = new THREE.Vector2();
      let arr = value.split(' ');
      vec2.fromArray(arr);
      return vec2;
    }
  },
  write(value) {
    if (value) {
      return value.toArray().join(' ');
    }
    return null;
  }
});
elation.elements.registerType('euler', {
  read(value) {
    if (value instanceof THREE.Euler) {
      return value;
    } else if (elation.utils.isString(value)) {
      let euler = new THREE.Euler();
      let arr = value.split(' ');
      euler.fromArray(arr);
      return euler;
    }
  },
  write(value) {
    return value.toArray().join(' ');
  }
});
elation.elements.registerType('color', {
  read(value) {
    if (value instanceof THREE.Color) {
      return value;
    } else if (elation.utils.isString(value)) {
      let color = new THREE.Color();
      let arr = value.split(' ');
      color.fromArray(arr);
      return color;
    }
  },
  write(value) {
    return (value ? value.toArray().join(' ') : 'null');
  }
});
elation.elements.define('janus-ui-editor-property', class extends elation.elements.ui.item {
  create() {
    super.create();
    this.defineAttributes({
      editing: { type: 'boolean', default: false },
      label: { type: 'string' },
      propertyname: { type: 'string' },
      value: { type: 'object' },
    });

    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
  }
  updateValue(value) {
    this.value = value;
  }
  render() {
  }
  focus() {
    this.editing = true;
  }
  resetChangeTimer() {
    if (this.changetimer) {
      clearTimeout(this.changetimer);
    }
    this.changetimer = setTimeout(() => {
      elation.events.fire({type: 'editorchange', element: this, data: this.value});
    }, 350);
  }
  handleInputFocus(ev) {
    this.editing = true;
    console.log('editing', this);
  }
  handleInputBlur(ev) {
    this.editing = false;
    console.log('stop editing', this);
  }
});

elation.elements.define('janus-ui-editor-property-integer', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'integer' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));
    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      if (this.value !== null && typeof this.value != 'undefined' && !isNaN(this.value)) {
        this.input.value = this.value.toString();
      } else {
        this.input.value = '';
      }
    }
  }
});
elation.elements.define('janus-ui-editor-property-float', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'float' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));
    this.input.addEventListener('input', (ev) => {
      this.value = this.input.value;
      this.resetChangeTimer();
    });
    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value;
    if (this.input) {
      if (this.value !== null && typeof this.value != 'undefined' && !isNaN(this.value)) {
        this.input.value = +this.value.toFixed(4);
      } else {
        this.input.value = '';
      }
    }
  }
});
elation.elements.define('janus-ui-editor-property-string', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'string' },
    });
    this.input = elation.elements.create('ui-input', { name: 'value', label: '', append: this });
    this.input.addEventListener('change', (ev) => this.handleInputChange(ev));
    elation.events.add(this.input, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.input, 'blur', (ev) => this.handleInputBlur(ev));

    this.updateValue(this.value);
  }
  render() {
  }
  updateValue(value) {
    this.value = value || '';
    if (this.input) {
      this.input.value = this.value;
    }
  }
  focus() {
    this.input.focus();
    this.input.select();
  }
  handleInputChange(ev) {
    this.value = this.input.value;
    //this.resetChangeTimer();
    elation.events.fire({type: 'editorchange', element: this, data: this.value});
  }
});
elation.elements.define('janus-ui-editor-property-vector3', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'vector3' },
      constraint: { type: 'string' },
    });

    this.inputs = [];
    let inputs = 'xyz'.split('');
    let i = 0;
    inputs.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
        if (this.inputs[0].value != this.value.x) this.value.x = +this.inputs[0].value;
        if (this.inputs[1].value != this.value.y) this.value.y = +this.inputs[1].value;
        if (this.inputs[2].value != this.value.z) this.value.z = +this.inputs[2].value;
        //elation.events.fire({type: 'editorchange', element: this})
        this.resetChangeTimer();
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.inputs && this.value instanceof THREE.Vector3) {
      this.inputs[0].value = +this.value.x.toFixed(4);
      this.inputs[1].value = +this.value.y.toFixed(4);
      this.inputs[2].value = +this.value.z.toFixed(4);
    }
  }
  render() {
  }
});
elation.elements.define('janus-ui-editor-property-vector2', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'vector2' },
      constraint: { type: 'string' },
    });

    this.inputs = [];
    let inputs = 'xy'.split('');
    let i = 0;
    inputs.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
        if (this.inputs[0].value != this.value.x) this.value.x = +this.inputs[0].value;
        if (this.inputs[1].value != this.value.y) this.value.y = +this.inputs[1].value;
        //elation.events.fire({type: 'editorchange', element: this})
        this.resetChangeTimer();
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.inputs && this.value instanceof THREE.Vector2) {
      this.inputs[0].value = +this.value.x.toFixed(4);
      this.inputs[1].value = +this.value.y.toFixed(4);
    }
  }
  render() {
  }
});
elation.elements.define('janus-ui-editor-property-euler', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      value: { type: 'euler' },
    });

    this.elements = elation.elements.fromString(`
      <ui-input name="x" label="x"></ui-input>
      <ui-input name="y" label="y"></ui-input>
      <ui-input name="z" label="z"></ui-input>
    `, this);
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      this.elements.x.value = this.value.x * THREE.Math.RAD2DEG;
      this.elements.y.value = this.value.y * THREE.Math.RAD2DEG;
      this.elements.z.value = this.value.z * THREE.Math.RAD2DEG;
    }
  }
});
elation.elements.define('janus-ui-editor-property-color', class extends elation.elements.janus.ui.editor.property {
  create() {
    super.create();
    this.defineAttributes({
      label: { type: 'string' },
      value: { type: 'color' },
    });
/*
    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
*/
    this.inputs = [];
    let inputnames = 'rgb'.split('');
    let i = 0;
    inputnames.forEach(t => {
      let input = elation.elements.create('ui-input', {
        name: t,
        label: t,
        append: this
      });
      elation.events.add(input, 'input', (ev) => {
/*
        if (this.inputs[0].value != this.value.r) this.value.r = this.inputs[0].value / 255;
        if (this.inputs[1].value != this.value.g) this.value.g = this.inputs[1].value / 255;
        if (this.inputs[2].value != this.value.b) this.value.b = this.inputs[2].value / 255;
*/
        if (this.inputs[0].value != this.value.r ||
            this.inputs[1].value != this.value.g ||
            this.inputs[2].value != this.value.b) {
          this.value = new THREE.Color(this.inputs[0].value / 255, this.inputs[1].value / 255, this.inputs[2].value / 255);
          this.colorpicker.value = '#' + this.value.getHexString();
          this.resetChangeTimer();
        }
      });
      this.inputs.push(input);
    });
    elation.events.add(this.inputs, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.inputs, 'blur', (ev) => this.handleInputBlur(ev));

    this.colorpicker = elation.elements.create('input', {
      type: 'color',
      name: 'color',
      append: this
    });
    elation.events.add(this.colorpicker, 'focus', (ev) => this.handleInputFocus(ev));
    elation.events.add(this.colorpicker, 'blur', (ev) => this.handleInputBlur(ev));
    if (this.value) {
      this.updateValue(this.value);
    }
    this.colorpicker.addEventListener('input', (ev) => {
      this.updateValue(new THREE.Color(this.colorpicker.value));
      //this.resetChangeTimer();
      elation.events.fire({type: 'editorchange', element: this, data: this.value});
    });
  }
  createInputs(type='rgb') {
    super.createInputs(type);
  }
  updateValue(value) {
    this.value = value;
    if (this.colorpicker && value) {
      this.colorpicker.value = '#' + value.getHexString();
      this.inputs[0].value = (value.r * 255) | 0;
      this.inputs[1].value = (value.g * 255) | 0;
      this.inputs[2].value = (value.b * 255) | 0;
    }
  }
});
elation.elements.define('janus-ui-editor-property-boolean', class extends elation.elements.janus.ui.editor.property {
  create() {
    this.defineAttributes({
      label: { type: 'string' },
      value: { type: 'boolean' },
    });
    if (this.label) {
      elation.elements.create('ui-text', {
        text: this.label,
        append: this
      });
    }
    this.elements = elation.elements.fromString(`
      <ui-toggle name="toggle"></ui-toggle>
    `, this);
    elation.events.add(this.elements.toggle, 'toggle', (ev) => {
      this.value = ev.target.checked;
      elation.events.fire({type: 'editorchange', element: this, data: this.value})
    });
    if (this.value) {
      this.updateValue(this.value);
    }
  }
  updateValue(value) {
    this.value = value;
    if (this.elements) {
      this.elements.toggle.checked = value;
    }
  }
});
<|endoftext|>


<commit_before>/* global __filename, module, require */
var logger = require('jitsi-meet-logger').getLogger(__filename);
var MediaType = require('../../service/RTC/MediaType');
var RTCBrowserType = require('../RTC/RTCBrowserType');
var RTCEvents = require('../../service/RTC/RTCEvents');

import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
import * as JitsiTrackEvents from '../../JitsiTrackEvents';
import Statistics from '../statistics/statistics';

/**
 * Default value of 2000 milliseconds for
 * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
 *
 * @type {number}
 */
const DEFAULT_RTC_MUTE_TIMEOUT = 2000;

/**
 * Class is responsible for emitting
 * JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED events.
 */
export default class ParticipantConnectionStatus {
    /**
     * Creates new instance of <tt>ParticipantConnectionStatus</tt>.
     *
     * @constructor
     * @param {RTC} rtc the RTC service instance
     * @param {JitsiConference} conference parent conference instance
     * @param {number} rtcMuteTimeout (optional) custom value for
     * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
     */
    constructor(rtc, conference, rtcMuteTimeout) {
        this.rtc = rtc;
        this.conference = conference;
        /**
         * A map of the "endpoint ID"(which corresponds to the resource part
         * of MUC JID(nickname)) to the timeout callback IDs scheduled using
         * window.setTimeout.
         * @type {Object.<string, number>}
         */
        this.trackTimers = {};
        /**
         * This map holds the endpoint connection status received from the JVB
         * (as it might be different than the one stored in JitsiParticipant).
         * Required for getting back in sync when remote video track is removed.
         * @type {Object.<string, boolean>}
         */
        this.connStatusFromJvb = { };
        /**
         * How long we're going to wait after the RTC video track muted event
         * for the corresponding signalling mute event, before the connection
         * interrupted is fired. The default value is
         * {@link DEFAULT_RTC_MUTE_TIMEOUT}.
         *
         * @type {number} amount of time in milliseconds
         */
        this.rtcMuteTimeout
            = typeof rtcMuteTimeout === 'number'
                ? rtcMuteTimeout : DEFAULT_RTC_MUTE_TIMEOUT;
        /**
         * This map holds a timestamp indicating  when participant's video track
         * was RTC muted (it is assumed that each participant can have only 1
         * video track at a time). The purpose of storing the timestamp is to
         * avoid the transition to disconnected status in case of legitimate
         * video mute operation where the signalling video muted event can
         * arrive shortly after RTC muted event.
         *
         * The key is participant's ID which is the same as endpoint id in
         * the Colibri conference allocated on the JVB.
         *
         * The value is a timestamp measured in milliseconds obtained with
         * <tt>Date.now()</tt>.
         *
         * FIXME merge this logic with NO_DATA_FROM_SOURCE event
         *       implemented in JitsiLocalTrack by extending the event to
         *       the remote track and allowing to set different timeout for
         *       local and remote tracks.
         *
         * @type {Object.<string, number>}
         */
        this.rtcMutedTimestamp = { };
        logger.info("RtcMuteTimeout set to: " + this.rtcMuteTimeout);
    }

    /**
     * Initializes <tt>ParticipantConnectionStatus</tt> and bind required event
     * listeners.
     */
    init() {

        this._onEndpointConnStatusChanged
            = this.onEndpointConnStatusChanged.bind(this);

        this.rtc.addListener(
            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
            this._onEndpointConnStatusChanged);

        // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
        // events for video type tracks when they stop receiving data which is
        // often a sign that remote user is having connectivity issues
        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {

            this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
            this.rtc.addListener(
                RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);

            this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
            this.rtc.addListener(
                RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);

            // Track added/removed listeners are used to bind "mute"/"unmute"
            // event handlers
            this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
            this.conference.on(
                JitsiConferenceEvents.TRACK_ADDED,
                this._onRemoteTrackAdded);

            this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
            this.conference.on(
                JitsiConferenceEvents.TRACK_REMOVED,
                this._onRemoteTrackRemoved);

            // Listened which will be bound to JitsiRemoteTrack to listen for
            // signalling mute/unmute events.
            this._onSignallingMuteChanged
                = this.onSignallingMuteChanged.bind(this);
        }
    }

    /**
     * Removes all event listeners and disposes of all resources held by this
     * instance.
     */
    dispose() {

        this.rtc.removeListener(
            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
            this._onEndpointConnStatusChanged);

        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
            this.rtc.removeListener(
                RTCEvents.REMOTE_TRACK_MUTE,
                this._onTrackRtcMuted);
            this.rtc.removeListener(
                RTCEvents.REMOTE_TRACK_UNMUTE,
                this._onTrackRtcUnmuted);

            this.conference.off(
                JitsiConferenceEvents.TRACK_ADDED,
                this._onRemoteTrackAdded);
            this.conference.off(
                JitsiConferenceEvents.TRACK_REMOVED,
                this._onRemoteTrackRemoved);
        }

        Object.keys(this.trackTimers).forEach(function (participantId) {
            this.clearTimeout(participantId);
            this.clearRtcMutedTimestamp(participantId);
        }.bind(this));

        // Clear RTC connection status cache
        this.connStatusFromJvb = {};
    }

    /**
     * Handles RTCEvents.ENDPOINT_CONN_STATUS_CHANGED triggered when we receive
     * notification over the data channel from the bridge about endpoint's
     * connection status update.
     * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
     * @param isActive {boolean} true if the connection is OK or false otherwise
     */
    onEndpointConnStatusChanged(endpointId, isActive) {

        logger.debug(
            'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
                + Date.now() +'): ' + endpointId + ': ' + isActive);

        // Filter out events for the local JID for now
        if (endpointId !== this.conference.myUserId()) {
            // Store the status received over the data channels
            this.connStatusFromJvb[endpointId] = isActive;
            this.figureOutConnectionStatus(endpointId);
        }
    }

    _changeConnectionStatus(participant, newStatus) {
        if (participant.isConnectionActive() !== newStatus) {

            const endpointId = participant.getId();

            participant._setIsConnectionActive(newStatus);

            logger.debug(
                'Emit endpoint conn status(' + Date.now() + ') '
                    + endpointId + ": " + newStatus);

            // Log the event on CallStats
            Statistics.sendLog(
                JSON.stringify({
                    id: 'peer.conn.status',
                    participant: endpointId,
                    status: newStatus
                }));

            // and analytics
            Statistics.analytics.sendEvent('peer.conn.status',
                {label: newStatus});

            this.conference.eventEmitter.emit(
                JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
                endpointId, newStatus);
        }
    }

    /**
     * Reset the postponed "connection interrupted" event which was previously
     * scheduled as a timeout on RTC 'onmute' event.
     *
     * @param participantId the participant for which the "connection
     * interrupted" timeout was scheduled
     */
    clearTimeout(participantId) {
        if (this.trackTimers[participantId]) {
            window.clearTimeout(this.trackTimers[participantId]);
            this.trackTimers[participantId] = null;
        }
    }

    /**
     * Clears the timestamp of the RTC muted event for participant's video track
     * @param participantId the id of the conference participant which is
     * the same as the Colibri endpoint ID of the video channel allocated for
     * the user on the videobridge.
     */
    clearRtcMutedTimestamp(participantId) {
        this.rtcMutedTimestamp[participantId] = null;
    }

    /**
     * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
     * a new one is added to the conference.
     *
     * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
     * the conference.
     */
    onRemoteTrackAdded(remoteTrack) {
        if (!remoteTrack.isLocal()
                && remoteTrack.getType() === MediaType.VIDEO) {

            logger.debug(
                'Detector on remote track added for: '
                    + remoteTrack.getParticipantId());

            remoteTrack.on(
                JitsiTrackEvents.TRACK_MUTE_CHANGED,
                this._onSignallingMuteChanged);
        }
    }

    /**
     * Removes all event listeners bound to the remote video track and clears
     * any related timeouts.
     *
     * @param {JitsiRemoteTrack} remoteTrack the remote track which is being
     * removed from the conference.
     */
    onRemoteTrackRemoved(remoteTrack) {
        if (!remoteTrack.isLocal()
                && remoteTrack.getType() === MediaType.VIDEO) {

            const endpointId = remoteTrack.getParticipantId();

            logger.debug(
                'Detector on remote track removed: ' + endpointId);

            remoteTrack.off(
                JitsiTrackEvents.TRACK_MUTE_CHANGED,
                this._onSignallingMuteChanged);

            this.clearTimeout(endpointId);
            this.clearRtcMutedTimestamp(endpointId);

            this.figureOutConnectionStatus(endpointId);
        }
    }

    /**
     * Checks if given participant's video is considered frozen.
     * @param {JitsiParticipant} participant
     * @return {boolean} <tt>true</tt> if the video has frozen for given
     * participant or <tt>false</tt> when it's either not considered frozen
     * (yet) or if freeze detection is not supported by the current browser.
     *
     * FIXME merge this logic with NO_DATA_FROM_SOURCE event
     *       implemented in JitsiLocalTrack by extending the event to
     *       the remote track and allowing to set different timeout for
     *       local and remote tracks.
     *
     */
    isVideoTrackFrozen (participant) {
        if (!RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
            return false;
        }

        const hasAnyVideoRTCMuted = participant.hasAnyVideoTrackWebRTCMuted();
        const rtcMutedTimestamp
            = this.rtcMutedTimestamp[participant.getId()];

        return hasAnyVideoRTCMuted
            && typeof rtcMutedTimestamp === 'number'
            && (Date.now() - rtcMutedTimestamp) >= this.rtcMuteTimeout;
    }

    /**
     * Figures out (and updates) the current connectivity status for
     * the participant identified by the given id.
     *
     * @param {string} id the participant's id (MUC nickname or Colibri endpoint
     * ID).
     */
    figureOutConnectionStatus(id) {
        const participant = this.conference.getParticipantById(id);
        if (!participant) {
            // Probably the participant is no longer in the conference
            // (at the time of writing this code, participant is
            // detached from the conference and TRACK_REMOVED events are
            // fired),
            // so we don't care, but let's print the warning for
            // debugging purpose
            logger.warn('figure out conn status - no participant for: ' + id);
            return;
        }

        const isVideoMuted = participant.isVideoMuted();
        const isVideoTrackFrozen = this.isVideoTrackFrozen(participant);
        let isConnActiveByJvb = this.connStatusFromJvb[id];

        // If no status was received from the JVB it means that it's active
        // (the bridge does not send notification unless there is a problem).
        if (typeof isConnActiveByJvb !== 'boolean') {
            logger.debug('Assuming connection active by JVB - no notification');
            isConnActiveByJvb = true;
        }

        let isConnectionActive = true;
        if (!isVideoMuted && (isVideoTrackFrozen || !isConnActiveByJvb)) {
            // Disconnected when not video muted and either got that status from
            // JVB or the video track is "RTC muted"
            isConnectionActive = false;
        } else if (isVideoMuted && !isConnActiveByJvb){
            // If is video muted, but JVB reports no data
            isConnectionActive = false;
        }

        logger.debug(
            'Figure out conn status, is video muted: ' + isVideoMuted
                + ' is active(jvb): ' + isConnActiveByJvb
                + ' video track frozen: ' + isVideoTrackFrozen
                + ' => ' + isConnectionActive);

        this._changeConnectionStatus(participant, isConnectionActive);
    }

    /**
     * Handles RTC 'onmute' event for the video track.
     *
     * @param {JitsiRemoteTrack} track the video track for which 'onmute' event
     * will be processed.
     */
    onTrackRtcMuted(track) {
        const participantId = track.getParticipantId();
        const participant = this.conference.getParticipantById(participantId);
        logger.debug('Detector track RTC muted: ' + participantId);
        if (!participant) {
            logger.error('No participant for id: ' + participantId);
            return;
        }
        this.rtcMutedTimestamp[participantId] = Date.now();
        if (!participant.isVideoMuted()) {
            // If the user is not muted according to the signalling we'll give
            // it some time, before the connection interrupted event is
            // triggered.
            this.clearTimeout(participantId);
            this.trackTimers[participantId] = window.setTimeout(function () {
                logger.debug('RTC mute timeout for: ' + participantId);
                this.clearTimeout(participantId);
                this.figureOutConnectionStatus(participantId);
            }.bind(this), this.rtcMuteTimeout);
        }
    }

    /**
     * Handles RTC 'onunmute' event for the video track.
     *
     * @param {JitsiRemoteTrack} track the video track for which 'onunmute'
     * event will be processed.
     */
    onTrackRtcUnmuted(track) {
        const participantId = track.getParticipantId();

        logger.debug('Detector track RTC unmuted: ' + participantId);

        this.clearTimeout(participantId);
        this.clearRtcMutedTimestamp(participantId);

        this.figureOutConnectionStatus(participantId);
    }

    /**
     * Here the signalling "mute"/"unmute" events are processed.
     *
     * @param {JitsiRemoteTrack} track the remote video track for which
     * the signalling mute/unmute event will be processed.
     */
    onSignallingMuteChanged (track) {
        const participantId = track.getParticipantId();

        logger.debug(
            'Detector on track signalling mute changed: '
                + participantId, track.isMuted());

        this.figureOutConnectionStatus(participantId);
    }

}
<commit_msg>ref(ParticipantConnectionStatus): simplify the condition<commit_after>/* global __filename, module, require */
var logger = require('jitsi-meet-logger').getLogger(__filename);
var MediaType = require('../../service/RTC/MediaType');
var RTCBrowserType = require('../RTC/RTCBrowserType');
var RTCEvents = require('../../service/RTC/RTCEvents');

import * as JitsiConferenceEvents from '../../JitsiConferenceEvents';
import * as JitsiTrackEvents from '../../JitsiTrackEvents';
import Statistics from '../statistics/statistics';

/**
 * Default value of 2000 milliseconds for
 * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
 *
 * @type {number}
 */
const DEFAULT_RTC_MUTE_TIMEOUT = 2000;

/**
 * Class is responsible for emitting
 * JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED events.
 */
export default class ParticipantConnectionStatus {
    /**
     * Creates new instance of <tt>ParticipantConnectionStatus</tt>.
     *
     * @constructor
     * @param {RTC} rtc the RTC service instance
     * @param {JitsiConference} conference parent conference instance
     * @param {number} rtcMuteTimeout (optional) custom value for
     * {@link ParticipantConnectionStatus.rtcMuteTimeout}.
     */
    constructor(rtc, conference, rtcMuteTimeout) {
        this.rtc = rtc;
        this.conference = conference;
        /**
         * A map of the "endpoint ID"(which corresponds to the resource part
         * of MUC JID(nickname)) to the timeout callback IDs scheduled using
         * window.setTimeout.
         * @type {Object.<string, number>}
         */
        this.trackTimers = {};
        /**
         * This map holds the endpoint connection status received from the JVB
         * (as it might be different than the one stored in JitsiParticipant).
         * Required for getting back in sync when remote video track is removed.
         * @type {Object.<string, boolean>}
         */
        this.connStatusFromJvb = { };
        /**
         * How long we're going to wait after the RTC video track muted event
         * for the corresponding signalling mute event, before the connection
         * interrupted is fired. The default value is
         * {@link DEFAULT_RTC_MUTE_TIMEOUT}.
         *
         * @type {number} amount of time in milliseconds
         */
        this.rtcMuteTimeout
            = typeof rtcMuteTimeout === 'number'
                ? rtcMuteTimeout : DEFAULT_RTC_MUTE_TIMEOUT;
        /**
         * This map holds a timestamp indicating  when participant's video track
         * was RTC muted (it is assumed that each participant can have only 1
         * video track at a time). The purpose of storing the timestamp is to
         * avoid the transition to disconnected status in case of legitimate
         * video mute operation where the signalling video muted event can
         * arrive shortly after RTC muted event.
         *
         * The key is participant's ID which is the same as endpoint id in
         * the Colibri conference allocated on the JVB.
         *
         * The value is a timestamp measured in milliseconds obtained with
         * <tt>Date.now()</tt>.
         *
         * FIXME merge this logic with NO_DATA_FROM_SOURCE event
         *       implemented in JitsiLocalTrack by extending the event to
         *       the remote track and allowing to set different timeout for
         *       local and remote tracks.
         *
         * @type {Object.<string, number>}
         */
        this.rtcMutedTimestamp = { };
        logger.info("RtcMuteTimeout set to: " + this.rtcMuteTimeout);
    }

    /**
     * Initializes <tt>ParticipantConnectionStatus</tt> and bind required event
     * listeners.
     */
    init() {

        this._onEndpointConnStatusChanged
            = this.onEndpointConnStatusChanged.bind(this);

        this.rtc.addListener(
            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
            this._onEndpointConnStatusChanged);

        // On some browsers MediaStreamTrack trigger "onmute"/"onunmute"
        // events for video type tracks when they stop receiving data which is
        // often a sign that remote user is having connectivity issues
        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {

            this._onTrackRtcMuted = this.onTrackRtcMuted.bind(this);
            this.rtc.addListener(
                RTCEvents.REMOTE_TRACK_MUTE, this._onTrackRtcMuted);

            this._onTrackRtcUnmuted = this.onTrackRtcUnmuted.bind(this);
            this.rtc.addListener(
                RTCEvents.REMOTE_TRACK_UNMUTE, this._onTrackRtcUnmuted);

            // Track added/removed listeners are used to bind "mute"/"unmute"
            // event handlers
            this._onRemoteTrackAdded = this.onRemoteTrackAdded.bind(this);
            this.conference.on(
                JitsiConferenceEvents.TRACK_ADDED,
                this._onRemoteTrackAdded);

            this._onRemoteTrackRemoved = this.onRemoteTrackRemoved.bind(this);
            this.conference.on(
                JitsiConferenceEvents.TRACK_REMOVED,
                this._onRemoteTrackRemoved);

            // Listened which will be bound to JitsiRemoteTrack to listen for
            // signalling mute/unmute events.
            this._onSignallingMuteChanged
                = this.onSignallingMuteChanged.bind(this);
        }
    }

    /**
     * Removes all event listeners and disposes of all resources held by this
     * instance.
     */
    dispose() {

        this.rtc.removeListener(
            RTCEvents.ENDPOINT_CONN_STATUS_CHANGED,
            this._onEndpointConnStatusChanged);

        if (RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
            this.rtc.removeListener(
                RTCEvents.REMOTE_TRACK_MUTE,
                this._onTrackRtcMuted);
            this.rtc.removeListener(
                RTCEvents.REMOTE_TRACK_UNMUTE,
                this._onTrackRtcUnmuted);

            this.conference.off(
                JitsiConferenceEvents.TRACK_ADDED,
                this._onRemoteTrackAdded);
            this.conference.off(
                JitsiConferenceEvents.TRACK_REMOVED,
                this._onRemoteTrackRemoved);
        }

        Object.keys(this.trackTimers).forEach(function (participantId) {
            this.clearTimeout(participantId);
            this.clearRtcMutedTimestamp(participantId);
        }.bind(this));

        // Clear RTC connection status cache
        this.connStatusFromJvb = {};
    }

    /**
     * Handles RTCEvents.ENDPOINT_CONN_STATUS_CHANGED triggered when we receive
     * notification over the data channel from the bridge about endpoint's
     * connection status update.
     * @param endpointId {string} the endpoint ID(MUC nickname/resource JID)
     * @param isActive {boolean} true if the connection is OK or false otherwise
     */
    onEndpointConnStatusChanged(endpointId, isActive) {

        logger.debug(
            'Detector RTCEvents.ENDPOINT_CONN_STATUS_CHANGED('
                + Date.now() +'): ' + endpointId + ': ' + isActive);

        // Filter out events for the local JID for now
        if (endpointId !== this.conference.myUserId()) {
            // Store the status received over the data channels
            this.connStatusFromJvb[endpointId] = isActive;
            this.figureOutConnectionStatus(endpointId);
        }
    }

    _changeConnectionStatus(participant, newStatus) {
        if (participant.isConnectionActive() !== newStatus) {

            const endpointId = participant.getId();

            participant._setIsConnectionActive(newStatus);

            logger.debug(
                'Emit endpoint conn status(' + Date.now() + ') '
                    + endpointId + ": " + newStatus);

            // Log the event on CallStats
            Statistics.sendLog(
                JSON.stringify({
                    id: 'peer.conn.status',
                    participant: endpointId,
                    status: newStatus
                }));

            // and analytics
            Statistics.analytics.sendEvent('peer.conn.status',
                {label: newStatus});

            this.conference.eventEmitter.emit(
                JitsiConferenceEvents.PARTICIPANT_CONN_STATUS_CHANGED,
                endpointId, newStatus);
        }
    }

    /**
     * Reset the postponed "connection interrupted" event which was previously
     * scheduled as a timeout on RTC 'onmute' event.
     *
     * @param participantId the participant for which the "connection
     * interrupted" timeout was scheduled
     */
    clearTimeout(participantId) {
        if (this.trackTimers[participantId]) {
            window.clearTimeout(this.trackTimers[participantId]);
            this.trackTimers[participantId] = null;
        }
    }

    /**
     * Clears the timestamp of the RTC muted event for participant's video track
     * @param participantId the id of the conference participant which is
     * the same as the Colibri endpoint ID of the video channel allocated for
     * the user on the videobridge.
     */
    clearRtcMutedTimestamp(participantId) {
        this.rtcMutedTimestamp[participantId] = null;
    }

    /**
     * Bind signalling mute event listeners for video {JitsiRemoteTrack} when
     * a new one is added to the conference.
     *
     * @param {JitsiTrack} remoteTrack the {JitsiTrack} which is being added to
     * the conference.
     */
    onRemoteTrackAdded(remoteTrack) {
        if (!remoteTrack.isLocal()
                && remoteTrack.getType() === MediaType.VIDEO) {

            logger.debug(
                'Detector on remote track added for: '
                    + remoteTrack.getParticipantId());

            remoteTrack.on(
                JitsiTrackEvents.TRACK_MUTE_CHANGED,
                this._onSignallingMuteChanged);
        }
    }

    /**
     * Removes all event listeners bound to the remote video track and clears
     * any related timeouts.
     *
     * @param {JitsiRemoteTrack} remoteTrack the remote track which is being
     * removed from the conference.
     */
    onRemoteTrackRemoved(remoteTrack) {
        if (!remoteTrack.isLocal()
                && remoteTrack.getType() === MediaType.VIDEO) {

            const endpointId = remoteTrack.getParticipantId();

            logger.debug(
                'Detector on remote track removed: ' + endpointId);

            remoteTrack.off(
                JitsiTrackEvents.TRACK_MUTE_CHANGED,
                this._onSignallingMuteChanged);

            this.clearTimeout(endpointId);
            this.clearRtcMutedTimestamp(endpointId);

            this.figureOutConnectionStatus(endpointId);
        }
    }

    /**
     * Checks if given participant's video is considered frozen.
     * @param {JitsiParticipant} participant
     * @return {boolean} <tt>true</tt> if the video has frozen for given
     * participant or <tt>false</tt> when it's either not considered frozen
     * (yet) or if freeze detection is not supported by the current browser.
     *
     * FIXME merge this logic with NO_DATA_FROM_SOURCE event
     *       implemented in JitsiLocalTrack by extending the event to
     *       the remote track and allowing to set different timeout for
     *       local and remote tracks.
     *
     */
    isVideoTrackFrozen (participant) {
        if (!RTCBrowserType.isVideoMuteOnConnInterruptedSupported()) {
            return false;
        }

        const hasAnyVideoRTCMuted = participant.hasAnyVideoTrackWebRTCMuted();
        const rtcMutedTimestamp
            = this.rtcMutedTimestamp[participant.getId()];

        return hasAnyVideoRTCMuted
            && typeof rtcMutedTimestamp === 'number'
            && (Date.now() - rtcMutedTimestamp) >= this.rtcMuteTimeout;
    }

    /**
     * Figures out (and updates) the current connectivity status for
     * the participant identified by the given id.
     *
     * @param {string} id the participant's id (MUC nickname or Colibri endpoint
     * ID).
     */
    figureOutConnectionStatus(id) {
        const participant = this.conference.getParticipantById(id);
        if (!participant) {
            // Probably the participant is no longer in the conference
            // (at the time of writing this code, participant is
            // detached from the conference and TRACK_REMOVED events are
            // fired),
            // so we don't care, but let's print the warning for
            // debugging purpose
            logger.warn('figure out conn status - no participant for: ' + id);
            return;
        }

        const isVideoMuted = participant.isVideoMuted();
        const isVideoTrackFrozen = this.isVideoTrackFrozen(participant);
        let isConnActiveByJvb = this.connStatusFromJvb[id];

        // If no status was received from the JVB it means that it's active
        // (the bridge does not send notification unless there is a problem).
        if (typeof isConnActiveByJvb !== 'boolean') {
            logger.debug('Assuming connection active by JVB - no notification');
            isConnActiveByJvb = true;
        }

        let isConnectionActive
            = isConnActiveByJvb && (isVideoMuted || !isVideoTrackFrozen);

        logger.debug(
            'Figure out conn status, is video muted: ' + isVideoMuted
                + ' is active(jvb): ' + isConnActiveByJvb
                + ' video track frozen: ' + isVideoTrackFrozen
                + ' => ' + isConnectionActive);

        this._changeConnectionStatus(participant, isConnectionActive);
    }

    /**
     * Handles RTC 'onmute' event for the video track.
     *
     * @param {JitsiRemoteTrack} track the video track for which 'onmute' event
     * will be processed.
     */
    onTrackRtcMuted(track) {
        const participantId = track.getParticipantId();
        const participant = this.conference.getParticipantById(participantId);
        logger.debug('Detector track RTC muted: ' + participantId);
        if (!participant) {
            logger.error('No participant for id: ' + participantId);
            return;
        }
        this.rtcMutedTimestamp[participantId] = Date.now();
        if (!participant.isVideoMuted()) {
            // If the user is not muted according to the signalling we'll give
            // it some time, before the connection interrupted event is
            // triggered.
            this.clearTimeout(participantId);
            this.trackTimers[participantId] = window.setTimeout(function () {
                logger.debug('RTC mute timeout for: ' + participantId);
                this.clearTimeout(participantId);
                this.figureOutConnectionStatus(participantId);
            }.bind(this), this.rtcMuteTimeout);
        }
    }

    /**
     * Handles RTC 'onunmute' event for the video track.
     *
     * @param {JitsiRemoteTrack} track the video track for which 'onunmute'
     * event will be processed.
     */
    onTrackRtcUnmuted(track) {
        const participantId = track.getParticipantId();

        logger.debug('Detector track RTC unmuted: ' + participantId);

        this.clearTimeout(participantId);
        this.clearRtcMutedTimestamp(participantId);

        this.figureOutConnectionStatus(participantId);
    }

    /**
     * Here the signalling "mute"/"unmute" events are processed.
     *
     * @param {JitsiRemoteTrack} track the remote video track for which
     * the signalling mute/unmute event will be processed.
     */
    onSignallingMuteChanged (track) {
        const participantId = track.getParticipantId();

        logger.debug(
            'Detector on track signalling mute changed: '
                + participantId, track.isMuted());

        this.figureOutConnectionStatus(participantId);
    }

}
<|endoftext|>


<commit_before>// for product and user per location selection
$(function () {
    $("#id_users-selected_ids").width(800).height(400).multiselect();
    $("#id_products-selected_ids").width(800).height(400).multiselect();
});

var insert_new_user = function(user) {
    $('#id_users-selected_ids').multiselect('addSelected', user.user_id, user.text);
};

$('#new_user').on('show', function() {

    var url = NEW_MOBILE_WORKER_URL + "?location_id=" + LOCATION_ID;
    var form_node = $("#new_user form");

    $.get(url, function(data) {
        form_node.html(data['form_html']);
    });

    var handle_submission = function(event) {
        event.preventDefault();
        $.post(url, form_node.serialize(), function(data) {
            if (data['status'] === 'success') {
                insert_new_user(data['user']);
                $('#new_user').modal('hide');
            } else {
                form_node.html(data['form_html']);
            }
        });
    };

    form_node.submit(handle_submission);
});
<commit_msg>Dimagimon<commit_after>// for product and user per location selection
$(function () {
    $("#id_users-selected_ids").width(800).height(400).multiselect();
    $("#id_products-selected_ids").width(800).height(400).multiselect();
});

var insert_new_user = function(user) {
    $('#id_users-selected_ids').multiselect('addSelected', user.user_id, user.text);
};

$('#new_user').on('show', function() {

    var url = NEW_MOBILE_WORKER_URL + "?location_id=" + LOCATION_ID;
    var form_node = $("#new_user form");

    $.get(url, function(data) {
        form_node.html(data.form_html);
    });

    var handle_submission = function(event) {
        event.preventDefault();
        $.post(url, form_node.serialize(), function(data) {
            if (data.status === 'success') {
                insert_new_user(data.user);
                $('#new_user').modal('hide');
            } else {
                form_node.html(data.form_html);
            }
        });
    };

    form_node.submit(handle_submission);
});
<|endoftext|>


<commit_before>hqDefine("reports/js/case_details", function() {
    var EditPropertiesModel = function(options) {
        var self = this;

        self.propertyNames = ko.observableArray();  // ordered list of names, populated by ajax call because it's slow
        self.properties;                            // map of name => value

        // If there are a lot of items, make a bigger modal and render properties as columns
        // Supports a small one-column modal, a larger two-column modal, or a full-screen three-column modal
        self.itemsPerPage = new ko.observable(12);
        self.columnsPerPage = ko.computed(function() {
            return Math.min(3, Math.ceil(self.propertyNames().length / self.itemsPerPage()));
        });
        self.columnsPerPage.subscribe(function(newValue) {
            self.itemsPerPage(self.itemsPerPage() * newValue);
        });
        self.columnClass = ko.computed(function() {
            return "col-sm-" + (12 / self.columnsPerPage());
        });
        self.modalClass = ko.computed(function() {
            return self.columnsPerPage() === 3 ? "full-screen-modal" : "";
        });
        self.modalDialogClass = ko.computed(function() {
            return self.columnsPerPage() === 2 ? "modal-lg" : "";
        });

        // This modal supports pagination and a search box, all of which is done client-side
        self.currentPage = new ko.observable();
        self.totalPages = new ko.observable();  // observable because it will change if there's a search query
        self.query = new ko.observable();

        self.showSpinner = new ko.observable(true);
        self.showPagination = ko.computed(function() {
            return !self.showSpinner() && self.propertyNames().length > self.itemsPerPage();
        });
        self.showError = new ko.observable(false);
        self.showRetry = new ko.observable(false);
        self.disallowSave = ko.computed(function() {
            return self.showSpinner() || self.showError();
        });

        self.incrementPage = function(increment) {
            var newCurrentPage = self.currentPage() + increment;
            if (newCurrentPage <= 0 || newCurrentPage > self.totalPages()) {
                return;
            }
            self.currentPage(newCurrentPage);
        };

        self.visibleItems = ko.observableArray([]);     // All items visible on the current page
        self.visibleColumns = ko.observableArray([]);   // visibleItems broken down into columns for rendering; an array of arrays

        self.showNoData = ko.computed(function() {
            return !self.showError() && self.visibleItems().length === 0;
        });

        // Handle pagination and filtering, filling visibleItems with whatever should be on the current page
        // Forces a re-render because it clears and re-fills visibleColumns
        self.render = function() {
            var added = 0,
                index = 0;

            // Remove all items
            self.visibleItems.splice(0);

            // Cycle over all items on previous pages
            while (added < self.itemsPerPage() * (self.currentPage() - 1) && index < self.propertyNames().length) {
                if (self.matchesQuery(self.propertyNames()[index])) {
                    added++;
                }
                index++;
            }

            // Add as many items as fit on a page
            added = 0;
            while (added < self.itemsPerPage() && index < self.propertyNames().length) {
                if (self.matchesQuery(self.propertyNames()[index])) {
                    self.visibleItems.push({
                        name: self.propertyNames()[index],
                        value: self.properties[self.propertyNames()[index]],
                    });
                    added++;
                }
                index++;
            }

            // Break visibleItems into separate columns for rendering
            self.visibleColumns.splice(0);
            var itemsPerColumn = self.itemsPerPage() / self.columnsPerPage();
            for (var i = 0; i < self.itemsPerPage(); i += itemsPerColumn) {
                self.visibleColumns.push(self.visibleItems.slice(i, i + itemsPerColumn));
            }
        };

        self.initQuery = function() {
            self.query("");
        };

        self.query.subscribe(function() {
            self.currentPage(1);
            self.totalPages(Math.ceil(_.filter(self.propertyNames(), self.matchesQuery).length / self.itemsPerPage()) || 1);
            self.render();
        });

        // Track an array of page numbers, e.g., [1, 2, 3], to render the pagination widget.
        // Having it as an array makes knockout rendering simpler.
        self.visiblePages = ko.observableArray([]);
        self.totalPages.subscribe(function(newValue) {
            self.visiblePages(_.map(_.range(newValue), function(p) { return p + 1; }));
        });

        self.matchesQuery = function(propertyName) {
            return !self.query() || propertyName.indexOf(self.query()) !== -1;
        };

        self.currentPage.subscribe(self.render);

        self.propertyChange = function(model, e) {
            var $input = $(e.currentTarget);
            self.properties[$input.data('name')] = $input.val();
        };

        self.submitForm = function(model, e) {
            var $button = $(e.currentTarget);
            $button.disableButton();
            $.post({
                url: hqImport("hqwebapp/js/initial_page_data").reverse("edit_case"),
                data: self.properties,
                success: function() {
                    window.location.reload();
                },
                error: function() {
                    $button.enableButton();
                    self.showRetry(true);
                },
            });
            return true;
        };

        self.init = function() {
            self.properties = _.extend({}, options.properties);
            self.initQuery();
            self.currentPage(1);
            self.render();
        };

        $.get({
            url: hqImport("hqwebapp/js/initial_page_data").reverse('case_property_names'),
            success: function(names) {
                _.each(names, function(name) {
                    self.propertyNames.push(name);
                });
                self.showSpinner(false);
                self.init();
            },
            error: function() {
                self.showSpinner(false);
                self.showError(true);
            },
        });

        return self;
    };

    var XFormDataModel = function(data) {
        var self = this;
        self.id = ko.observable(data.id);

        self.format_user = function(username) {
            if (username === undefined || username === null) {
                return gettext("Unknown");
            }
            return username.split('@')[0];
        };

        self.pad_zero = function(val) {
            if (val < 10) {
                return "0" + val;
            }
            return val;
        };

        self.format_date = function(isodatestring) {
            if (!isodatestring) {
                return gettext('present');
            }
            //parse and format the date timestamps - seconds since epoch into date object
            var date = new Date(isodatestring.split('+')[0]);

            // Get the TZ offset based the project's timezone and create a new date
            // object with that as it's "UTC" date
            var _configuredTZOffset = hqImport("hqwebapp/js/initial_page_data").get('timezone_offset');
            date = new Date(date.getTime() + _configuredTZOffset);

            // hours part from the timestamp
            var hours = self.pad_zero(date.getUTCHours());
            // minutes part from the timestamp
            var minutes = self.pad_zero(date.getUTCMinutes());

            var year = date.getUTCFullYear();
            var month = date.getUTCMonth() + 1;
            var day = date.getUTCDate();

            return  year + '-' + month + '-' + day + ' ' + hours + ":" + minutes;
        };

        self.received_on = ko.observable(self.format_date(data.received_on));
        self.userID = ko.observable(data.user.id);
        self.username = ko.observable(self.format_user(data.user.username));
        self.readable_name = ko.observable(data.readable_name);
    };

    var XFormListViewModel = function() {
        var self = this;

        self.pagination_options = [10,25,50,100];

        self.xforms = ko.observableArray([]);
        self.page_size = ko.observable(10);
        self.disp_page_index = ko.observable(1);
        self.total_rows = ko.observable(-1);
        self.selected_xform_idx = ko.observable(-1);
        self.selected_xform_doc_id = ko.observable("");
        self.selected_xforms = ko.observableArray([]);

        self.form_type_facets = ko.observableArray([]);
        self.form_recv_facets = ko.observableArray([]);

        self.data_loading = ko.observable(false);

        self.getParameterByName = function(name, url) {
            if (!url) url = window.location.href;
            name = name.replace(/[\[\]]/g, "\\$&");
            var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
                results = regex.exec(url);
            if (!results) return null;
            if (!results[2]) return '';
            return decodeURIComponent(results[2].replace(/\+/g, " "));
        };

        var api_url = hqImport("hqwebapp/js/initial_page_data").get('xform_api_url');
        var init = function() {
            var hash = window.location.hash.split('?');
            if (hash[0] !== '#!history') {
                return;
            }

            var formId = self.getParameterByName('form_id', window.location.hash);
            if (formId) {
                self.get_xform_data(formId);
                self.selected_xform_doc_id(formId);
            }
        };

        self.get_xform_data = function(xform_id) {
            $.cachedAjax({
                "type": "GET",
                "url": hqImport("hqwebapp/js/initial_page_data").reverse('case_form_data', xform_id),
                "success": function(data) {
                    $("#xform_data_panel").html(data);
                },
            });
        };

        init();

        self.xform_history_cb = function(data) {
            self.total_rows(hqImport("hqwebapp/js/initial_page_data").get('xform_ids').length);
            var mapped_xforms = $.map(data, function (item) {
                return new XFormDataModel(item);
            });
            self.xforms(mapped_xforms);
            var xformId = self.selected_xform_doc_id();
            if (xformId) {
                self.selected_xform_idx(self.xforms.indexOf());
            } else {
                self.selected_xform_idx(-1);
            }
        };

        self.all_rows_loaded = ko.computed(function() {
            return self.total_rows() === self.xforms().length;
        });

        self.page_count = ko.computed(function() {
            return Math.ceil(self.total_rows()/self.page_size());
        });

        self.refresh_forms = ko.computed(function () {
            var disp_index = self.disp_page_index();
            if (disp_index > self.page_count.peek()) {
                self.disp_page_index(self.page_count.peek());
                return;
            }
            if (self.total_rows.peek() > 0 && self.all_rows_loaded.peek()) {
                return;
            }
            self.data_loading(true);
            var start_num = disp_index || 1;
            var start_range = (start_num - 1) * self.page_size();
            var end_range = start_range + self.page_size();
            $.ajax({
                "type": "GET",
                "url":  api_url,
                "data": {
                    'start_range': start_range,
                    'end_range': end_range,
                },
                "success": function(data) {
                    self.xform_history_cb(data);
                },
                "complete": function() {
                    self.data_loading(false);
                },
            });
        }, this).extend({deferred: true});

        self.nextPage = function() {
            self.disp_page_index(self.disp_page_index() + 1);
        };

        self.prevPage = function() {
            self.disp_page_index(self.disp_page_index() - 1);
        };

        self.clickRow = function(item) {
            $("#xform_data_panel").html("<img src='/static/hqwebapp/images/ajax-loader.gif' alt='loading indicator' />");
            var idx = self.xforms().indexOf(item);

            self.get_xform_data(self.xforms()[idx].id());
            self.selected_xform_idx(idx);
            self.selected_xform_doc_id(self.xforms()[idx].id());
            if (idx > -1) {
                self.selected_xforms([]);
                self.selected_xforms.push(self.xforms()[self.selected_xform_idx()]);
            }
            window.history.pushState({}, '', '#!history?form_id=' + self.selected_xform_doc_id());
        };

        self.page_start_num = ko.computed(function() {
            var start_num = self.disp_page_index() || 1;
            var calc_start_num = ((start_num - 1) * self.page_size()) + 1;
            return calc_start_num;
        });

        self.page_end_num = ko.computed(function() {
            var start_num = self.disp_page_index() || 1;
            var end_page_num = ((start_num - 1) * self.page_size()) + self.page_size();
            if (end_page_num > self.total_rows()) {
                return self.total_rows();
            }
            else {
                return end_page_num;
            }
        });

        self.all_pages = function() {
            return _.range(1, self.page_count()+1);
        };

        self.xform_view = ko.computed(function () {
            return self.selected_xform_doc_id() !== undefined;
        });

        self.row_highlight = ko.computed(function() {
            //hitting next page will not disappear the xform display just remove the highlight
            if (self.selected_xform_idx() === -1) {
                return false;
            } else  {
                if (self.selected_xforms[0] !== undefined) {
                    return self.selected_xform_doc_id() === self.selected_xforms()[0].id();
                } else {
                    return true;
                }
            }
        });
    };

    $(function() {
        $('#close_case').submit(function() {
            hqImport('analytix/js/google').track.event('Edit Data', 'Close Case', '-', "", {}, function () {
                document.getElementById('close_case').submit();
            });
            return false;
        });

        var initial_page_data = hqImport("hqwebapp/js/initial_page_data").get,
            $editPropertiesModal = $("#edit-dynamic-properties");
        if ($editPropertiesModal.length) {
            $("#edit-dynamic-properties-trigger").click(function() {
                $editPropertiesModal.modal();
            });
            $editPropertiesModal.koApplyBindings(new EditPropertiesModel({
                properties: initial_page_data('dynamic_properties'),
            }));
        }

        $("#history").koApplyBindings(new XFormListViewModel());

        $("#properties").koApplyBindings();
    });
});
<commit_msg>Make search case-insensitive<commit_after>hqDefine("reports/js/case_details", function() {
    var EditPropertiesModel = function(options) {
        var self = this;

        self.propertyNames = ko.observableArray();  // ordered list of names, populated by ajax call because it's slow
        self.properties;                            // map of name => value

        // If there are a lot of items, make a bigger modal and render properties as columns
        // Supports a small one-column modal, a larger two-column modal, or a full-screen three-column modal
        self.itemsPerPage = new ko.observable(12);
        self.columnsPerPage = ko.computed(function() {
            return Math.min(3, Math.ceil(self.propertyNames().length / self.itemsPerPage()));
        });
        self.columnsPerPage.subscribe(function(newValue) {
            self.itemsPerPage(self.itemsPerPage() * newValue);
        });
        self.columnClass = ko.computed(function() {
            return "col-sm-" + (12 / self.columnsPerPage());
        });
        self.modalClass = ko.computed(function() {
            return self.columnsPerPage() === 3 ? "full-screen-modal" : "";
        });
        self.modalDialogClass = ko.computed(function() {
            return self.columnsPerPage() === 2 ? "modal-lg" : "";
        });

        // This modal supports pagination and a search box, all of which is done client-side
        self.currentPage = new ko.observable();
        self.totalPages = new ko.observable();  // observable because it will change if there's a search query
        self.query = new ko.observable();

        self.showSpinner = new ko.observable(true);
        self.showPagination = ko.computed(function() {
            return !self.showSpinner() && self.propertyNames().length > self.itemsPerPage();
        });
        self.showError = new ko.observable(false);
        self.showRetry = new ko.observable(false);
        self.disallowSave = ko.computed(function() {
            return self.showSpinner() || self.showError();
        });

        self.incrementPage = function(increment) {
            var newCurrentPage = self.currentPage() + increment;
            if (newCurrentPage <= 0 || newCurrentPage > self.totalPages()) {
                return;
            }
            self.currentPage(newCurrentPage);
        };

        self.visibleItems = ko.observableArray([]);     // All items visible on the current page
        self.visibleColumns = ko.observableArray([]);   // visibleItems broken down into columns for rendering; an array of arrays

        self.showNoData = ko.computed(function() {
            return !self.showError() && self.visibleItems().length === 0;
        });

        // Handle pagination and filtering, filling visibleItems with whatever should be on the current page
        // Forces a re-render because it clears and re-fills visibleColumns
        self.render = function() {
            var added = 0,
                index = 0;

            // Remove all items
            self.visibleItems.splice(0);

            // Cycle over all items on previous pages
            while (added < self.itemsPerPage() * (self.currentPage() - 1) && index < self.propertyNames().length) {
                if (self.matchesQuery(self.propertyNames()[index])) {
                    added++;
                }
                index++;
            }

            // Add as many items as fit on a page
            added = 0;
            while (added < self.itemsPerPage() && index < self.propertyNames().length) {
                if (self.matchesQuery(self.propertyNames()[index])) {
                    self.visibleItems.push({
                        name: self.propertyNames()[index],
                        value: self.properties[self.propertyNames()[index]],
                    });
                    added++;
                }
                index++;
            }

            // Break visibleItems into separate columns for rendering
            self.visibleColumns.splice(0);
            var itemsPerColumn = self.itemsPerPage() / self.columnsPerPage();
            for (var i = 0; i < self.itemsPerPage(); i += itemsPerColumn) {
                self.visibleColumns.push(self.visibleItems.slice(i, i + itemsPerColumn));
            }
        };

        self.initQuery = function() {
            self.query("");
        };

        self.query.subscribe(function() {
            self.currentPage(1);
            self.totalPages(Math.ceil(_.filter(self.propertyNames(), self.matchesQuery).length / self.itemsPerPage()) || 1);
            self.render();
        });

        // Track an array of page numbers, e.g., [1, 2, 3], to render the pagination widget.
        // Having it as an array makes knockout rendering simpler.
        self.visiblePages = ko.observableArray([]);
        self.totalPages.subscribe(function(newValue) {
            self.visiblePages(_.map(_.range(newValue), function(p) { return p + 1; }));
        });

        self.matchesQuery = function(propertyName) {
            return !self.query() || propertyName.toLowerCase().indexOf(self.query().toLowerCase()) !== -1;
        };

        self.currentPage.subscribe(self.render);

        self.propertyChange = function(model, e) {
            var $input = $(e.currentTarget);
            self.properties[$input.data('name')] = $input.val();
        };

        self.submitForm = function(model, e) {
            var $button = $(e.currentTarget);
            $button.disableButton();
            $.post({
                url: hqImport("hqwebapp/js/initial_page_data").reverse("edit_case"),
                data: self.properties,
                success: function() {
                    window.location.reload();
                },
                error: function() {
                    $button.enableButton();
                    self.showRetry(true);
                },
            });
            return true;
        };

        self.init = function() {
            self.properties = _.extend({}, options.properties);
            self.initQuery();
            self.currentPage(1);
            self.render();
        };

        $.get({
            url: hqImport("hqwebapp/js/initial_page_data").reverse('case_property_names'),
            success: function(names) {
                _.each(names, function(name) {
                    self.propertyNames.push(name);
                });
                self.showSpinner(false);
                self.init();
            },
            error: function() {
                self.showSpinner(false);
                self.showError(true);
            },
        });

        return self;
    };

    var XFormDataModel = function(data) {
        var self = this;
        self.id = ko.observable(data.id);

        self.format_user = function(username) {
            if (username === undefined || username === null) {
                return gettext("Unknown");
            }
            return username.split('@')[0];
        };

        self.pad_zero = function(val) {
            if (val < 10) {
                return "0" + val;
            }
            return val;
        };

        self.format_date = function(isodatestring) {
            if (!isodatestring) {
                return gettext('present');
            }
            //parse and format the date timestamps - seconds since epoch into date object
            var date = new Date(isodatestring.split('+')[0]);

            // Get the TZ offset based the project's timezone and create a new date
            // object with that as it's "UTC" date
            var _configuredTZOffset = hqImport("hqwebapp/js/initial_page_data").get('timezone_offset');
            date = new Date(date.getTime() + _configuredTZOffset);

            // hours part from the timestamp
            var hours = self.pad_zero(date.getUTCHours());
            // minutes part from the timestamp
            var minutes = self.pad_zero(date.getUTCMinutes());

            var year = date.getUTCFullYear();
            var month = date.getUTCMonth() + 1;
            var day = date.getUTCDate();

            return  year + '-' + month + '-' + day + ' ' + hours + ":" + minutes;
        };

        self.received_on = ko.observable(self.format_date(data.received_on));
        self.userID = ko.observable(data.user.id);
        self.username = ko.observable(self.format_user(data.user.username));
        self.readable_name = ko.observable(data.readable_name);
    };

    var XFormListViewModel = function() {
        var self = this;

        self.pagination_options = [10,25,50,100];

        self.xforms = ko.observableArray([]);
        self.page_size = ko.observable(10);
        self.disp_page_index = ko.observable(1);
        self.total_rows = ko.observable(-1);
        self.selected_xform_idx = ko.observable(-1);
        self.selected_xform_doc_id = ko.observable("");
        self.selected_xforms = ko.observableArray([]);

        self.form_type_facets = ko.observableArray([]);
        self.form_recv_facets = ko.observableArray([]);

        self.data_loading = ko.observable(false);

        self.getParameterByName = function(name, url) {
            if (!url) url = window.location.href;
            name = name.replace(/[\[\]]/g, "\\$&");
            var regex = new RegExp("[?&]" + name + "(=([^&#]*)|&|#|$)"),
                results = regex.exec(url);
            if (!results) return null;
            if (!results[2]) return '';
            return decodeURIComponent(results[2].replace(/\+/g, " "));
        };

        var api_url = hqImport("hqwebapp/js/initial_page_data").get('xform_api_url');
        var init = function() {
            var hash = window.location.hash.split('?');
            if (hash[0] !== '#!history') {
                return;
            }

            var formId = self.getParameterByName('form_id', window.location.hash);
            if (formId) {
                self.get_xform_data(formId);
                self.selected_xform_doc_id(formId);
            }
        };

        self.get_xform_data = function(xform_id) {
            $.cachedAjax({
                "type": "GET",
                "url": hqImport("hqwebapp/js/initial_page_data").reverse('case_form_data', xform_id),
                "success": function(data) {
                    $("#xform_data_panel").html(data);
                },
            });
        };

        init();

        self.xform_history_cb = function(data) {
            self.total_rows(hqImport("hqwebapp/js/initial_page_data").get('xform_ids').length);
            var mapped_xforms = $.map(data, function (item) {
                return new XFormDataModel(item);
            });
            self.xforms(mapped_xforms);
            var xformId = self.selected_xform_doc_id();
            if (xformId) {
                self.selected_xform_idx(self.xforms.indexOf());
            } else {
                self.selected_xform_idx(-1);
            }
        };

        self.all_rows_loaded = ko.computed(function() {
            return self.total_rows() === self.xforms().length;
        });

        self.page_count = ko.computed(function() {
            return Math.ceil(self.total_rows()/self.page_size());
        });

        self.refresh_forms = ko.computed(function () {
            var disp_index = self.disp_page_index();
            if (disp_index > self.page_count.peek()) {
                self.disp_page_index(self.page_count.peek());
                return;
            }
            if (self.total_rows.peek() > 0 && self.all_rows_loaded.peek()) {
                return;
            }
            self.data_loading(true);
            var start_num = disp_index || 1;
            var start_range = (start_num - 1) * self.page_size();
            var end_range = start_range + self.page_size();
            $.ajax({
                "type": "GET",
                "url":  api_url,
                "data": {
                    'start_range': start_range,
                    'end_range': end_range,
                },
                "success": function(data) {
                    self.xform_history_cb(data);
                },
                "complete": function() {
                    self.data_loading(false);
                },
            });
        }, this).extend({deferred: true});

        self.nextPage = function() {
            self.disp_page_index(self.disp_page_index() + 1);
        };

        self.prevPage = function() {
            self.disp_page_index(self.disp_page_index() - 1);
        };

        self.clickRow = function(item) {
            $("#xform_data_panel").html("<img src='/static/hqwebapp/images/ajax-loader.gif' alt='loading indicator' />");
            var idx = self.xforms().indexOf(item);

            self.get_xform_data(self.xforms()[idx].id());
            self.selected_xform_idx(idx);
            self.selected_xform_doc_id(self.xforms()[idx].id());
            if (idx > -1) {
                self.selected_xforms([]);
                self.selected_xforms.push(self.xforms()[self.selected_xform_idx()]);
            }
            window.history.pushState({}, '', '#!history?form_id=' + self.selected_xform_doc_id());
        };

        self.page_start_num = ko.computed(function() {
            var start_num = self.disp_page_index() || 1;
            var calc_start_num = ((start_num - 1) * self.page_size()) + 1;
            return calc_start_num;
        });

        self.page_end_num = ko.computed(function() {
            var start_num = self.disp_page_index() || 1;
            var end_page_num = ((start_num - 1) * self.page_size()) + self.page_size();
            if (end_page_num > self.total_rows()) {
                return self.total_rows();
            }
            else {
                return end_page_num;
            }
        });

        self.all_pages = function() {
            return _.range(1, self.page_count()+1);
        };

        self.xform_view = ko.computed(function () {
            return self.selected_xform_doc_id() !== undefined;
        });

        self.row_highlight = ko.computed(function() {
            //hitting next page will not disappear the xform display just remove the highlight
            if (self.selected_xform_idx() === -1) {
                return false;
            } else  {
                if (self.selected_xforms[0] !== undefined) {
                    return self.selected_xform_doc_id() === self.selected_xforms()[0].id();
                } else {
                    return true;
                }
            }
        });
    };

    $(function() {
        $('#close_case').submit(function() {
            hqImport('analytix/js/google').track.event('Edit Data', 'Close Case', '-', "", {}, function () {
                document.getElementById('close_case').submit();
            });
            return false;
        });

        var initial_page_data = hqImport("hqwebapp/js/initial_page_data").get,
            $editPropertiesModal = $("#edit-dynamic-properties");
        if ($editPropertiesModal.length) {
            $("#edit-dynamic-properties-trigger").click(function() {
                $editPropertiesModal.modal();
            });
            $editPropertiesModal.koApplyBindings(new EditPropertiesModel({
                properties: initial_page_data('dynamic_properties'),
            }));
        }

        $("#history").koApplyBindings(new XFormListViewModel());

        $("#properties").koApplyBindings();
    });
});
<|endoftext|>


<commit_before>// @flow

import sumBy from 'lodash/sumBy'

// Sums up the number of credits offered by a set of courses
// TODO(rives): fix this to not be "Object"
export function countCredits(courses: Array<Object> = []) {
	return sumBy(courses, c => (c ? c.credits : 0)) || 0
}
<commit_msg>update countCredits to only accept CourseType again<commit_after>// @flow

import sumBy from 'lodash/sumBy'
import {type Course as CourseType} from '@gob/types'

// Sums up the number of credits offered by a set of courses
export function countCredits(courses: Array<CourseType> = []) {
	return sumBy(courses, c => (c ? c.credits : 0)) || 0
}
<|endoftext|>