<commit_before>/*jslint indent: 2, unparam: true*/
/*global $: false, document: false, togglbutton: false*/

'use strict';

// Basecamp Next
togglbutton.render('section.todos li.todo:not(.toggl)', {observe: true}, function (elem) {
  var link, behavior = 'hover_content',
    container = $('.wrapper', elem), spanTag;

  if (container === null) {
    return;
  }

  link = togglbutton.createTimerLink({
    className: 'basecamp',
    description: $('.content_for_perma', elem).textContent,
    projectName: (!!$(".project")) ? ($(".project > title") || $(".project > header > h1 > a")).innerHTML : ""
  });

  link.setAttribute('data-behavior', behavior);
  link.addEventListener('click', function (e) {
    if (link.getAttribute('data-behavior') === '') {
      link.setAttribute('data-behavior', behavior);
    } else {
      link.setAttribute('data-behavior', '');
    }
  });

  spanTag = document.createElement("span");
  container.appendChild(spanTag.appendChild(link));
});

// Basecamp Classic
togglbutton.render('.items_wrapper .item > .content:not(.toggl)', {observe: true}, function (elem) {
  var link, behavior = 'selectable_target', spanTag;

  link = togglbutton.createTimerLink({
    className: 'basecamphq',
    description: elem.querySelector('span.content > span').textContent.trim(),
    projectName: (!!$(".project")) ? ($(".project > title") || $(".project > header > h1 > a")).innerHTML : ""
  });

  link.setAttribute('data-behavior', '');
  link.addEventListener('click', function (e) {
    if (link.getAttribute('data-behavior') === '') {
      link.setAttribute('data-behavior', behavior);
    } else {
      link.setAttribute('data-behavior', '');
    }
  });

  spanTag = document.createElement("span");
  $(".content", elem).appendChild(spanTag.appendChild(link));
});

// Basecamp 3
togglbutton.render('.todos li.todo:not(.toggl)', {observe: true}, function (elem) {
  var link, project,
    description = $('.checkbox__content', elem);

  if (!description) {
    return;
  }

  description = description.childNodes[1].textContent;
  project = $('.checkbox__content', elem).parentNode.parentNode.parentNode.parentNode.parentNode;
  project = $('.todolist__permalink', project).textContent;

  link = togglbutton.createTimerLink({
    className: 'basecamp3',
    description: description,
    projectName: project
  });

  $('.todo_assignee', elem).appendChild(link);
});<commit_msg>Fixed basecamp3 issues with completed todo items<commit_after>/*jslint indent: 2, unparam: true*/
/*global $: false, document: false, togglbutton: false*/

'use strict';

// Basecamp Next
togglbutton.render('section.todos li.todo:not(.toggl)', {observe: true}, function (elem) {
  var link, behavior = 'hover_content',
    container = $('.wrapper', elem), spanTag;

  if (container === null) {
    return;
  }

  link = togglbutton.createTimerLink({
    className: 'basecamp',
    description: $('.content_for_perma', elem).textContent,
    projectName: (!!$(".project")) ? ($(".project > title") || $(".project > header > h1 > a")).innerHTML : ""
  });

  link.setAttribute('data-behavior', behavior);
  link.addEventListener('click', function (e) {
    if (link.getAttribute('data-behavior') === '') {
      link.setAttribute('data-behavior', behavior);
    } else {
      link.setAttribute('data-behavior', '');
    }
  });

  spanTag = document.createElement("span");
  container.appendChild(spanTag.appendChild(link));
});

// Basecamp Classic
togglbutton.render('.items_wrapper .item > .content:not(.toggl)', {observe: true}, function (elem) {
  var link, behavior = 'selectable_target', spanTag;

  link = togglbutton.createTimerLink({
    className: 'basecamphq',
    description: elem.querySelector('span.content > span').textContent.trim(),
    projectName: (!!$(".project")) ? ($(".project > title") || $(".project > header > h1 > a")).innerHTML : ""
  });

  link.setAttribute('data-behavior', '');
  link.addEventListener('click', function (e) {
    if (link.getAttribute('data-behavior') === '') {
      link.setAttribute('data-behavior', behavior);
    } else {
      link.setAttribute('data-behavior', '');
    }
  });

  spanTag = document.createElement("span");
  $(".content", elem).appendChild(spanTag.appendChild(link));
});

// Basecamp 3
togglbutton.render('.todos li.todo:not(.toggl):not(.completed)', {observe: true}, function (elem) {
  var link, project,
    description = $('.checkbox__content', elem);

  if (!description) {
    return;
  }

  description = description.childNodes[1].textContent;
  project = $('.checkbox__content', elem).parentNode.parentNode.parentNode.parentNode.parentNode;
  project = $('.todolist__permalink', project).textContent;

  link = togglbutton.createTimerLink({
    className: 'basecamp3',
    description: description,
    projectName: project
  });

  $('.todo_assignee', elem).appendChild(link);
});<|endoftext|>


<commit_before>/**
 * @author centsent
 */
import angular from 'angular';
import servicesModule from './_index';
import FullscreenApi from '../utils/FullscreenApi';
import {
  mediaState
}
from './../AppSettings';

const _mediaElement = new WeakMap();
const _hydeoElement = new WeakMap();

/**
 * Provide APIs and event bindings for audio/video.
 */
class HyMediaService {

  /**
   * Store the hydeo element.
   */
  setHydeoElement(element) {
    _hydeoElement.set(this, element);
  }

  /**
   * Store the audio/video element.
   */
  setMediaElement(element) {
    _mediaElement.set(this, element);
  }

  /**
   * Fires when the audio/video was started or no longer paused.
   *
   * @param handler {Function} A function to execute each time the `play` event
   * is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onPlay(handler, thisArg) {
    this.bindEvent('play', event => {
      this.currentState = mediaState.PLAY;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentState, event);
      }
    });
  }

  /**
   * Fires when the audio/video was paused.
   *
   * @param handler {Function} A function to execute each time the `pause` event
   * is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onPause(handler, thisArg) {
    this.bindEvent('pause', event => {
      this.currentState = mediaState.PAUSE;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentState, event);
      }
    });
  }

  /**
   * Start buffering.
   *
   * @param handler {Function} A function to execute when audio/video starting buffering.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onWaiting(handler, thisArg) {
    this.bindEvent('waiting', event => {
      this.isBuffering = true;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the audio/video resumed playing after been paused or stopped
   * for buffering.
   *
   * @param handler {Function} A function to execute when the audio/video is resumed playing.
   * @param thisArg {Object} The this binding of handler.
   */
  onPlaying(handler, thisArg) {
    this.bindEvent('playing', event => {
      this.isBuffering = false;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the audio/video is ready to play.
   *
   * @param handler {Function} A function to execute once the audio/video is ready to play.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onCanPlay(handler, thisArg) {
    this.bindEvent('canplay', event => {
      this.isBuffering = false;
      this.totaltime = event.target.duration;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.totalTime, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the current playlist is ended.
   *
   * @param handler {Function} A function to execute when the audio/video is ended.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onEnded(handler, thisArg) {
    this.bindEvent('ended', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the current playback position was changed.
   *
   * @param handler {Function} A function to execute each time the `timeupdate`
   * event is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onTimeUpdate(handler, thisArg) {
    this.bindEvent('timeupdate', event => {
      const target = event.target;
      this.currentTime = target.currentTime;

      if (target.duration !== Infinity) {
        this.totalTime = target.duration;
        this.timeLeft = this.totalTime - this.currentTime;
        this.isLive = false;
      } else {
        this.isLive = true;
      }

      this.checkCuepoints();

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentTime, this.timeLeft, event);
      }
    });
  }

  /**
   * Check if reached the cuepoint.
   */
  checkCuepoints() {
    if (!this.cuepoints) {
      return;
    }

    const currentSecond = parseInt(this.currentTime, 10);
    this.cuepoints.forEach(cuepoint => {
      const start = parseInt(cuepoint.time, 10);

      if (currentSecond === start) {
        if (angular.isFunction(cuepoint.onEnter) && !cuepoint.$$isDirty) {
          cuepoint.onEnter(this.currentTime, cuepoint.params);
        }
        cuepoint.$$isDirty = true;
      }

      if (currentSecond > start) {
        if (angular.isFunction(cuepoint.onComplete)) {
          cuepoint.onComplete(this.currentTime, cuepoint.params);
        }
        cuepoint.$$isDirty = false;
      }

      if (currentSecond < start) {
        cuepoint.$$isDirty = false;
      }
    });
  }

  /**
   * Fires when the browser is downloading the audio/video.
   *
   * @param handler {Function} A function to execute each time the `progress`
   * event is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onProgress(handler, thisArg) {
    this.bindEvent('progress', event => {
      this.buffered = event.target.buffered;

      if (this.buffered.length && this.totalTime) {
        this.bufferedEnd = this.buffered.end(this.buffered.length - 1);

        if (angular.isFunction(handler)) {
          handler.call(thisArg, this.buffered, this.bufferedEnd, this.totalTime, event);
        }
      }
    });
  }

  /**
   * Fires when a user enter/exit fullscreen mode.
   *
   * @param handler {Function} A function to execute each time enter/exit full screen
   * mode.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onFullScreenChange(handler, thisArg) {
    const hydeoElement = _hydeoElement.get(this)[0];

    FullscreenApi.onChange(hydeoElement, event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isFullScreen, event);
      }
    });
  }

  /**
   * Fires when the user starts moving/skipping to a new position in the audio/video.
   * @param handler {Function} A function to execute each time the user moving/skiping to a new position.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onSeeking(handler, thisArg) {
    this.bindEvent('seeking', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the user is finished moving/skipping to a new position in the audio/video.
   *
   * @param handler {Function} A function to execute when the user is finished moving/skiping
   * to a new position in the audio/video.
   * @param thisArg {Object} The this binding of handler.
   */
  onSeeked(handler, thisArg) {
    this.bindEvent('seeked', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when an error occurred during the loading of an audio/video.
   *
   * @param handler {Function} A function to execute when an error occurred.
   * @param thisArg {Object} The this binding of handler.
   */
  onError(handler, thisArg) {
    this.bindEvent('error', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the volume has been changed.
   *
   * @param handler {Function} A function to execute each time change the audio/video volume.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onVolumeChange(handler, thisArg) {
    const mediaElement = _mediaElement.get(this);
    this.bindEvent('volumechange', event => {
      const volume = mediaElement.prop('volume');
      // TODO extract 100 to a configurable constant
      this.currentVolume = volume * 100;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentVolume, this.isMuted, event);
      }
    });
  }

  /**
   * Fires when the browser has loaded the current frame of the audio/video.
   *
   * @param handler {Function} A function to execute when loaded the audio/video.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onLoaded(handler, thisArg) {
    this.bindEvent('loadeddata', event => {
      this.reset();
      this.totalTime = event.target.duration;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.totalTime);
      }
    });
  }

  /**
   * Check whether the audio/video is paused or not.
   *
   * @returns {boolean} Returns `true` for the audio/video is paused, else `false`.
   */
  get isPause() {
    return this.currentState === mediaState.PAUSE;
  }

  /**
   * Check whether the audio/video is on playing or not.
   *
   * @returns {boolean} Returns `true` in case the audio/video is playing, else `false`.
   */
  get isPlay() {
    return this.currentState === mediaState.PLAY;
  }

  /**
   * Check whether the audio/video is stopped or not.
   *
   * @return {boolean} Returns `true` in case the audio/video is stopped, else `false`.
   */
  get isStop() {
    return this.currentState === mediaState.STOP;
  }

  /**
   * Determine a user enter/exit the full screen mode.
   */
  get isFullScreen() {
    return FullscreenApi.isFullscreen();
  }

  /**
   * Return `true` if the audio/video is muted, else `false`.
   *
   * @returns {boolean} Returns whether the audio/video is muted or not.
   */
  get isMuted() {
    const mediaElement = _mediaElement.get(this);
    return mediaElement.prop('muted');
  }

  /**
   * Attach a handler to an event for the video/audio elements.
   *
   * @param eventType {String} A string containing a DOM event types.
   * @param handler {Function} A function to execute each time the event is triggered.
   */
  bindEvent(eventType, handler) {
    const mediaElement = _mediaElement.get(this);
    if (eventType && angular.isFunction(handler)) {
      mediaElement.bind(eventType, handler);
    }
  }

  /**
   * Moving/skipping to a new position in the audio/video.
   *
   * @param time {number} A time point in second.
   */
  seek(time) {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('currentTime', time);
    this.currentTime = time;
  }

  /**
   * Play the audio/video if it's paused, else pause it.
   *
   * @param onPlay {Function} A function to execute when the audio/video is starting to play.
   * @param onPause {Function} A function to execute when the audio/video is starting to pause.
   *
   */
  togglePlay(onPlay, onPause) {
    this.onPause(onPause);
    this.onPlay(onPlay);

    if (this.isPlay) {
      this.pause();
    } else {
      this.play();
    }
  }

  /**
   * Enter full screen mode.
   */
  requestFullScreen() {
    const element = _hydeoElement.get(this)[0];
    FullscreenApi.request(element);
  }

  /**
   * Exit full screen mode.
   */
  exitFullScreen() {
    FullscreenApi.exit();
  }

  /**
   * Change the audio/video volume level.
   *
   * @param level {number} A number range from 0 to 100.
   *
   */
  volume(level) {
    if (!level || level < 0 || level > 100) {
      return;
    }

    const mediaElement = _mediaElement.get(this);
    // TODO extract 100 to a configurable constant.
    const volume = level / 100;
    mediaElement.prop('volume', volume);
  }

  /**
   * Enter the full screen mode if not being displayed full-screen, else exit.
   */
  toggleFullScreen() {
    if (this.isFullScreen) {
      this.exitFullScreen();
    } else {
      this.requestFullScreen();
    }
  }

  /**
   * Pauses the currently playing audio/video.
   */
  pause() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isPause && mediaElement) {
      mediaElement[0].pause();
      this.currentState = mediaState.PAUSE;
    }
  }

  /**
   * Starts playing the audio/video.
   */
  play() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isPlay && mediaElement) {
      mediaElement[0].play();
      this.currentState = mediaState.PLAY;
    }
  }

  /**
   * Pauses the currently playing audio/video and reset current time to 0.
   */
  stop() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isStop && mediaElement) {
      const elem = mediaElement[0];
      elem.pause();
      elem.currentTime = 0;
      this.currentState = mediaState.STOP;
    }
  }

  /**
   * Sets the audio/video is muted.
   */
  mute() {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('muted', true);
  }

  /**
   * Sets the audio/video to unmute.
   */
  unmute() {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('muted', false);
  }

  /**
   * Switch the audio/video to muted whether is not muted or vice verse.
   */
  toggleMuted() {
    if (this.isMuted) {
      this.unmute();
    } else {
      this.mute();
    }
  }

  /**
   * Returns a TimeRanges object representing the seekable parts of the audio/video.
   */
  seekable() {
    const elem = _mediaElement.get(this)[0];
    return elem.seekable;
  }

  /**
   * Replay the audio/video file currently loaded.
   */
  replay() {
    const elem = _mediaElement.get(this)[0];
    this.reset();
    elem.play();
  }

  /**
   * Resets the audio/video's play position to beginning.
   */
  reset() {
    const mediaElement = _mediaElement.get(this);
    this.currentTime = 0;
    this.timeLeft = 0;
    this.totalTime = 0;
    this.currentState = null;
    mediaElement.prop('currentTime', 0);
  }

  /**
   * Change the current source of the audio/video element.
   */
  changeSource(source) {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('src', source);
  }

}

servicesModule.factory('$hyMedia', () => new HyMediaService());
<commit_msg>feat: add isEnded property to hyMedia service<commit_after>/**
 * @author centsent
 */
import angular from 'angular';
import servicesModule from './_index';
import FullscreenApi from '../utils/FullscreenApi';
import {
  mediaState
}
from './../AppSettings';

const _mediaElement = new WeakMap();
const _hydeoElement = new WeakMap();

/**
 * Provide APIs and event bindings for audio/video.
 */
class HyMediaService {

  /**
   * Store the hydeo element.
   */
  setHydeoElement(element) {
    _hydeoElement.set(this, element);
  }

  /**
   * Store the audio/video element.
   */
  setMediaElement(element) {
    _mediaElement.set(this, element);
  }

  /**
   * Fires when the audio/video was started or no longer paused.
   *
   * @param handler {Function} A function to execute each time the `play` event
   * is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onPlay(handler, thisArg) {
    this.bindEvent('play', event => {
      this.currentState = mediaState.PLAY;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentState, event);
      }
    });
  }

  /**
   * Fires when the audio/video was paused.
   *
   * @param handler {Function} A function to execute each time the `pause` event
   * is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onPause(handler, thisArg) {
    this.bindEvent('pause', event => {
      this.currentState = mediaState.PAUSE;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentState, event);
      }
    });
  }

  /**
   * Start buffering.
   *
   * @param handler {Function} A function to execute when audio/video starting buffering.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onWaiting(handler, thisArg) {
    this.bindEvent('waiting', event => {
      this.isBuffering = true;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the audio/video resumed playing after been paused or stopped
   * for buffering.
   *
   * @param handler {Function} A function to execute when the audio/video is resumed playing.
   * @param thisArg {Object} The this binding of handler.
   */
  onPlaying(handler, thisArg) {
    this.bindEvent('playing', event => {
      this.isBuffering = false;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the audio/video is ready to play.
   *
   * @param handler {Function} A function to execute once the audio/video is ready to play.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onCanPlay(handler, thisArg) {
    this.bindEvent('canplay', event => {
      this.isBuffering = false;
      this.totaltime = event.target.duration;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.totalTime, this.isBuffering, event);
      }
    });
  }

  /**
   * Fires when the current playlist is ended.
   *
   * @param handler {Function} A function to execute when the audio/video is ended.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onEnded(handler, thisArg) {
    this.bindEvent('ended', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the current playback position was changed.
   *
   * @param handler {Function} A function to execute each time the `timeupdate`
   * event is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onTimeUpdate(handler, thisArg) {
    this.bindEvent('timeupdate', event => {
      const target = event.target;
      this.currentTime = target.currentTime;

      if (target.duration !== Infinity) {
        this.totalTime = target.duration;
        this.timeLeft = this.totalTime - this.currentTime;
        this.isLive = false;
      } else {
        this.isLive = true;
      }

      this.checkCuepoints();

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentTime, this.timeLeft, event);
      }
    });
  }

  /**
   * Check if reached the cuepoint.
   */
  checkCuepoints() {
    if (!this.cuepoints) {
      return;
    }

    const currentSecond = parseInt(this.currentTime, 10);
    this.cuepoints.forEach(cuepoint => {
      const start = parseInt(cuepoint.time, 10);

      if (currentSecond === start) {
        if (angular.isFunction(cuepoint.onEnter) && !cuepoint.$$isDirty) {
          cuepoint.onEnter(this.currentTime, cuepoint.params);
        }
        cuepoint.$$isDirty = true;
      }

      if (currentSecond > start) {
        if (angular.isFunction(cuepoint.onComplete)) {
          cuepoint.onComplete(this.currentTime, cuepoint.params);
        }
        cuepoint.$$isDirty = false;
      }

      if (currentSecond < start) {
        cuepoint.$$isDirty = false;
      }
    });
  }

  /**
   * Fires when the browser is downloading the audio/video.
   *
   * @param handler {Function} A function to execute each time the `progress`
   * event is triggered.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onProgress(handler, thisArg) {
    this.bindEvent('progress', event => {
      this.buffered = event.target.buffered;

      if (this.buffered.length && this.totalTime) {
        this.bufferedEnd = this.buffered.end(this.buffered.length - 1);

        if (angular.isFunction(handler)) {
          handler.call(thisArg, this.buffered, this.bufferedEnd, this.totalTime, event);
        }
      }
    });
  }

  /**
   * Fires when a user enter/exit fullscreen mode.
   *
   * @param handler {Function} A function to execute each time enter/exit full screen
   * mode.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onFullScreenChange(handler, thisArg) {
    const hydeoElement = _hydeoElement.get(this)[0];

    FullscreenApi.onChange(hydeoElement, event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.isFullScreen, event);
      }
    });
  }

  /**
   * Fires when the user starts moving/skipping to a new position in the audio/video.
   * @param handler {Function} A function to execute each time the user moving/skiping to a new position.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onSeeking(handler, thisArg) {
    this.bindEvent('seeking', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the user is finished moving/skipping to a new position in the audio/video.
   *
   * @param handler {Function} A function to execute when the user is finished moving/skiping
   * to a new position in the audio/video.
   * @param thisArg {Object} The this binding of handler.
   */
  onSeeked(handler, thisArg) {
    this.bindEvent('seeked', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when an error occurred during the loading of an audio/video.
   *
   * @param handler {Function} A function to execute when an error occurred.
   * @param thisArg {Object} The this binding of handler.
   */
  onError(handler, thisArg) {
    this.bindEvent('error', event => {
      if (angular.isFunction(handler)) {
        handler.call(thisArg, event);
      }
    });
  }

  /**
   * Fires when the volume has been changed.
   *
   * @param handler {Function} A function to execute each time change the audio/video volume.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onVolumeChange(handler, thisArg) {
    const mediaElement = _mediaElement.get(this);
    this.bindEvent('volumechange', event => {
      const volume = mediaElement.prop('volume');
      // TODO extract 100 to a configurable constant
      this.currentVolume = volume * 100;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.currentVolume, this.isMuted, event);
      }
    });
  }

  /**
   * Fires when the browser has loaded the current frame of the audio/video.
   *
   * @param handler {Function} A function to execute when loaded the audio/video.
   * @param thisArg {Object} The this binding of handler.
   *
   */
  onLoaded(handler, thisArg) {
    this.bindEvent('loadeddata', event => {
      this.reset();
      this.totalTime = event.target.duration;

      if (angular.isFunction(handler)) {
        handler.call(thisArg, this.totalTime);
      }
    });
  }

  /**
   * Check whether the audio/video is paused or not.
   *
   * @returns {boolean} Returns `true` for the audio/video is paused, else `false`.
   */
  get isPause() {
    return this.currentState === mediaState.PAUSE;
  }

  /**
   * Check whether the audio/video is on playing or not.
   *
   * @returns {boolean} Returns `true` in case the audio/video is playing, else `false`.
   */
  get isPlay() {
    return this.currentState === mediaState.PLAY;
  }

  /**
   * Check whether the audio/video is stopped or not.
   *
   * @return {boolean} Returns `true` in case the audio/video is stopped, else `false`.
   */
  get isStop() {
    return this.currentState === mediaState.STOP;
  }

  /**
   * Determine a user enter/exit the full screen mode.
   */
  get isFullScreen() {
    return FullscreenApi.isFullscreen();
  }

  /**
   * Return `true` if the audio/video is muted, else `false`.
   *
   * @returns {boolean} Returns whether the audio/video is muted or not.
   */
  get isMuted() {
    const mediaElement = _mediaElement.get(this);
    return mediaElement.prop('muted');
  }

  /**
   * Returns whether the playback of the audio/video has ended or not.
   */
  get isEnded() {
    const mediaElement = _mediaElement.get(this);
    return mediaElement.prop('ended');
  }

  /**
   * Attach a handler to an event for the video/audio elements.
   *
   * @param eventType {String} A string containing a DOM event types.
   * @param handler {Function} A function to execute each time the event is triggered.
   */
  bindEvent(eventType, handler) {
    const mediaElement = _mediaElement.get(this);
    if (eventType && angular.isFunction(handler)) {
      mediaElement.bind(eventType, handler);
    }
  }

  /**
   * Moving/skipping to a new position in the audio/video.
   *
   * @param time {number} A time point in second.
   */
  seek(time) {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('currentTime', time);
    this.currentTime = time;
  }

  /**
   * Play the audio/video if it's paused, else pause it.
   *
   * @param onPlay {Function} A function to execute when the audio/video is starting to play.
   * @param onPause {Function} A function to execute when the audio/video is starting to pause.
   *
   */
  togglePlay(onPlay, onPause) {
    this.onPause(onPause);
    this.onPlay(onPlay);

    if (this.isPlay) {
      this.pause();
    } else {
      this.play();
    }
  }

  /**
   * Enter full screen mode.
   */
  requestFullScreen() {
    const element = _hydeoElement.get(this)[0];
    FullscreenApi.request(element);
  }

  /**
   * Exit full screen mode.
   */
  exitFullScreen() {
    FullscreenApi.exit();
  }

  /**
   * Change the audio/video volume level.
   *
   * @param level {number} A number range from 0 to 100.
   *
   */
  volume(level) {
    if (!level || level < 0 || level > 100) {
      return;
    }

    const mediaElement = _mediaElement.get(this);
    // TODO extract 100 to a configurable constant.
    const volume = level / 100;
    mediaElement.prop('volume', volume);
  }

  /**
   * Enter the full screen mode if not being displayed full-screen, else exit.
   */
  toggleFullScreen() {
    if (this.isFullScreen) {
      this.exitFullScreen();
    } else {
      this.requestFullScreen();
    }
  }

  /**
   * Pauses the currently playing audio/video.
   */
  pause() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isPause && mediaElement) {
      mediaElement[0].pause();
      this.currentState = mediaState.PAUSE;
    }
  }

  /**
   * Starts playing the audio/video.
   */
  play() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isPlay && mediaElement) {
      mediaElement[0].play();
      this.currentState = mediaState.PLAY;
    }
  }

  /**
   * Pauses the currently playing audio/video and reset current time to 0.
   */
  stop() {
    const mediaElement = _mediaElement.get(this);

    if (!this.isStop && mediaElement) {
      const elem = mediaElement[0];
      elem.pause();
      elem.currentTime = 0;
      this.currentState = mediaState.STOP;
    }
  }

  /**
   * Sets the audio/video is muted.
   */
  mute() {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('muted', true);
  }

  /**
   * Sets the audio/video to unmute.
   */
  unmute() {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('muted', false);
  }

  /**
   * Switch the audio/video to muted whether is not muted or vice verse.
   */
  toggleMuted() {
    if (this.isMuted) {
      this.unmute();
    } else {
      this.mute();
    }
  }

  /**
   * Returns a TimeRanges object representing the seekable parts of the audio/video.
   */
  seekable() {
    const mediaElement = _mediaElement.get(this);
    return mediaElement.prop('seekable');
  }

  /**
   * Replay the audio/video file currently loaded.
   */
  replay() {
    const elem = _mediaElement.get(this)[0];
    this.reset();
    elem.play();
  }

  /**
   * Resets the audio/video's play position to beginning.
   */
  reset() {
    const mediaElement = _mediaElement.get(this);
    this.currentTime = 0;
    this.timeLeft = 0;
    this.totalTime = 0;
    this.currentState = null;
    mediaElement.prop('currentTime', 0);
  }

  /**
   * Change the current source of the audio/video element.
   */
  changeSource(source) {
    const mediaElement = _mediaElement.get(this);
    mediaElement.prop('src', source);
  }

}

servicesModule.factory('$hyMedia', () => new HyMediaService());
<|endoftext|>


<commit_before>import jwt from 'jsonwebtoken';
import { pick } from 'lodash';
import bcrypt from 'bcryptjs';

import FieldError from '../../../common/FieldError';

export const createTokens = async (user, secret, refreshSecret) => {
  const createToken = jwt.sign(
    {
      user: pick(user, ['id', 'username', 'isAdmin'])
    },
    secret,
    {
      expiresIn: '1m'
    }
  );

  const createRefreshToken = jwt.sign(
    {
      user: user.id
    },
    refreshSecret,
    {
      expiresIn: '7d'
    }
  );

  return Promise.all([createToken, createRefreshToken]);
};

export const refreshTokens = async (token, refreshToken, User, SECRET) => {
  let userId = -1;
  try {
    const { user } = jwt.decode(refreshToken);
    userId = user;
  } catch (err) {
    console.log(err);
    return {};
  }

  const user = await User.getUserWithPassword(userId);
  if (!user) {
    return {};
  }
  const refreshSecret = SECRET + user.password;

  try {
    jwt.verify(refreshToken, refreshSecret);
  } catch (err) {
    return {};
  }

  const [newToken, newRefreshToken] = await createTokens(user, SECRET, refreshSecret);

  return {
    token: newToken,
    refreshToken: newRefreshToken,
    user: pick(user, ['id', 'username', 'isAdmin'])
  };
};

export const tryLogin = async (email, password, User, SECRET) => {
  const e = new FieldError();
  const localAuth = await User.getLocalOuthByEmail(email);

  if (!localAuth) {
    // user with provided email not found
    e.setError('email', 'Please enter a valid e-mail.');
  }

  const valid = await bcrypt.compare(password, localAuth.password);
  if (!valid) {
    // bad password
    e.setError('password', 'Please enter a valid password.');
  }

  e.throwIf();

  const user = await User.getUserWithPassword(localAuth.userId);
  const refreshSecret = SECRET + user.password;

  const [token, refreshToken] = await createTokens(user, SECRET, refreshSecret);

  return {
    token,
    refreshToken
  };
};

export const tryLoginSerial = async (serial, User, SECRET) => {
  try {
    const certAuth = await User.getUserWithSerial(serial);

    const user = await User.getUserWithPassword(certAuth.id);

    const refreshSecret = SECRET + user.password;
    const [token, refreshToken] = await createTokens(user, SECRET, refreshSecret);

    return {
      user,
      token,
      refreshToken
    };
  } catch (err) {
    console.log(err);
    return {};
  }
};
<commit_msg>throw error if email not found (#392)<commit_after>import jwt from 'jsonwebtoken';
import { pick } from 'lodash';
import bcrypt from 'bcryptjs';

import FieldError from '../../../common/FieldError';

export const createTokens = async (user, secret, refreshSecret) => {
  const createToken = jwt.sign(
    {
      user: pick(user, ['id', 'username', 'isAdmin'])
    },
    secret,
    {
      expiresIn: '1m'
    }
  );

  const createRefreshToken = jwt.sign(
    {
      user: user.id
    },
    refreshSecret,
    {
      expiresIn: '7d'
    }
  );

  return Promise.all([createToken, createRefreshToken]);
};

export const refreshTokens = async (token, refreshToken, User, SECRET) => {
  let userId = -1;
  try {
    const { user } = jwt.decode(refreshToken);
    userId = user;
  } catch (err) {
    console.log(err);
    return {};
  }

  const user = await User.getUserWithPassword(userId);
  if (!user) {
    return {};
  }
  const refreshSecret = SECRET + user.password;

  try {
    jwt.verify(refreshToken, refreshSecret);
  } catch (err) {
    return {};
  }

  const [newToken, newRefreshToken] = await createTokens(user, SECRET, refreshSecret);

  return {
    token: newToken,
    refreshToken: newRefreshToken,
    user: pick(user, ['id', 'username', 'isAdmin'])
  };
};

export const tryLogin = async (email, password, User, SECRET) => {
  const e = new FieldError();
  const localAuth = await User.getLocalOuthByEmail(email);

  if (!localAuth) {
    // user with provided email not found
    e.setError('email', 'Please enter a valid e-mail.');
  }

  e.throwIf();

  const valid = await bcrypt.compare(password, localAuth.password);
  if (!valid) {
    // bad password
    e.setError('password', 'Please enter a valid password.');
  }

  e.throwIf();

  const user = await User.getUserWithPassword(localAuth.userId);
  const refreshSecret = SECRET + user.password;

  const [token, refreshToken] = await createTokens(user, SECRET, refreshSecret);

  return {
    token,
    refreshToken
  };
};

export const tryLoginSerial = async (serial, User, SECRET) => {
  try {
    const certAuth = await User.getUserWithSerial(serial);

    const user = await User.getUserWithPassword(certAuth.id);

    const refreshSecret = SECRET + user.password;
    const [token, refreshToken] = await createTokens(user, SECRET, refreshSecret);

    return {
      user,
      token,
      refreshToken
    };
  } catch (err) {
    console.log(err);
    return {};
  }
};
<|endoftext|>


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

import {layoutRectLtwh} from '../layout-rect';
import {registerServiceBuilder, getService} from '../service';
import {Services} from '../services';
import {whenDocumentComplete} from '../document-ready';
import {getMode} from '../mode';
import {isCanary} from '../experiments';
import {throttle} from '../utils/rate-limit';
import {dict, map} from '../utils/object';

/**
 * Maximum number of tick events we allow to accumulate in the performance
 * instance's queue before we start dropping those events and can no longer
 * be forwarded to the actual `tick` function when it is set.
 */
const QUEUE_LIMIT = 50;

/**
 * Fields:
 * {{
 *   label: string,
 *   delta: (number|null|undefined),
 *   value: (number|null|undefined)
 * }}
 * @typedef {!JsonObject}
 */
let TickEventDef;

/**
 * Increments the value, else defaults to 0 for the given object key.
 * @param {!Object<string, (string|number|boolean|Array|Object|null)>} obj
 * @param {?string} name
 */
function incOrDef(obj, name) {
  if (!name) {
    return;
  }

  if (!obj[name]) {
    obj[name] = 1;
  } else {
    obj[name]++;
  }
}

/**
 * Performance holds the mechanism to call `tick` to stamp out important
 * events in the lifecycle of the AMP runtime. It can hold a small amount
 * of tick events to forward to the external `tick` function when it is set.
 */
export class Performance {

  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @const {!Window} */
    this.win = win;

    /** @private @const {number} */
    this.initTime_ = this.win.Date.now();

    /** @const @private {!Array<TickEventDef>} */
    this.events_ = [];

    /** @private {?./viewer-impl.Viewer} */
    this.viewer_ = null;

    /** @private {?./resources-impl.Resources} */
    this.resources_ = null;

    /** @private {boolean} */
    this.isMessagingReady_ = false;

    /** @private {boolean} */
    this.isPerformanceTrackingOn_ = false;

    /** @private {!Object<string,boolean>} */
    this.enabledExperiments_ = map();
    /** @private {string} */
    this.ampexp_ = '';

    // Add RTV version as experiment ID, so we can slice the data by version.
    this.addEnabledExperiment('rtv-' + getMode(this.win).rtvVersion);
    if (isCanary(this.win)) {
      this.addEnabledExperiment('canary');
    }

    // Tick window.onload event.
    whenDocumentComplete(win.document).then(() => this.onload_());
    this.registerPaintTimingObserver_();
  }

  /**
   * Listens to viewer and resource events.
   * @return {!Promise}
   */
  coreServicesAvailable() {
    this.viewer_ = Services.viewerForDoc(this.win.document);
    this.resources_ = Services.resourcesForDoc(this.win.document);

    this.isPerformanceTrackingOn_ = this.viewer_.isEmbedded() &&
        this.viewer_.getParam('csi') === '1';

    // This is for redundancy. Call flush on any visibility change.
    this.viewer_.onVisibilityChanged(this.flush.bind(this));

    // Does not need to wait for messaging ready since it will be queued
    // if it isn't ready.
    this.measureUserPerceivedVisualCompletenessTime_();

    // Can be null which would mean this AMP page is not embedded
    // and has no messaging channel.
    const channelPromise = this.viewer_.whenMessagingReady();

    this.viewer_.whenFirstVisible().then(() => {
      this.tick('ofv');
      this.flush();
    });

    // We don't check `isPerformanceTrackingOn` here since there are some
    // events that we call on the viewer even though performance tracking
    // is off we only need to know if the AMP page has a messaging
    // channel or not.
    if (!channelPromise) {
      return Promise.resolve();
    }

    return channelPromise.then(() => {
      this.isMessagingReady_ = true;

      // Tick the "messaging ready" signal.
      this.tickDelta('msr', this.win.Date.now() - this.initTime_);

      // Forward all queued ticks to the viewer since messaging
      // is now ready.
      this.flushQueuedTicks_();

      // Send all csi ticks through.
      this.flush();
    });
  }

  onload_() {
    this.tick('ol');
    this.tickLegacyFirstPaintTime_();
    this.flush();
  }

  /**
   * Reports first pain and first contentful paint timings.
   * See https://github.com/WICG/paint-timing
   */
  registerPaintTimingObserver_() {
    if (!this.win.PerformancePaintTiming) {
      return;
    }
    const observer = new this.win.PerformanceObserver(list => {
      list.getEntries().forEach(entry => {
        if (entry.name == 'first-paint') {
          this.tickDelta('fp', entry.startTime + entry.duration);
          this.flush();
        }
        else if (entry.name == 'first-contentful-paint') {
          this.tickDelta('fcp', entry.startTime + entry.duration);
          this.flush();
        }
      });
    });

    observer.observe({entryTypes: ['paint']});
  }

  /**
   * Tick fp time based on Chrome's legacy paint timing API when
   * appropriate.
   * `registerPaintTimingObserver_` calls the standards based API and this
   * method does nothing if it is available.
   */
  tickLegacyFirstPaintTime_() {
    // Detect deprecated first pain time API
    // https://bugs.chromium.org/p/chromium/issues/detail?id=621512
    // We'll use this until something better is available.
    if (!this.win.PerformancePaintTiming
        && this.win.chrome
        && typeof this.win.chrome.loadTimes == 'function') {
      const fpTime = this.win.chrome.loadTimes().firstPaintTime
          * 1000 - this.win.performance.timing.navigationStart;
      if (fpTime <= 1) {
        // Throw away bad data generated from an apparent Chrome bug
        // that is fixed in later Chrome versions.
        return;
      }
      this.tickDelta('fp', fpTime);
    }
  }

  /**
   * Measure the delay the user perceives of how long it takes
   * to load the initial viewport.
   * @private
   */
  measureUserPerceivedVisualCompletenessTime_() {
    const didStartInPrerender = !this.viewer_.hasBeenVisible();
    let docVisibleTime = didStartInPrerender ? -1 : this.initTime_;

    // This is only relevant if the viewer is in prerender mode.
    // (hasn't been visible yet, ever at this point)
    if (didStartInPrerender) {
      this.viewer_.whenFirstVisible().then(() => {
        docVisibleTime = this.win.Date.now();
      });
    }

    this.whenViewportLayoutComplete_().then(() => {
      if (didStartInPrerender) {
        const userPerceivedVisualCompletenesssTime = docVisibleTime > -1
            ? (this.win.Date.now() - docVisibleTime)
            //  Prerender was complete before visibility.
            : 0;
        this.viewer_.whenFirstVisible().then(() => {
          // We only tick this if the page eventually becomes visible,
          // since otherwise we heavily skew the metric towards the
          // 0 case, since pre-renders that are never used are highly
          // likely to fully load before they are never used :)
          this.tickDelta('pc', userPerceivedVisualCompletenesssTime);
        });
        this.prerenderComplete_(userPerceivedVisualCompletenesssTime);
      } else {
        // If it didnt start in prerender, no need to calculate anything
        // and we just need to tick `pc`. (it will give us the relative
        // time since the viewer initialized the timer)
        this.tick('pc');
        // We don't have the actual csi timer's clock start time,
        // so we just have to use `docVisibleTime`.
        this.prerenderComplete_(this.win.Date.now() - docVisibleTime);
      }
      this.flush();
    });
  }

  /**
   * Returns a promise that is resolved when resources in viewport
   * have been finished being laid out.
   * @return {!Promise}
   * @private
   */
  whenViewportLayoutComplete_() {
    const size = Services.viewportForDoc(this.win.document).getSize();
    const rect = layoutRectLtwh(0, 0, size.width, size.height);
    return this.resources_.getResourcesInRect(
        this.win, rect, /* isInPrerender */ true)
        .then(resources => Promise.all(resources.map(r => r.loadedOnce())));
  }

  /**
   * Ticks a timing event.
   *
   * @param {string} label The variable name as it will be reported.
   *     See TICKEVENTS.md for available metrics, and edit this file
   *     when adding a new metric.
   * @param {number=} opt_delta The delta. Call tickDelta instead of setting
   *     this directly.
   */
  tick(label, opt_delta) {
    const value = (opt_delta == undefined) ? this.win.Date.now() : undefined;

    const data = dict({
      'label': label,
      'value': value,
      // Delta can negative, but will always be changed to 0.
      'delta': opt_delta != null ? Math.max(opt_delta, 0) : undefined,
    });
    if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) {
      this.viewer_.sendMessage('tick', data);
    } else {
      this.queueTick_(data);
    }
    // Add browser performance timeline entries for simple ticks.
    // These are for example exposed in WPT.
    if (this.win.performance
        && this.win.performance.mark
        && arguments.length == 1) {
      this.win.performance.mark(label);
    }
  }

  /**
   * Tick a very specific value for the label. Use this method if you
   * measure the time it took to do something yourself.
   * @param {string} label The variable name as it will be reported.
   * @param {number} value The value in milliseconds that should be ticked.
   */
  tickDelta(label, value) {
    this.tick(label, value);
  }

  /**
   * Tick time delta since the document has become visible.
   * @param {string} label The variable name as it will be reported.
   */
  tickSinceVisible(label) {
    const now = this.win.Date.now();
    const visibleTime = this.viewer_ ? this.viewer_.getFirstVisibleTime() : 0;
    const v = visibleTime ? Math.max(now - visibleTime, 0) : 0;
    this.tickDelta(label, v);
  }

  /**
   * Ask the viewer to flush the ticks
   */
  flush() {
    if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) {
      this.viewer_.sendMessage('sendCsi', dict({
        'ampexp': this.ampexp_,
      }), /* cancelUnsent */true);
    }
  }

  /**
   * Flush with a rate limit of 10 per second.
   */
  throttledFlush() {
    if (!this.throttledFlush_) {
      /** @private {function()} */
      this.throttledFlush_ = throttle(this.win, this.flush.bind(this), 100);
    }
    this.throttledFlush_();
  }

  /**
   * @param {string} experimentId
   */
  addEnabledExperiment(experimentId) {
    this.enabledExperiments_[experimentId] = true;
    this.ampexp_ = Object.keys(this.enabledExperiments_).join(',');
  }

  /**
   * Queues the events to be flushed when tick function is set.
   *
   * @param {TickEventDef} data Tick data to be queued.
   * @private
   */
  queueTick_(data) {
    // Start dropping the head of the queue if we've reached the limit
    // so that we don't take up too much memory in the runtime.
    if (this.events_.length >= QUEUE_LIMIT) {
      this.events_.shift();
    }

    this.events_.push(data);
  }

  /**
   * Forwards all queued ticks to the viewer tick method.
   * @private
   */
  flushQueuedTicks_() {
    if (!this.viewer_) {
      return;
    }

    if (!this.isPerformanceTrackingOn_) {
      // drop all queued ticks to not leak
      this.events_.length = 0;
      return;
    }

    this.events_.forEach(tickEvent => {
      this.viewer_.sendMessage('tick', tickEvent);
    });
    this.events_.length = 0;
  }

  /**
   * @private
   * @param {number} value
   */
  prerenderComplete_(value) {
    if (this.viewer_) {
      this.viewer_.sendMessage('prerenderComplete', dict({'value': value}),
          /* cancelUnsent */true);
    }
  }

  /**
   * Identifies if the viewer is able to track performance.
   * If the document is not embedded, there is no messaging channel,
   * so no performance tracking is needed since there is nobody to forward the events.
   * @return {boolean}
   */
  isPerformanceTrackingOn() {
    return this.isPerformanceTrackingOn_;
  }
}

/**
 * @param {!Window} window
 */
export function installPerformanceService(window) {
  registerServiceBuilder(window, 'performance', Performance);
}

/**
 * @param {!Window} window
 * @return {!Performance}
 */
export function performanceFor(window) {
  return getService(window, 'performance');
}
<commit_msg>Explicitly report past Performance Timineline values. (#11521)<commit_after>/**
 * Copyright 2015 The AMP HTML Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS-IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {layoutRectLtwh} from '../layout-rect';
import {registerServiceBuilder, getService} from '../service';
import {Services} from '../services';
import {whenDocumentComplete} from '../document-ready';
import {getMode} from '../mode';
import {isCanary} from '../experiments';
import {throttle} from '../utils/rate-limit';
import {dict, map} from '../utils/object';

/**
 * Maximum number of tick events we allow to accumulate in the performance
 * instance's queue before we start dropping those events and can no longer
 * be forwarded to the actual `tick` function when it is set.
 */
const QUEUE_LIMIT = 50;

/**
 * Fields:
 * {{
 *   label: string,
 *   delta: (number|null|undefined),
 *   value: (number|null|undefined)
 * }}
 * @typedef {!JsonObject}
 */
let TickEventDef;

/**
 * Increments the value, else defaults to 0 for the given object key.
 * @param {!Object<string, (string|number|boolean|Array|Object|null)>} obj
 * @param {?string} name
 */
function incOrDef(obj, name) {
  if (!name) {
    return;
  }

  if (!obj[name]) {
    obj[name] = 1;
  } else {
    obj[name]++;
  }
}

/**
 * Performance holds the mechanism to call `tick` to stamp out important
 * events in the lifecycle of the AMP runtime. It can hold a small amount
 * of tick events to forward to the external `tick` function when it is set.
 */
export class Performance {

  /**
   * @param {!Window} win
   */
  constructor(win) {
    /** @const {!Window} */
    this.win = win;

    /** @private @const {number} */
    this.initTime_ = this.win.Date.now();

    /** @const @private {!Array<TickEventDef>} */
    this.events_ = [];

    /** @private {?./viewer-impl.Viewer} */
    this.viewer_ = null;

    /** @private {?./resources-impl.Resources} */
    this.resources_ = null;

    /** @private {boolean} */
    this.isMessagingReady_ = false;

    /** @private {boolean} */
    this.isPerformanceTrackingOn_ = false;

    /** @private {!Object<string,boolean>} */
    this.enabledExperiments_ = map();
    /** @private {string} */
    this.ampexp_ = '';

    // Add RTV version as experiment ID, so we can slice the data by version.
    this.addEnabledExperiment('rtv-' + getMode(this.win).rtvVersion);
    if (isCanary(this.win)) {
      this.addEnabledExperiment('canary');
    }

    // Tick window.onload event.
    whenDocumentComplete(win.document).then(() => this.onload_());
    this.registerPaintTimingObserver_();
  }

  /**
   * Listens to viewer and resource events.
   * @return {!Promise}
   */
  coreServicesAvailable() {
    this.viewer_ = Services.viewerForDoc(this.win.document);
    this.resources_ = Services.resourcesForDoc(this.win.document);

    this.isPerformanceTrackingOn_ = this.viewer_.isEmbedded() &&
        this.viewer_.getParam('csi') === '1';

    // This is for redundancy. Call flush on any visibility change.
    this.viewer_.onVisibilityChanged(this.flush.bind(this));

    // Does not need to wait for messaging ready since it will be queued
    // if it isn't ready.
    this.measureUserPerceivedVisualCompletenessTime_();

    // Can be null which would mean this AMP page is not embedded
    // and has no messaging channel.
    const channelPromise = this.viewer_.whenMessagingReady();

    this.viewer_.whenFirstVisible().then(() => {
      this.tick('ofv');
      this.flush();
    });

    // We don't check `isPerformanceTrackingOn` here since there are some
    // events that we call on the viewer even though performance tracking
    // is off we only need to know if the AMP page has a messaging
    // channel or not.
    if (!channelPromise) {
      return Promise.resolve();
    }

    return channelPromise.then(() => {
      this.isMessagingReady_ = true;

      // Tick the "messaging ready" signal.
      this.tickDelta('msr', this.win.Date.now() - this.initTime_);

      // Forward all queued ticks to the viewer since messaging
      // is now ready.
      this.flushQueuedTicks_();

      // Send all csi ticks through.
      this.flush();
    });
  }

  onload_() {
    this.tick('ol');
    this.tickLegacyFirstPaintTime_();
    this.flush();
  }

  /**
   * Reports first pain and first contentful paint timings.
   * See https://github.com/WICG/paint-timing
   */
  registerPaintTimingObserver_() {
    if (!this.win.PerformancePaintTiming) {
      return;
    }
    // Chrome doesn't implement the buffered flag for PerformanceObserver.
    // That means we need to read existing entries and maintain state
    // as to whether we have reported a value yet, since in the future it may
    // be reported twice.
    // https://bugs.chromium.org/p/chromium/issues/detail?id=725567
    let recordedFirstPaint = false;
    let recordedFirstContentfulPaint = false;
    const processEntry = entry => {
      if (entry.name == 'first-paint' && !recordedFirstPaint) {
        this.tickDelta('fp', entry.startTime + entry.duration);
        recordedFirstPaint = true;
      }
      else if (entry.name == 'first-contentful-paint'
          && !recordedFirstContentfulPaint) {
        this.tickDelta('fcp', entry.startTime + entry.duration);
        recordedFirstContentfulPaint = true;
      }
    };
    const observer = new this.win.PerformanceObserver(list => {
      list.getEntries().forEach(processEntry);
      this.flush();
    });
    // Programmatically read once as currently PerformanceObserver does not
    // report past entries as of Chrome 61.
    // https://bugs.chromium.org/p/chromium/issues/detail?id=725567
    this.win.performance.getEntriesByType('paint').forEach(processEntry);

    observer.observe({entryTypes: ['paint']});
  }

  /**
   * Tick fp time based on Chrome's legacy paint timing API when
   * appropriate.
   * `registerPaintTimingObserver_` calls the standards based API and this
   * method does nothing if it is available.
   */
  tickLegacyFirstPaintTime_() {
    // Detect deprecated first pain time API
    // https://bugs.chromium.org/p/chromium/issues/detail?id=621512
    // We'll use this until something better is available.
    if (!this.win.PerformancePaintTiming
        && this.win.chrome
        && typeof this.win.chrome.loadTimes == 'function') {
      const fpTime = this.win.chrome.loadTimes().firstPaintTime
          * 1000 - this.win.performance.timing.navigationStart;
      if (fpTime <= 1) {
        // Throw away bad data generated from an apparent Chrome bug
        // that is fixed in later Chrome versions.
        return;
      }
      this.tickDelta('fp', fpTime);
    }
  }

  /**
   * Measure the delay the user perceives of how long it takes
   * to load the initial viewport.
   * @private
   */
  measureUserPerceivedVisualCompletenessTime_() {
    const didStartInPrerender = !this.viewer_.hasBeenVisible();
    let docVisibleTime = didStartInPrerender ? -1 : this.initTime_;

    // This is only relevant if the viewer is in prerender mode.
    // (hasn't been visible yet, ever at this point)
    if (didStartInPrerender) {
      this.viewer_.whenFirstVisible().then(() => {
        docVisibleTime = this.win.Date.now();
      });
    }

    this.whenViewportLayoutComplete_().then(() => {
      if (didStartInPrerender) {
        const userPerceivedVisualCompletenesssTime = docVisibleTime > -1
            ? (this.win.Date.now() - docVisibleTime)
            //  Prerender was complete before visibility.
            : 0;
        this.viewer_.whenFirstVisible().then(() => {
          // We only tick this if the page eventually becomes visible,
          // since otherwise we heavily skew the metric towards the
          // 0 case, since pre-renders that are never used are highly
          // likely to fully load before they are never used :)
          this.tickDelta('pc', userPerceivedVisualCompletenesssTime);
        });
        this.prerenderComplete_(userPerceivedVisualCompletenesssTime);
      } else {
        // If it didnt start in prerender, no need to calculate anything
        // and we just need to tick `pc`. (it will give us the relative
        // time since the viewer initialized the timer)
        this.tick('pc');
        // We don't have the actual csi timer's clock start time,
        // so we just have to use `docVisibleTime`.
        this.prerenderComplete_(this.win.Date.now() - docVisibleTime);
      }
      this.flush();
    });
  }

  /**
   * Returns a promise that is resolved when resources in viewport
   * have been finished being laid out.
   * @return {!Promise}
   * @private
   */
  whenViewportLayoutComplete_() {
    const size = Services.viewportForDoc(this.win.document).getSize();
    const rect = layoutRectLtwh(0, 0, size.width, size.height);
    return this.resources_.getResourcesInRect(
        this.win, rect, /* isInPrerender */ true)
        .then(resources => Promise.all(resources.map(r => r.loadedOnce())));
  }

  /**
   * Ticks a timing event.
   *
   * @param {string} label The variable name as it will be reported.
   *     See TICKEVENTS.md for available metrics, and edit this file
   *     when adding a new metric.
   * @param {number=} opt_delta The delta. Call tickDelta instead of setting
   *     this directly.
   */
  tick(label, opt_delta) {
    const value = (opt_delta == undefined) ? this.win.Date.now() : undefined;

    const data = dict({
      'label': label,
      'value': value,
      // Delta can negative, but will always be changed to 0.
      'delta': opt_delta != null ? Math.max(opt_delta, 0) : undefined,
    });
    if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) {
      this.viewer_.sendMessage('tick', data);
    } else {
      this.queueTick_(data);
    }
    // Add browser performance timeline entries for simple ticks.
    // These are for example exposed in WPT.
    if (this.win.performance
        && this.win.performance.mark
        && arguments.length == 1) {
      this.win.performance.mark(label);
    }
  }

  /**
   * Tick a very specific value for the label. Use this method if you
   * measure the time it took to do something yourself.
   * @param {string} label The variable name as it will be reported.
   * @param {number} value The value in milliseconds that should be ticked.
   */
  tickDelta(label, value) {
    this.tick(label, value);
  }

  /**
   * Tick time delta since the document has become visible.
   * @param {string} label The variable name as it will be reported.
   */
  tickSinceVisible(label) {
    const now = this.win.Date.now();
    const visibleTime = this.viewer_ ? this.viewer_.getFirstVisibleTime() : 0;
    const v = visibleTime ? Math.max(now - visibleTime, 0) : 0;
    this.tickDelta(label, v);
  }

  /**
   * Ask the viewer to flush the ticks
   */
  flush() {
    if (this.isMessagingReady_ && this.isPerformanceTrackingOn_) {
      this.viewer_.sendMessage('sendCsi', dict({
        'ampexp': this.ampexp_,
      }), /* cancelUnsent */true);
    }
  }

  /**
   * Flush with a rate limit of 10 per second.
   */
  throttledFlush() {
    if (!this.throttledFlush_) {
      /** @private {function()} */
      this.throttledFlush_ = throttle(this.win, this.flush.bind(this), 100);
    }
    this.throttledFlush_();
  }

  /**
   * @param {string} experimentId
   */
  addEnabledExperiment(experimentId) {
    this.enabledExperiments_[experimentId] = true;
    this.ampexp_ = Object.keys(this.enabledExperiments_).join(',');
  }

  /**
   * Queues the events to be flushed when tick function is set.
   *
   * @param {TickEventDef} data Tick data to be queued.
   * @private
   */
  queueTick_(data) {
    // Start dropping the head of the queue if we've reached the limit
    // so that we don't take up too much memory in the runtime.
    if (this.events_.length >= QUEUE_LIMIT) {
      this.events_.shift();
    }

    this.events_.push(data);
  }

  /**
   * Forwards all queued ticks to the viewer tick method.
   * @private
   */
  flushQueuedTicks_() {
    if (!this.viewer_) {
      return;
    }

    if (!this.isPerformanceTrackingOn_) {
      // drop all queued ticks to not leak
      this.events_.length = 0;
      return;
    }

    this.events_.forEach(tickEvent => {
      this.viewer_.sendMessage('tick', tickEvent);
    });
    this.events_.length = 0;
  }

  /**
   * @private
   * @param {number} value
   */
  prerenderComplete_(value) {
    if (this.viewer_) {
      this.viewer_.sendMessage('prerenderComplete', dict({'value': value}),
          /* cancelUnsent */true);
    }
  }

  /**
   * Identifies if the viewer is able to track performance.
   * If the document is not embedded, there is no messaging channel,
   * so no performance tracking is needed since there is nobody to forward the events.
   * @return {boolean}
   */
  isPerformanceTrackingOn() {
    return this.isPerformanceTrackingOn_;
  }
}

/**
 * @param {!Window} window
 */
export function installPerformanceService(window) {
  registerServiceBuilder(window, 'performance', Performance);
}

/**
 * @param {!Window} window
 * @return {!Performance}
 */
export function performanceFor(window) {
  return getService(window, 'performance');
}
<|endoftext|>


<commit_before>import {parseGameType} from 'app/services/GameTypes';
import moment from 'moment-timezone';
import he from 'he';
import _ from 'lodash';

const IP_REGEX = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})?/g;
const DOMAIN_REGEX = /[^\w](IP|Address).*?([a-z\d]([a-z\d\-]{0,61}[a-z\d])?(\.[a-z\d]([a-z\d\-]{0,61}[a-z\d])?)+(:\d{1,5})?)/gi; // jscs:ignore maximumLineLength
const SIZE_REGEX = /To([\dX]+)/i;
const EXTRAS_REGEX = /(\[.*?])/g;

class MatchPostParser {
    constructor(DateTime, $location, $log) {
        this.DateTime = DateTime;
        this.$location = $location;
        this.$log = $log;
    }

    _parseTitle(title) {
        let details = {
            title,
            extras: []
        };

        let pipeIndex = title.indexOf('|');
        details.isStartTime = pipeIndex !== -1;

        // Replace the | with a -
        if (details.isStartTime) {
            title = title.substr(0, pipeIndex) + '-' + title.substr(pipeIndex + 1);
        }

        let sections = title
            .split('-')
            .map(part => part.trim());

        // Post isnt formatted correctly, don't display it at all
        if (sections.length < 3) {
            throw new Error('Not enough parts in post title');
        }

        let [dateRegionSection, titleSection, teamStyleSection, scenariosSection] = sections;

        // Grab the opening time and region from the first section
        _.merge(details, this._parseDateAndRegion(dateRegionSection));

        // Copy the title across without the other sections
        details.title = titleSection;

        // Assume vanilla if no gamemodes section
        if (_.isUndefined(scenariosSection)) {
            details.gamemodes = ['Vanilla'];

            // Cut out the extras from team types
            let extrasInfo = this._parseExtras(teamStyleSection);
            details.extras = extrasInfo.extras;
            teamStyleSection = extrasInfo.section;
        } else {
            // Add gamemodes
            details.gamemodes = this._parseScenarios(scenariosSection);

            // Cut out the extras from the final scenario

            if (details.gamemodes.length > 0) {
                let finalIndex = details.gamemodes.length - 1;

                let extrasInfo = this._parseExtras(details.gamemodes[finalIndex]);
                details.extras = extrasInfo.extras;
                details.gamemodes[finalIndex] = extrasInfo.section;
            }
        }

        let teamInfo = this._parseTeamStyle(teamStyleSection);
        details.teams = teamInfo.type;
        details.teamSize = teamInfo.size;

        return details;
    }

    _parseTeamStyle(section) {
        section = section.trim();

        SIZE_REGEX.exec('');
        let sizeCheck = SIZE_REGEX.exec(section);

        let size = 0;
        let style = section;

        // Check for a teamsize if we can
        if (!_.isNull(sizeCheck)) {
            size = sizeCheck[1];
            style = section.replace(SIZE_REGEX, '').trim();
        }

        // Attempt to get the game type
        let type = parseGameType(style);

        return {
            type: _.isUndefined(type) ? section : type.name,
            size: size
        };
    }

    _parseExtras(section) {
        let extras = [];
        section = section
            .replace(EXTRAS_REGEX, part => {
                extras.push(part);
                return '';
            })
            .trim();

        return { section, extras };
    }

    _parseDateAndRegion(section) {
        // Definitely not formatted correctly
        if (section.length < 3) {
            throw new Error('Incorrect formatting in date and region section');
        }

        // Parse the region as the last 2 characters
        let region = section.slice(-2).toUpperCase();

        // Attempt to parse the date from the remains
        let opens = moment.utc(section.slice(0, -2), 'MMM DD HH:mm', 'en');

        return { region, opens };
    }

    _parseScenarios(section) {
        return section.split(',').map(mode => mode.trim());
    }

    _getAddress(content) {
        // Reset regex in case of exact copy of previous post
        IP_REGEX.exec('');
        let ipcheck = IP_REGEX.exec(content);

        // Check IP before falling back to domain
        if (!_.isNull(ipcheck)) {
            let address = ipcheck[1];

            // Only show port if it's not the default port
            if (!_.isEmpty(ipcheck[2]) && ipcheck[2] !== ':25565') {
                address += ipcheck[2];
            }

            return address;
        }

        DOMAIN_REGEX.exec('');
        let domainCheck = DOMAIN_REGEX.exec(content);

        if (!_.isNull(domainCheck)) {
            return domainCheck[2];
        }
    }

    /**
     * @param element the raw post element from the JSON api
     */
    parse(element) {
        // Add basic stuff from the post itself
        // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
        let post = {
            id: element.id,
            title: element.title,
            selftext: he.decode(element.selftext),
            author: element.author,
            permalink: `https://reddit.com${element.permalink}`,
            posted: moment(element.created_utc, 'X'),
            anchorlink: `#${this.$location.path()}?post=${element.id}`,
            isCommunityGame: element.link_flair_text && element.link_flair_text === 'Community Game',
            subreddit: element.subreddit,
            valid: false
        };
        // jscs:enable requireCamelCaseOrUpperCaseIdentifiers

        // Grab all of the information from the title
        try {
            let parsed = this._parseTitle(element.title);

            // Append the extras to the title
            if (parsed.extras.length > 0) {
                parsed.title = parsed.title + ' ' + parsed.extras.join(' ');
            }

            // Fix html encoded entities breaking in post titles
            parsed.title = he.decode(parsed.title);

            delete parsed.extras;
            _.merge(post, parsed);
        } catch (err) {
            this.$log.warn('Unable to parse post', element.title, post.permalink, err);
            return post;
        }

        post.address = this._getAddress(element.selftext);

        if (_.isNull(post.opens) || !post.opens.isValid()) {
            return post;
        }

        let current = this.DateTime.getTime();
        let monthsAgo = post.opens.diff(current, 'months');

        // If it's more than 6 months old, assume it's in the next year
        if (monthsAgo < -6) {
            post.opens.add(1, 'years');
        }
        // If it's more than 6 months in the future assume it's in the last year
        else if (monthsAgo > 6) {
            post.opens.subtract(1, 'years');
        }

        post.valid = true;
        return post;
    }
}
MatchPostParser.$inject = ['DateTime', '$location', '$log'];

export default MatchPostParser;
<commit_msg>feat(MatchPostParser): treat (x) the same as [x] for extras<commit_after>import {parseGameType} from 'app/services/GameTypes';
import moment from 'moment-timezone';
import he from 'he';
import _ from 'lodash';

const IP_REGEX = /(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})(:\d{1,5})?/g;
const DOMAIN_REGEX = /[^\w](IP|Address).*?([a-z\d]([a-z\d\-]{0,61}[a-z\d])?(\.[a-z\d]([a-z\d\-]{0,61}[a-z\d])?)+(:\d{1,5})?)/gi; // jscs:ignore maximumLineLength
const SIZE_REGEX = /To([\dX]+)/i;
const EXTRAS_REGEX = /([\[\(].*?[\]\)])/g;

class MatchPostParser {
    constructor(DateTime, $location, $log) {
        this.DateTime = DateTime;
        this.$location = $location;
        this.$log = $log;
    }

    _parseTitle(title) {
        let details = {
            title,
            extras: []
        };

        let pipeIndex = title.indexOf('|');
        details.isStartTime = pipeIndex !== -1;

        // Replace the | with a -
        if (details.isStartTime) {
            title = title.substr(0, pipeIndex) + '-' + title.substr(pipeIndex + 1);
        }

        let sections = title
            .split('-')
            .map(part => part.trim());

        // Post isnt formatted correctly, don't display it at all
        if (sections.length < 3) {
            throw new Error('Not enough parts in post title');
        }

        let [dateRegionSection, titleSection, teamStyleSection, scenariosSection] = sections;

        // Grab the opening time and region from the first section
        _.merge(details, this._parseDateAndRegion(dateRegionSection));

        // Copy the title across without the other sections
        details.title = titleSection;

        // Assume vanilla if no gamemodes section
        if (_.isUndefined(scenariosSection)) {
            details.gamemodes = ['Vanilla'];

            // Cut out the extras from team types
            let extrasInfo = this._parseExtras(teamStyleSection);
            details.extras = extrasInfo.extras;
            teamStyleSection = extrasInfo.section;
        } else {
            // Add gamemodes
            details.gamemodes = this._parseScenarios(scenariosSection);

            // Cut out the extras from the final scenario

            if (details.gamemodes.length > 0) {
                let finalIndex = details.gamemodes.length - 1;

                let extrasInfo = this._parseExtras(details.gamemodes[finalIndex]);
                details.extras = extrasInfo.extras;
                details.gamemodes[finalIndex] = extrasInfo.section;
            }
        }

        let teamInfo = this._parseTeamStyle(teamStyleSection);
        details.teams = teamInfo.type;
        details.teamSize = teamInfo.size;

        return details;
    }

    _parseTeamStyle(section) {
        section = section.trim();

        SIZE_REGEX.exec('');
        let sizeCheck = SIZE_REGEX.exec(section);

        let size = 0;
        let style = section;

        // Check for a teamsize if we can
        if (!_.isNull(sizeCheck)) {
            size = sizeCheck[1];
            style = section.replace(SIZE_REGEX, '').trim();
        }

        // Attempt to get the game type
        let type = parseGameType(style);

        return {
            type: _.isUndefined(type) ? section : type.name,
            size: size
        };
    }

    _parseExtras(section) {
        let extras = [];
        section = section
            .replace(EXTRAS_REGEX, part => {
                extras.push(part);
                return '';
            })
            .trim();

        return { section, extras };
    }

    _parseDateAndRegion(section) {
        // Definitely not formatted correctly
        if (section.length < 3) {
            throw new Error('Incorrect formatting in date and region section');
        }

        // Parse the region as the last 2 characters
        let region = section.slice(-2).toUpperCase();

        // Attempt to parse the date from the remains
        let opens = moment.utc(section.slice(0, -2), 'MMM DD HH:mm', 'en');

        return { region, opens };
    }

    _parseScenarios(section) {
        return section.split(',').map(mode => mode.trim());
    }

    _getAddress(content) {
        // Reset regex in case of exact copy of previous post
        IP_REGEX.exec('');
        let ipcheck = IP_REGEX.exec(content);

        // Check IP before falling back to domain
        if (!_.isNull(ipcheck)) {
            let address = ipcheck[1];

            // Only show port if it's not the default port
            if (!_.isEmpty(ipcheck[2]) && ipcheck[2] !== ':25565') {
                address += ipcheck[2];
            }

            return address;
        }

        DOMAIN_REGEX.exec('');
        let domainCheck = DOMAIN_REGEX.exec(content);

        if (!_.isNull(domainCheck)) {
            return domainCheck[2];
        }
    }

    /**
     * @param element the raw post element from the JSON api
     */
    parse(element) {
        // Add basic stuff from the post itself
        // jscs:disable requireCamelCaseOrUpperCaseIdentifiers
        let post = {
            id: element.id,
            title: element.title,
            selftext: he.decode(element.selftext),
            author: element.author,
            permalink: `https://reddit.com${element.permalink}`,
            posted: moment(element.created_utc, 'X'),
            anchorlink: `#${this.$location.path()}?post=${element.id}`,
            isCommunityGame: element.link_flair_text && element.link_flair_text === 'Community Game',
            subreddit: element.subreddit,
            valid: false
        };
        // jscs:enable requireCamelCaseOrUpperCaseIdentifiers

        // Grab all of the information from the title
        try {
            let parsed = this._parseTitle(element.title);

            // Append the extras to the title
            if (parsed.extras.length > 0) {
                parsed.title = parsed.title + ' ' + parsed.extras.join(' ');
            }

            // Fix html encoded entities breaking in post titles
            parsed.title = he.decode(parsed.title);

            delete parsed.extras;
            _.merge(post, parsed);
        } catch (err) {
            this.$log.warn('Unable to parse post', element.title, post.permalink, err);
            return post;
        }

        post.address = this._getAddress(element.selftext);

        if (_.isNull(post.opens) || !post.opens.isValid()) {
            return post;
        }

        let current = this.DateTime.getTime();
        let monthsAgo = post.opens.diff(current, 'months');

        // If it's more than 6 months old, assume it's in the next year
        if (monthsAgo < -6) {
            post.opens.add(1, 'years');
        }
        // If it's more than 6 months in the future assume it's in the last year
        else if (monthsAgo > 6) {
            post.opens.subtract(1, 'years');
        }

        post.valid = true;
        return post;
    }
}
MatchPostParser.$inject = ['DateTime', '$location', '$log'];

export default MatchPostParser;
<|endoftext|>


<commit_before>
function TradeifyWidget(offers_selector, current_tags_selector) {
    var offers;
    var offers_render_fn;
    var current_tags;
    var offers_uri;

    var map;

    /*
    google.maps.Map.prototype.markers = new Array();

    google.maps.Map.prototype.getMarkers = function() {
        return this.markers
    };

    google.maps.Map.prototype.clearMarkers = function() {
        for (var i = 0; i < this.markers.length; i++) {
            this.markers[i].setMap(null);
        }
        this.markers = new Array();
    };

    google.maps.Marker.prototype._setMap = google.maps.Marker.prototype.setMap;

    google.maps.Marker.prototype.setMap = function(map) {
        if (map) {
            map.markers[map.markers.length] = this;
        }
        this._setMap(map);
    };
    */
    var offers_directives = {
        'div.offer': {
            'offer <- messages': {
                'a.username@href': 'offer.user.more_info_url',
                'a.avatar@href': 'offer.user.more_info_url',
                'a.username': 'offer.user.screen_name',
                '.avatar img@src': 'offer.user.profile_pic_url',
                '.msg .text': 'offer.offer_text',
                'span.tags': {
                    'tag <- offer.tags': {
                        'a': 'tag.tag',
                        '+a@class': 'tag.type'
                    }
                },
                '.when': 'offer.date'
            }
        }
    };
    var map_directives = {
        'div.map_offer': {
        'map_offer <- messages': {
        'a.map_username@href': 'map_offer.user.more_info_url',
        'a.map_username': 'map_offer.user.screen_name',
        '.map_avatar img@src': 'map_offer.user.profile_pic_url',
        '.map_msg .map_text': 'map_offer.offer_text',
        'span.map_tags': {
        'map_tag <- map_offer.tags': {
        'a': 'map_tag.tag',
        '+a@class': 'map_tag.type'
                    }
                },
                '.map_when': 'map_offer.date'
            }
        }
    };
    function updateMap() {
        var myLatlng = new google.maps.LatLng(-25.363882, 131.044922);
        var myOptions = {
            zoom: 2,
            center: myLatlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        }
        map = new google.maps.Map(document.getElementById("offer_map"), myOptions);
        //map.clearMarkers();
        var latlng = new Array();
        $.each(offers, function() {
            var post = new google.maps.LatLng(this.offer_latitude, this.offer_longitude);
            latlng.push(post);
            var title = this.offer_address; //this.offer_text + " " + this.user.more_info_url;
            var tags = new Array();
            $.each(this.tags,function(){
                if(this.type=="loc")tags.push(this);
            });
            var marker = new google.maps.Marker({
                clickable: true,
                title: title,
                position: post,
                map: map,
                tags: tags
            });

            google.maps.event.addListener(marker, "click", function() {
                createPopup(map, marker);
            });
        });
        var latlngbounds = new google.maps.LatLngBounds();
        for (var i = 0; i < latlng.length; i++) {
            // latlngbounds.extend(latlng[i]);
            $.extend(latlngbounds, latlngbounds, latlng[i]);
        }
        map.setCenter(latlngbounds.getCenter(), map.getBoundsZoomLevel(latlngbounds));
    }
    var map_popup = $("#map_popup" + ' .map_template').compile(map_directives);
    $("#map_popup" + ' .map_template').hide();
    function createPopup(map, marker) {
        var tags_backup = current_tags.tags.concat(marker.tags);
        /*
        current_tags.tags.slice();
        $.each(marker.tags,function{
        var tag = new Tag();
        tag.type = "loc";
        tag.text = marker.title;
        tags_backup.push(tag);
        });
        */
        var json_url = build_search_query(offers_uri, tags_backup);
        $.getJSON(json_url, function(raw_data) {
            $("#map_popup" + ' .map_template').render(raw_data, map_popup);

            //$("#map_offer_template").quickPager({ pageSize: 2},"#pager");
            var infowindow = new google.maps.InfoWindow(
            {
                content: $("#map_popup").html()
            });
            $("#map_popup" + ' .map_template').hide();
            infowindow.open(map, marker);
        });
    }
    var init = function() {
    $("#results").tabs({
        //event: 'mouseover'
        fx: { height: 'toggle', opacity: 'toggle' },
        show: function(event, ui) {
            if (ui.panel.id == "results-2") {
                $(ui.panel).css("height", "100%");
                updateMap();
                // map.checkResize();

            }
        }
    });
        offers_uri = container.offers_uri;

        //compile to a function as soon as possible (ie in 'constructor')
        offers_render_fn = $(offers_selector + ' .template').compile(offers_directives);

        current_tags = new Tags(current_tags_selector);
        current_tags.tag_click(function() {
            remove_filter($(this).text());
            return false;
        });
        //update_offers();
        }
    function offers_(onCompletion){
       var json_url = build_search_query(offers_uri);
       $.getJSON(json_url, function(data) {
           $(offers_selector + ' .template').render(data, offers_render_fn);

           $(offers_selector + ' .tags a').click(function() {
               //add a filter when tags under a message are clicked
               add_filter($(this).text(), $(this).css());
           });
           onCompletion(data);
       });
    }
    var update_offers = function() {
    offers_(function(data) {
            offers = data.messages;
            updateMap();
            $("#offer_template").quickPager({ pageSize: 4 }, "#pager");
        });
        //$("#results").tabs();
    };

    var add_filter = function(tag_text, tag_type) {
        current_tags.add_tag(tag_text, tag_type);
        update_offers();
    };

    var remove_filter = function(tag_text) {
        current_tags.remove_tag(tag_text);
        update_offers();
    };

    var add_fixed_filter = function(tag_text, tag_type) {
        current_tags.add_fixed_tag(tag_text, tag_type, false);
        update_offers();
    };

    var build_search_query = function(baseUrl) {
    var query = "";
    var current_tags_array = (arguments.length > 1) ? arguments[1] : current_tags.tags;
    for (tag in current_tags_array) {
            if (query != "")
                query = query + "&";
            query = query + "tag=" + current_tags_array[tag].tag;
        }
        return baseUrl + "?" + query + "&jsoncallback=?";
    };

    init();

    /* 'public' methods */
    this.update_offers = update_offers;
    this.add_filter = add_filter;
    this.add_fixed_filter = add_fixed_filter;
    this.get_fixed_tags = function(){
        return current_tags.get_fixed_tags();
    }
    this.get_offers = function() {
        return offers;
    }
}
<commit_msg>tryed to centre map zoom on markers 6<commit_after>
function TradeifyWidget(offers_selector, current_tags_selector) {
    var offers;
    var offers_render_fn;
    var current_tags;
    var offers_uri;

    var map;

    /*
    google.maps.Map.prototype.markers = new Array();

    google.maps.Map.prototype.getMarkers = function() {
        return this.markers
    };

    google.maps.Map.prototype.clearMarkers = function() {
        for (var i = 0; i < this.markers.length; i++) {
            this.markers[i].setMap(null);
        }
        this.markers = new Array();
    };

    google.maps.Marker.prototype._setMap = google.maps.Marker.prototype.setMap;

    google.maps.Marker.prototype.setMap = function(map) {
        if (map) {
            map.markers[map.markers.length] = this;
        }
        this._setMap(map);
    };
    */
    var offers_directives = {
        'div.offer': {
            'offer <- messages': {
                'a.username@href': 'offer.user.more_info_url',
                'a.avatar@href': 'offer.user.more_info_url',
                'a.username': 'offer.user.screen_name',
                '.avatar img@src': 'offer.user.profile_pic_url',
                '.msg .text': 'offer.offer_text',
                'span.tags': {
                    'tag <- offer.tags': {
                        'a': 'tag.tag',
                        '+a@class': 'tag.type'
                    }
                },
                '.when': 'offer.date'
            }
        }
    };
    var map_directives = {
        'div.map_offer': {
        'map_offer <- messages': {
        'a.map_username@href': 'map_offer.user.more_info_url',
        'a.map_username': 'map_offer.user.screen_name',
        '.map_avatar img@src': 'map_offer.user.profile_pic_url',
        '.map_msg .map_text': 'map_offer.offer_text',
        'span.map_tags': {
        'map_tag <- map_offer.tags': {
        'a': 'map_tag.tag',
        '+a@class': 'map_tag.type'
                    }
                },
                '.map_when': 'map_offer.date'
            }
        }
    };
    function updateMap() {
        var myLatlng = new google.maps.LatLng(-25.363882, 131.044922);
        var myOptions = {
            zoom: 2,
            center: myLatlng,
            mapTypeId: google.maps.MapTypeId.ROADMAP
        }
        map = new google.maps.Map(document.getElementById("offer_map"), myOptions);
        //map.clearMarkers();
        var latlng = new Array();
        $.each(offers, function() {
            var post = new google.maps.LatLng(this.offer_latitude, this.offer_longitude);
            latlng.push(post);
            var title = this.offer_address; //this.offer_text + " " + this.user.more_info_url;
            var tags = new Array();
            $.each(this.tags,function(){
                if(this.type=="loc")tags.push(this);
            });
            var marker = new google.maps.Marker({
                clickable: true,
                title: title,
                position: post,
                map: map,
                tags: tags
            });

            google.maps.event.addListener(marker, "click", function() {
                createPopup(map, marker);
            });
        });
        var latlngbounds = new google.maps.LatLngBounds();
        for (var i = 0; i < latlng.length; i++) {
            // latlngbounds.extend(latlng[i]);
            $.extend(latlngbounds, latlngbounds, latlng[i]);
        }
        map.setCenter(latlngbounds);
    }
    var map_popup = $("#map_popup" + ' .map_template').compile(map_directives);
    $("#map_popup" + ' .map_template').hide();
    function createPopup(map, marker) {
        var tags_backup = current_tags.tags.concat(marker.tags);
        /*
        current_tags.tags.slice();
        $.each(marker.tags,function{
        var tag = new Tag();
        tag.type = "loc";
        tag.text = marker.title;
        tags_backup.push(tag);
        });
        */
        var json_url = build_search_query(offers_uri, tags_backup);
        $.getJSON(json_url, function(raw_data) {
            $("#map_popup" + ' .map_template').render(raw_data, map_popup);

            //$("#map_offer_template").quickPager({ pageSize: 2},"#pager");
            var infowindow = new google.maps.InfoWindow(
            {
                content: $("#map_popup").html()
            });
            $("#map_popup" + ' .map_template').hide();
            infowindow.open(map, marker);
        });
    }
    var init = function() {
    $("#results").tabs({
        //event: 'mouseover'
        fx: { height: 'toggle', opacity: 'toggle' },
        show: function(event, ui) {
            if (ui.panel.id == "results-2") {
                $(ui.panel).css("height", "100%");
                updateMap();
                // map.checkResize();

            }
        }
    });
        offers_uri = container.offers_uri;

        //compile to a function as soon as possible (ie in 'constructor')
        offers_render_fn = $(offers_selector + ' .template').compile(offers_directives);

        current_tags = new Tags(current_tags_selector);
        current_tags.tag_click(function() {
            remove_filter($(this).text());
            return false;
        });
        //update_offers();
        }
    function offers_(onCompletion){
       var json_url = build_search_query(offers_uri);
       $.getJSON(json_url, function(data) {
           $(offers_selector + ' .template').render(data, offers_render_fn);

           $(offers_selector + ' .tags a').click(function() {
               //add a filter when tags under a message are clicked
               add_filter($(this).text(), $(this).css());
           });
           onCompletion(data);
       });
    }
    var update_offers = function() {
    offers_(function(data) {
            offers = data.messages;
            updateMap();
            $("#offer_template").quickPager({ pageSize: 4 }, "#pager");
        });
        //$("#results").tabs();
    };

    var add_filter = function(tag_text, tag_type) {
        current_tags.add_tag(tag_text, tag_type);
        update_offers();
    };

    var remove_filter = function(tag_text) {
        current_tags.remove_tag(tag_text);
        update_offers();
    };

    var add_fixed_filter = function(tag_text, tag_type) {
        current_tags.add_fixed_tag(tag_text, tag_type, false);
        update_offers();
    };

    var build_search_query = function(baseUrl) {
    var query = "";
    var current_tags_array = (arguments.length > 1) ? arguments[1] : current_tags.tags;
    for (tag in current_tags_array) {
            if (query != "")
                query = query + "&";
            query = query + "tag=" + current_tags_array[tag].tag;
        }
        return baseUrl + "?" + query + "&jsoncallback=?";
    };

    init();

    /* 'public' methods */
    this.update_offers = update_offers;
    this.add_filter = add_filter;
    this.add_fixed_filter = add_fixed_filter;
    this.get_fixed_tags = function(){
        return current_tags.get_fixed_tags();
    }
    this.get_offers = function() {
        return offers;
    }
}
<|endoftext|>


<commit_before>HopObject.inject({
	getOrCreate: function(name, prototype) {
		var obj = this.get(name);
		if (!obj) {
			obj = new prototype();
			obj.name = name;
			// Make visible and set position if they are defined.
			if (obj.visible !== undefined)
				obj.visible = true;
			if (obj.position !== undefined)
				obj.position = this.count();
			this.add(obj);
		}
		return obj;
	},

	/**
	 * Renders a link to the HopObject.
	 * Param can either be a string, or a hash containing these values:
	 * - content: the content of the link
	 * - href: the href to be overriden
	 * - popup: to be passed to renderLink as popup parameter
	 * - default: all other values are rendered as link attributes
	 * If no content is defined, getDisplayName is called.
	 */
	renderLink: function(param, out) {
		var content, href;
		if (param) {
			if (typeof param == 'string') {
				content = param;
				param = null;
			} else {
				content = param.content;
				href = param.href;
			}
		}
		return renderLink(
			content || encode(this.getDisplayName()),
			href || this.href(), param, out);
	},

	/**
	 * getDisplayName can be used to define the way a object's name should be
	 * on the page displayed.
	 */
	getDisplayName: function() {
		return this.name;
	},

	/**
	 * Checks if the  object and all its parents have the visible property set.
	 * Returns true if that applies, false otherwise.
	 */
	isVisible: function() {
		var obj = this;
		while (obj && obj != root && obj.visible)
			obj = obj.getEditParent();
		return obj == root;
	}
});
<commit_msg>Make hidden resources and other sub elements visible while editing an object, by looking into the corresponding 'all' collections as well.<commit_after>HopObject.inject({
	getOrCreate: function(name, prototype) {
		var obj = this.get(name);
		if (!obj) {
			obj = new prototype();
			obj.name = name;
			// Make visible and set position if they are defined.
			if (obj.visible !== undefined)
				obj.visible = true;
			if (obj.position !== undefined)
				obj.position = this.count();
			this.add(obj);
		}
		return obj;
	},

	/**
	 * Define getChildElement for HopObject other than Nodes. This is used in
	 * collections of Nodes, such as resources, where hidden elements needs to
	 * be returned during editing too. It does so by checking wether the object
	 * has a corresponding 'all' collection and fetches from there.
	 * Node defines it's own getChildElement that does not call this one here,
	 * so all is fine.
	 * User overriding getChildElement anyhwere should make sure to use bootstrap
	 * and call this.base() within it.
	 */
	getChildElement: function(name) {
		var obj = this.get(name);
		if (!obj) {
			var parent = this._parent;
			if (parent instanceof Node && User.canEdit(parent)) {
				// For collection HopObjects, this._id points to the collection's
				// name. 'all' + this._id.capitalize() therefore should return the
				// corresponding 'all' collection. Try to fetch from there:
				var all = this._parent['all' + this._id.capitalize()];
				if (all)
					obj = all.get(name);
			}
		}
		return obj;
	},

	/**
	 * Renders a link to the HopObject.
	 * Param can either be a string, or a hash containing these values:
	 * - content: the content of the link
	 * - href: the href to be overriden
	 * - popup: to be passed to renderLink as popup parameter
	 * - default: all other values are rendered as link attributes
	 * If no content is defined, getDisplayName is called.
	 */
	renderLink: function(param, out) {
		var content, href;
		if (param) {
			if (typeof param == 'string') {
				content = param;
				param = null;
			} else {
				content = param.content;
				href = param.href;
			}
		}
		return renderLink(
			content || encode(this.getDisplayName()),
			href || this.href(), param, out);
	},

	/**
	 * getDisplayName can be used to define the way a object's name should be
	 * on the page displayed.
	 */
	getDisplayName: function() {
		return this.name;
	},

	/**
	 * Checks if the  object and all its parents have the visible property set.
	 * Returns true if that applies, false otherwise.
	 */
	isVisible: function() {
		var obj = this;
		while (obj && obj != root && obj.visible)
			obj = obj.getEditParent();
		return obj == root;
	}
});
<|endoftext|>