<commit_before>import { Juko8, Coryn, Talby, Anomoly, AttilioLH, Hewhosmites } from 'CONTRIBUTORS';

import React from 'react';
import ITEMS from 'common/ITEMS';
import SPELLS from 'common/SPELLS';
import ItemLink from 'common/ItemLink';
import SpellLink from 'common/SpellLink';

export default [
  {
    date: new Date('2018-06-16'),
    changes: <React.Fragment>Updated for 8.0 Battle for Azeroth prepatch. All artifact traits and related analysis removed. Bad <SpellLink id={SPELLS.BLACKOUT_KICK.id} icon /> casts statistic and suggestions has been replaced with statistic and suggestions on <SpellLink id={SPELLS.BLACKOUT_KICK.id} icon />'s new cooldown reductions mechanic </React.Fragment>,
  },
  {
    date: new Date('2018-05-29'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.TOUCH_OF_DEATH.id} /> module showing damage from the 10% duplication and its benefit from % damage taken buffs on targets</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-05-10'),
    changes: <React.Fragment>Added highlighting Combo Breaker casts of <SpellLink id={SPELLS.BLACKOUT_KICK.id} /> in Spellcast Timeline</React.Fragment>,
    contributors: [Coryn],
  },
  {
    date: new Date('2018-03-15'),
    changes: <React.Fragment>Added module tracking bad casts of <SpellLink id={SPELLS.BLACKOUT_KICK.id} /> </React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-09'),
    changes: `Added Checklist`,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-06'),
    changes: <React.Fragment>Added Statistics and Suggestions on use of <SpellLink id={SPELLS.TOUCH_OF_KARMA_CAST.id} icon /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-03'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.THE_WIND_BLOWS.id} icon /></React.Fragment>,
    contributors: [Hewhosmites],
  },
  {
    date: new Date('2018-02-16'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.SPINNING_CRANE_KICK.id} /> statistics and suggestion</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-27'),
    changes: <React.Fragment>Added item breakdown for <ItemLink id={ITEMS.DRINKING_HORN_COVER.id} /> showing average time gained for each <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> or <SpellLink id={SPELLS.SERENITY_TALENT.id} /> cast</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-24'),
    changes: <React.Fragment>Added Statistic showing actual casts vs expected casts of important spells during <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> or <SpellLink id={SPELLS.SERENITY_TALENT.id} /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-20'),
    changes: <React.Fragment>Updated Cooldown Tracker to include extended <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> duration from <ItemLink id={ITEMS.DRINKING_HORN_COVER.id} /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-03'),
    changes: "Updated AlwaysBeCasting with channeling and more accurate GCD and fixed Gol'ganneths ravaging storm being shown in Cooldowns tab",
    contributors: [Juko8],
  },
  {
    date: new Date('2017-12-05'),
    changes: <React.Fragment>Updated Cast Efficiency to better handle <SpellLink id={SPELLS.SERENITY_TALENT.id} /> cooldown reduction on <SpellLink id={SPELLS.FISTS_OF_FURY_CAST.id} /> and Strike of the Windlord /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-23'),
    changes: 'Added Chi breakdown tab, suggestions and chi wasted statistic',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-22'),
    changes: 'Added T21 4pc item breakdown',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-07'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.CRACKLING_JADE_LIGHTNING.id} /> damage from <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> clones in <ItemLink id={ITEMS.THE_EMPERORS_CAPACITOR.id} /> item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-06'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.ENERGIZING_ELIXIR_TALENT.id} /> energy gained calculation and statistic</React.Fragment>,
    contributors: [Coryn],
  },
  {
    date: new Date('2017-11-05'),
    changes: <React.Fragment>Added basic display for <SpellLink id={SPELLS.INVOKE_XUEN_THE_WHITE_TIGER_TALENT.id} /> to Cooldown Throughput with plans for refinement</React.Fragment>,
    contributors: [Talby],
  },
  {
    date: new Date('2017-11-05'),
    changes: <React.Fragment>Updated Cast Efficiency - Added <SpellLink id={SPELLS.ENERGIZING_ELIXIR_TALENT.id} /> and fixed <SpellLink id={SPELLS.RUSHING_JADE_WIND_TALENT.id} /> showing when not talented</React.Fragment>,
    contributors: [Talby],
  },
  {
    date: new Date('2017-10-25'),
    changes: 'Updated Cast Efficiency - Changed reduction values from T19 2pc and added a few talents',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-23'),
    changes: <React.Fragment>Added tracking of <SpellLink id={SPELLS.FISTS_OF_FURY_CAST.id} /> ticks</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-17'),
    changes: <React.Fragment>Updated Cast Efficiency - Added reductions from <ItemLink id={ITEMS.THE_WIND_BLOWS.id} /> and T19 2pc</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-17'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.CENEDRIL_REFLECTOR_OF_HATRED.id} />, <ItemLink id={ITEMS.THE_EMPERORS_CAPACITOR.id} /> and <ItemLink id={ITEMS.SOUL_OF_THE_GRANDMASTER.id} /> to legendary item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-15'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.KATSUOS_ECLIPSE.id} /> chi reduction item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-15'),
    changes: 'Finished AlwaysBeCasting',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-14'),
    changes: 'Updated Cast Efficiency - All primary Windwalker spells are now shown',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-13'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.COMBO_BREAKER_BUFF.id} /> tracking with suggestions and statistic</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-03'),
    changes: 'Added in mastery tracking and suggestions, along with updating the Maintainer and Configuration sections',
    contributors: [Anomoly],
  },
  {
    date: new Date('2017-09-24'),
    changes: <React.Fragment>Added additional Windwalker spells / cooldowns along with a simple <SpellLink id={SPELLS.HIT_COMBO_TALENT.id} /> tracker</React.Fragment>,
    contributors: [Anomoly],
  },
  {
    date: new Date('2017-07-22'),
    changes: 'Added basic Windwalker Monk support',
    contributors: [AttilioLH],
  },
];
<commit_msg>Fixed missing contributor on latest CHANGELOG entry<commit_after>import { Juko8, Coryn, Talby, Anomoly, AttilioLH, Hewhosmites } from 'CONTRIBUTORS';

import React from 'react';
import ITEMS from 'common/ITEMS';
import SPELLS from 'common/SPELLS';
import ItemLink from 'common/ItemLink';
import SpellLink from 'common/SpellLink';

export default [
  {
    date: new Date('2018-06-16'),
    changes: <React.Fragment>Updated for 8.0 Battle for Azeroth prepatch. All artifact traits and related analysis removed. Bad <SpellLink id={SPELLS.BLACKOUT_KICK.id} icon /> casts statistic and suggestions has been replaced with statistic and suggestions on <SpellLink id={SPELLS.BLACKOUT_KICK.id} icon />'s new cooldown reductions mechanic </React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-05-29'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.TOUCH_OF_DEATH.id} /> module showing damage from the 10% duplication and its benefit from % damage taken buffs on targets</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-05-10'),
    changes: <React.Fragment>Added highlighting Combo Breaker casts of <SpellLink id={SPELLS.BLACKOUT_KICK.id} /> in Spellcast Timeline</React.Fragment>,
    contributors: [Coryn],
  },
  {
    date: new Date('2018-03-15'),
    changes: <React.Fragment>Added module tracking bad casts of <SpellLink id={SPELLS.BLACKOUT_KICK.id} /> </React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-09'),
    changes: 'Added Checklist',
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-06'),
    changes: <React.Fragment>Added Statistics and Suggestions on use of <SpellLink id={SPELLS.TOUCH_OF_KARMA_CAST.id} icon /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-03-03'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.THE_WIND_BLOWS.id} icon /></React.Fragment>,
    contributors: [Hewhosmites],
  },
  {
    date: new Date('2018-02-16'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.SPINNING_CRANE_KICK.id} /> statistics and suggestion</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-27'),
    changes: <React.Fragment>Added item breakdown for <ItemLink id={ITEMS.DRINKING_HORN_COVER.id} /> showing average time gained for each <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> or <SpellLink id={SPELLS.SERENITY_TALENT.id} /> cast</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-24'),
    changes: <React.Fragment>Added Statistic showing actual casts vs expected casts of important spells during <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> or <SpellLink id={SPELLS.SERENITY_TALENT.id} /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-20'),
    changes: <React.Fragment>Updated Cooldown Tracker to include extended <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> duration from <ItemLink id={ITEMS.DRINKING_HORN_COVER.id} /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2018-01-03'),
    changes: <React.Fragment>Updated AlwaysBeCasting with channeling and more accurate GCD and fixed Gol'ganneths ravaging storm being shown in Cooldowns tab</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-12-05'),
    changes: <React.Fragment>Updated Cast Efficiency to better handle <SpellLink id={SPELLS.SERENITY_TALENT.id} /> cooldown reduction on <SpellLink id={SPELLS.FISTS_OF_FURY_CAST.id} /> and Strike of the Windlord /></React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-23'),
    changes: 'Added Chi breakdown tab, suggestions and chi wasted statistic',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-22'),
    changes: 'Added T21 4pc item breakdown',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-07'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.CRACKLING_JADE_LIGHTNING.id} /> damage from <SpellLink id={SPELLS.STORM_EARTH_AND_FIRE_CAST.id} /> clones in <ItemLink id={ITEMS.THE_EMPERORS_CAPACITOR.id} /> item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-11-06'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.ENERGIZING_ELIXIR_TALENT.id} /> energy gained calculation and statistic</React.Fragment>,
    contributors: [Coryn],
  },
  {
    date: new Date('2017-11-05'),
    changes: <React.Fragment>Added basic display for <SpellLink id={SPELLS.INVOKE_XUEN_THE_WHITE_TIGER_TALENT.id} /> to Cooldown Throughput with plans for refinement</React.Fragment>,
    contributors: [Talby],
  },
  {
    date: new Date('2017-11-05'),
    changes: <React.Fragment>Updated Cast Efficiency - Added <SpellLink id={SPELLS.ENERGIZING_ELIXIR_TALENT.id} /> and fixed <SpellLink id={SPELLS.RUSHING_JADE_WIND_TALENT.id} /> showing when not talented</React.Fragment>,
    contributors: [Talby],
  },
  {
    date: new Date('2017-10-25'),
    changes: 'Updated Cast Efficiency - Changed reduction values from T19 2pc and added a few talents',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-23'),
    changes: <React.Fragment>Added tracking of <SpellLink id={SPELLS.FISTS_OF_FURY_CAST.id} /> ticks</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-17'),
    changes: <React.Fragment>Updated Cast Efficiency - Added reductions from <ItemLink id={ITEMS.THE_WIND_BLOWS.id} /> and T19 2pc</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-17'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.CENEDRIL_REFLECTOR_OF_HATRED.id} />, <ItemLink id={ITEMS.THE_EMPERORS_CAPACITOR.id} /> and <ItemLink id={ITEMS.SOUL_OF_THE_GRANDMASTER.id} /> to legendary item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-15'),
    changes: <React.Fragment>Added <ItemLink id={ITEMS.KATSUOS_ECLIPSE.id} /> chi reduction item breakdown</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-15'),
    changes: 'Finished AlwaysBeCasting',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-14'),
    changes: 'Updated Cast Efficiency - All primary Windwalker spells are now shown',
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-13'),
    changes: <React.Fragment>Added <SpellLink id={SPELLS.COMBO_BREAKER_BUFF.id} /> tracking with suggestions and statistic</React.Fragment>,
    contributors: [Juko8],
  },
  {
    date: new Date('2017-10-03'),
    changes: 'Added in mastery tracking and suggestions, along with updating the Maintainer and Configuration sections',
    contributors: [Anomoly],
  },
  {
    date: new Date('2017-09-24'),
    changes: <React.Fragment>Added additional Windwalker spells / cooldowns along with a simple <SpellLink id={SPELLS.HIT_COMBO_TALENT.id} /> tracker</React.Fragment>,
    contributors: [Anomoly],
  },
  {
    date: new Date('2017-07-22'),
    changes: 'Added basic Windwalker Monk support',
    contributors: [AttilioLH],
  },
];
<|endoftext|>


<commit_before>"use strict";
/**
 * @fileoverview SessionDescriptionHandler
 */

 /* SessionDescriptionHandler
  * @class PeerConnection helper Class.
  * @param {SIP.Session} session
  * @param {Object} [options]
  */
module.exports = function (SIP) {

// Constructor
var SessionDescriptionHandler = function(session, options) {
  // TODO: Validate the options
  this.options = options || {};

  this.logger = session.ua.getLogger('sip.invitecontext.sessionDescriptionHandler', session.id);
  this.session = session;

  this.CONTENT_TYPE = 'application/sdp';

  this.modifiers = this.options.modifiers || [];
  if (!Array.isArray(this.modifiers)) {
    this.modifiers = [this.modifiers];
  }

  if (this.options.hackStripTcpCandidates) {
    this.modifiers.push(function(sdp) {
      sdp = sdp.replace(/^a=candidate:\d+ \d+ tcp .*?\r\n/img, "");
      return SIP.Utils.Promise.resolve(sdp);
    });
  }
  if (this.options.hackStripTelephoneEvent) {
    this.modifiers.push(function(sdp) {
      sdp = sdp.replace(/^a=rtpmap:\d+ telephone-event\/d+/img, "");
      return SIP.Utils.Promise.resolve(sdp);
    });
  }

  var environment = global.window || global;
  this.WebRTC = {
    MediaStream           : environment.MediaStream,
    getUserMedia          : environment.navigator.mediaDevices.getUserMedia.bind(environment.navigator.mediaDevices),
    RTCPeerConnection     : environment.RTCPeerConnection,
    RTCSessionDescription : environment.RTCSessionDescription
  };

  this.initPeerConnection(this.options);

  this.constraints = this.checkAndDefaultConstraints(this.options.constraints);
};

/**
 * @param {SIP.Session} session
 * @param {Object} [options]
 */

SessionDescriptionHandler.defaultFactory = function defaultFactory (session, options) {
  return new SessionDescriptionHandler(session, options);
};

SessionDescriptionHandler.prototype = Object.create(SIP.SessionDescriptionHandler.prototype, {
  // Functions the sesssion can use

  /**
   * Destructor
   */
  close: {writable: true, value: function () {
    this.logger.log('closing PeerConnection');
    // have to check signalingState since this.close() gets called multiple times
    if(this.peerConnection && this.peerConnection.signalingState !== 'closed') {
      this.peerConnection.close();
    }
  }},

  /**
   * Gets the local description from the underlying media implementation
   * @param {Object} [options] Options object to be used by getDescription
   * @param {MediaStreamConstraints} [options.constraints] MediaStreamConstraints https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
   * @param {Object} [options.peerConnectionOptions] If this is set it will recreate the peer connection with the new options
   * @param {Array} [modifiers] Array with one time use description modifiers
   * @returns {Promise} Promise that resolves with the local description to be used for the session
   */
  getDescription: {writable: true, value: function (options, modifiers) {
    var self = this;

    options = options || {};
    if (options.peerConnectionOptions) {
      this.initPeerConnection(options.peerConnectionOptions);
    }

    // Merge passed constraints with saved constraints and save
    this.constraints = Object.assign(this.constraints, options.constraints);
    this.constraints = this.checkAndDefaultConstraints(this.constraints);

    modifiers = modifiers || [];
    if (!Array.isArray(modifiers)) {
      modifiers = [modifiers];
    }
    modifiers = modifiers.concat(this.modifiers);

    // Check to see if the peerConnection already has a local description
    if (this.peerConnection.localDescription && this.peerConnection.localDescription.sdp && this.peerConnection.localDescription.sdp !== '') {
      if (this.peerConnection.signalingState === 'stable') {
        // TODO: Events.
        return SIP.Utils.reducePromises(modifiers, this.peerConnection.localDescription.sdp)
        .then(function(sdp) {
          return {
            body: sdp,
            contentType: self.CONTENT_TYPE
          };
        });
      }
      // TODO: Determine if we need to do another GUM
      return this.createOfferOrAnswer();
    }

    // GUM and set myself up
    self.logger.log('acquiring local media');
    return this.acquire(self.constraints)
      .then(function acquireSucceeded(streams) {
        self.logger.log('acquired local media streams');
        return streams;
      }, function acquireFailed(err) {
        self.logger.error('unable to acquire streams');
        self.logger.error(err);
        throw err;
      })
      .then(function addStreams(streams) {
        try {
          streams = [].concat(streams);
          streams.forEach(function (stream) {
            self.peerConnection.addStream(stream);
          }, this);
        } catch(e) {
          self.logger.error('error adding stream');
          self.logger.error(e);
          return SIP.Utils.Promise.reject(e);
        }
        return SIP.Utils.Promise.resolve();
      })
      .then(function streamAdditionSucceeded() {
        return self.createOfferOrAnswer();
      })
      .then(SIP.Utils.reducePromises.bind(null, modifiers))
      .then(function(sdp) {
        return {
          body: sdp,
          contentType: self.CONTENT_TYPE
        };
      });
  }},

  /**
   * Check if the Session Description Handler can handle the Content-Type described by a SIP Message
   * @param {String} contentType The content type that is in the SIP Message
   * @returns {boolean}
   */
  hasDescription: {writable: true, value: function hasDescription (contentType) {
    return contentType === this.CONTENT_TYPE;
  }},

  /**
   * The modifier that should be used when the session would like to place the call on hold
   * @param {String} [sdp] The description that will be modified
   * @returns {Promise} Promise that resolves with modified SDP
   */
  holdModifier: {writable: true, value: function holdModifier (sdp) {
    if (!(/a=(sendrecv|sendonly|recvonly|inactive)/).test(sdp)) {
      sdp = sdp.replace(/(m=[^\r]*\r\n)/g, '$1a=sendonly\r\n');
    } else {
      sdp = sdp.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n');
      sdp = sdp.replace(/a=recvonly\r\n/g, 'a=inactive\r\n');
    }
    return SIP.Utils.Promise.resolve(sdp);
  }},

  /**
   * Set the remote description to the underlying media implementation
   * @param {String} sessionDescription The description provided by a SIP message to be set on the media implementation
   * @param {Object} [options] Options object to be used by getDescription
   * @param {MediaStreamConstraints} [options.constraints] MediaStreamConstraints https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
   * @param {Object} [options.peerConnectionOptions] If this is set it will recreate the peer connection with the new options
   * @param {Array} [modifiers] Array with one time use description modifiers
   * @returns {Promise} Promise that resolves once the description is set
   */
  setDescription: {writable:true, value: function setDescription (sessionDescription, options, modifiers) {
    var self = this;

    if (this.setDescriptionLock) {
      // TODO: What do we want to do if we are locked?
      // TODO: This is weird because we want to block this, but the calling promise chain should only be called once.
      //       We do not want to reject the promise, because the failure case could cause a rejection to be sent, when
      //       really we should be accepting once the original promise completes. This is really a retransmission issue.
      return;
    }
    this.setDescriptionLock = true;
    options = options || {};
    if (options.peerConnectionOptions) {
      this.initPeerConnection(options.peerConnectionOptions);
    }

    // Merge passed constraints with saved constraints and save
    this.constraints = Object.assign(this.constraints, options.constraints);
    this.constraints = this.checkAndDefaultConstraints(this.constraints);

    modifiers = modifiers || [];
    if (!Array.isArray(modifiers)) {
      modifiers = [modifiers];
    }
    modifiers = modifiers.concat(this.modifiers);

    return SIP.Utils.reducePromises(modifiers, sessionDescription)
      .then(function(sdp) {
        var rawDescription = {
          type: self.hasOffer('local') ? 'answer' : 'offer',
          sdp: sdp
        };
        // TODO: This emit does not match what we are doing for getDescription... kind of re: modifier
        self.emit('setDescription', rawDescription);

        return self.peerConnection.setRemoteDescription(new self.WebRTC.RTCSessionDescription(rawDescription));
      })
      .catch(function modifierError(e) {
        self.logger.error("The modifiers did not resolve successfully");
        self.logger.error(e);
        throw e;
      })
      .then(function setRemoteDescriptionSuccess() {
        self.emit('setRemoteDescription', self.peerConnection.getRemoteStreams());
        // TODO: invite w/o sdp case, receiving call case
        self.emit('confirmed', self);
      })
      .catch(function setRemoteDescriptionError(e) {
        self.emit('peerConnection-setRemoteDescriptionFailed', e);
        throw e;
      })
      .finally(function setRemoteDescriptionDone() {
        this.setDescriptionLock = false;
      });
  }},

  // Internal functions
  createOfferOrAnswer: {writable: true, value: function createOfferOrAnswer () {
    var self = this;
    var methodName;
    var pc = self.peerConnection;
    // TODO: Lock?
    // TODO: We need to lock on the setRemoteDescription so that it cannot be done 2x

    methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';

    return SIP.Utils.promisify(pc, methodName, true)()
      .catch(function methodError(e) {
        self.emit('peerConnection-' + methodName + 'Failed', e);
        throw e;
      })
      .then(SIP.Utils.promisify(pc, 'setLocalDescription'))
      .catch(function localDescError(e) {
        self.emit('peerConnection-SetLocalDescriptionFailed', e);
        throw e;
      })
      .then(function onSetLocalDescriptionSuccess() {
        var deferred = SIP.Utils.defer();
        if (pc.iceGatheringState === 'complete' && (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) {
          deferred.resolve();
        } else {
          // TODO: Is this a race condition
          self.onIceCompleted.promise.then(deferred.resolve);
        }
        return  deferred.promise;
      })
      .then(function readySuccess() {
        var sdp = self.peerConnection.localDescription.sdp;
        var sdpWrapper = {
          type: methodName === 'createOffer' ? 'offer' : 'answer',
          sdp: sdp
        };
        // This returns unmodified SDP from getDescription
        self.emit('getDescription', sdpWrapper);
        return sdpWrapper.sdp;
      })
      .catch(function createOfferOrAnserError (e) {
        self.logger.error(e);
        // TODO: Not sure if this is correct
        throw new SIP.Exceptions.GetDescriptionError(e);
      });
  }},

  addDefaultIceServers: {writable: true, value: function addDefaultIceServers (rtcConfiguration) {
    if (!rtcConfiguration.iceServers) {
      rtcConfiguration.iceServers = [{urls: 'stun:stun.l.google.com:19302'}];
    }
    return rtcConfiguration;
  }},

  checkAndDefaultConstraints: {writable: true, value: function checkAndDefaultConstraints (constraints) {
    var defaultConstraints = {audio: true, video: true};
    constraints = constraints || defaultConstraints;
    // Empty object check
    if (Object.keys(constraints).length === 0 && constraints.constructor === Object) {
      return defaultConstraints;
    }
    return constraints;
  }},

  initPeerConnection: {writable: true, value: function initPeerConnection(options) {
    var self = this;
    options.rtcConfiguration = options.rtcConfiguration || {};
    options.rtcConfiguration = this.addDefaultIceServers(options.rtcConfiguration);

    this.onIceCompleted = SIP.Utils.defer();
    this.onIceCompleted.promise.then(function(pc) {
      self.emit('iceGatheringComplete', pc);
      if (self.iceCheckingTimer) {
        SIP.Timers.clearTimeout(self.iceCheckingTimer);
        self.iceCheckingTimer = null;
      }
    });

    if (this.peerConnection) {
      this.peerConnection.close();
    }

    this.peerConnection = new this.WebRTC.RTCPeerConnection(options.rtcConfiguration);

    this.peerConnection.onaddstream = function(e) {
      self.logger.log('stream added: '+ e.stream.id);
      self.emit('addStream', e);
    };

    this.peerConnection.onremovestream = function(e) {
      self.logger.log('stream removed: '+ e.stream.id);
    };

    this.startIceCheckingTimer = function () {
      if (!self.iceCheckingTimer) {
        self.iceCheckingTimer = SIP.Timers.setTimeout(function() {
          self.logger.log('RTCIceChecking Timeout Triggered after ' + options.iceCheckingTimeout + ' milliseconds');
          self.onIceCompleted.resolve(this);
        }.bind(this.peerConnection), options.iceCheckingTimeout);
      }
    };

    this.peerConnection.onicecandidate = function(e) {
      self.emit('iceCandidate', e);
      if (e.candidate) {
        self.logger.log('ICE candidate received: '+ (e.candidate.candidate === null ? null : e.candidate.candidate.trim()));
        self.startIceCheckingTimer();
      } else {
        self.onIceCompleted.resolve(this);
      }
    };

    this.peerConnection.onicegatheringstatechange = function () {
      self.logger.log('RTCIceGatheringState changed: ' + this.iceGatheringState);
      if (this.iceGatheringState === 'gathering') {
        self.emit('iceGathering', this);
      }
      if (this.iceGatheringState === 'complete') {
        self.onIceCompleted.resolve(this);
      }
    };

    this.peerConnection.oniceconnectionstatechange = function() {  //need e for commented out case
      var stateEvent;

      if (this.iceConnectionState === 'checking') {
        self.startIceCheckingTimer();
      }

      switch (this.iceConnectionState) {
      case 'new':
        stateEvent = 'iceConnection';
        break;
      case 'checking':
        stateEvent = 'iceConnectionChecking';
        break;
      case 'connected':
        stateEvent = 'iceConnectionConnected';
        break;
      case 'completed':
        stateEvent = 'iceConnectionCompleted';
        break;
      case 'failed':
        stateEvent = 'iceConnectionFailed';
        break;
      case 'disconnected':
        stateEvent = 'iceConnectionDisconnected';
        break;
      case 'closed':
        stateEvent = 'iceConnectionClosed';
        break;
      default:
        self.logger.warn('Unknown iceConnection state:', this.iceConnectionState);
        return;
      }
      self.emit(stateEvent, this);

      //Bria state changes are always connected -> disconnected -> connected on accept, so session gets terminated
      //normal calls switch from failed to connected in some cases, so checking for failed and terminated
      /*if (this.iceConnectionState === 'failed') {
        self.session.terminate({
        cause: SIP.C.causes.RTP_TIMEOUT,
        status_code: 200,
        reason_phrase: SIP.C.causes.RTP_TIMEOUT
      });
      } else if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') {
      self.onIceCompleted(this);
      }*/
    };
  }},

  acquire: {writable: true, value: function acquire (constraints) {
    // Default audio & video to true
    constraints = this.checkAndDefaultConstraints(constraints);

    return new SIP.Utils.Promise(function(resolve, reject) {
      /*
       * Make the call asynchronous, so that ICCs have a chance
       * to define callbacks to `userMediaRequest`
       */
      this.emit('userMediaRequest', constraints);

      var emitThenCall = function(eventName, callback) {
        var callbackArgs = Array.prototype.slice.call(arguments, 2);
        // Emit with all of the arguments from the real callback.
        var newArgs = [eventName].concat(callbackArgs);
        this.emit.apply(this, newArgs);
        return callback.apply(null, callbackArgs);
      }.bind(this);

      if (constraints.audio || constraints.video) {
        this.WebRTC.getUserMedia(constraints)
        .then(
          emitThenCall.bind(this, 'userMedia', function(streams) { resolve(streams); }),
          emitThenCall.bind(this, 'userMediaFailed', function(e) {
            reject(e);
            throw e;
          })
        );
      } else {
        // Local streams were explicitly excluded.
        resolve([]);
      }
    }.bind(this));
  }},

  hasOffer: {writable: true, value: function hasOffer (where) {
    var offerState = 'have-' + where + '-offer';
    return this.peerConnection.signalingState === offerState;
  }}
});

return SessionDescriptionHandler;
};
<commit_msg>Remove promisify from the SessionDescriptionHandler Make the handler work with Safari WebRTC implementation and Chrome<commit_after>"use strict";
/**
 * @fileoverview SessionDescriptionHandler
 */

 /* SessionDescriptionHandler
  * @class PeerConnection helper Class.
  * @param {SIP.Session} session
  * @param {Object} [options]
  */
module.exports = function (SIP) {

// Constructor
var SessionDescriptionHandler = function(session, options) {
  // TODO: Validate the options
  this.options = options || {};

  this.logger = session.ua.getLogger('sip.invitecontext.sessionDescriptionHandler', session.id);
  this.session = session;

  this.CONTENT_TYPE = 'application/sdp';

  this.modifiers = this.options.modifiers || [];
  if (!Array.isArray(this.modifiers)) {
    this.modifiers = [this.modifiers];
  }

  if (this.options.hackStripTcpCandidates) {
    this.modifiers.push(function(sdp) {
      sdp = sdp.replace(/^a=candidate:\d+ \d+ tcp .*?\r\n/img, "");
      return SIP.Utils.Promise.resolve(sdp);
    });
  }
  if (this.options.hackStripTelephoneEvent) {
    this.modifiers.push(function(sdp) {
      sdp = sdp.replace(/^a=rtpmap:\d+ telephone-event\/d+/img, "");
      return SIP.Utils.Promise.resolve(sdp);
    });
  }

  var environment = global.window || global;
  this.WebRTC = {
    MediaStream           : environment.MediaStream,
    getUserMedia          : environment.navigator.mediaDevices.getUserMedia.bind(environment.navigator.mediaDevices),
    RTCPeerConnection     : environment.RTCPeerConnection,
    RTCSessionDescription : environment.RTCSessionDescription
  };

  this.initPeerConnection(this.options);

  this.constraints = this.checkAndDefaultConstraints(this.options.constraints);
};

/**
 * @param {SIP.Session} session
 * @param {Object} [options]
 */

SessionDescriptionHandler.defaultFactory = function defaultFactory (session, options) {
  return new SessionDescriptionHandler(session, options);
};

SessionDescriptionHandler.prototype = Object.create(SIP.SessionDescriptionHandler.prototype, {
  // Functions the sesssion can use

  /**
   * Destructor
   */
  close: {writable: true, value: function () {
    this.logger.log('closing PeerConnection');
    // have to check signalingState since this.close() gets called multiple times
    if(this.peerConnection && this.peerConnection.signalingState !== 'closed') {
      this.peerConnection.close();
    }
  }},

  /**
   * Gets the local description from the underlying media implementation
   * @param {Object} [options] Options object to be used by getDescription
   * @param {MediaStreamConstraints} [options.constraints] MediaStreamConstraints https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
   * @param {Object} [options.peerConnectionOptions] If this is set it will recreate the peer connection with the new options
   * @param {Array} [modifiers] Array with one time use description modifiers
   * @returns {Promise} Promise that resolves with the local description to be used for the session
   */
  getDescription: {writable: true, value: function (options, modifiers) {
    var self = this;

    options = options || {};
    if (options.peerConnectionOptions) {
      this.initPeerConnection(options.peerConnectionOptions);
    }

    // Merge passed constraints with saved constraints and save
    this.constraints = Object.assign(this.constraints, options.constraints);
    this.constraints = this.checkAndDefaultConstraints(this.constraints);

    modifiers = modifiers || [];
    if (!Array.isArray(modifiers)) {
      modifiers = [modifiers];
    }
    modifiers = modifiers.concat(this.modifiers);

    // Check to see if the peerConnection already has a local description
    if (this.peerConnection.localDescription && this.peerConnection.localDescription.sdp && this.peerConnection.localDescription.sdp !== '') {
      if (this.peerConnection.signalingState === 'stable') {
        // TODO: Events.
        return SIP.Utils.reducePromises(modifiers, this.peerConnection.localDescription.sdp)
        .then(function(sdp) {
          return {
            body: sdp,
            contentType: self.CONTENT_TYPE
          };
        });
      }
      // TODO: Determine if we need to do another GUM
      return this.createOfferOrAnswer();
    }

    // GUM and set myself up
    self.logger.log('acquiring local media');
    return this.acquire(self.constraints)
      .then(function acquireSucceeded(streams) {
        self.logger.log('acquired local media streams');
        return streams;
      }, function acquireFailed(err) {
        self.logger.error('unable to acquire streams');
        self.logger.error(err);
        throw err;
      })
      .then(function addStreams(streams) {
        try {
          streams = [].concat(streams);
          streams.forEach(function (stream) {
            if (self.peerConnection.addTrack) {
              stream.getTracks().forEach(function (track) {
                self.peerConnection.addTrack(track, stream);
              });
            } else {
              // Chrome 59 does not support addTrack
              self.peerConnection.addStream(stream);
            }
          }, this);
        } catch(e) {
          self.logger.error('error adding stream');
          self.logger.error(e);
          return SIP.Utils.Promise.reject(e);
        }
        return SIP.Utils.Promise.resolve();
      })
      .then(function streamAdditionSucceeded() {
        return self.createOfferOrAnswer();
      })
      .then(SIP.Utils.reducePromises.bind(null, modifiers))
      .then(function(sdp) {
        return {
          body: sdp,
          contentType: self.CONTENT_TYPE
        };
      });
  }},

  /**
   * Check if the Session Description Handler can handle the Content-Type described by a SIP Message
   * @param {String} contentType The content type that is in the SIP Message
   * @returns {boolean}
   */
  hasDescription: {writable: true, value: function hasDescription (contentType) {
    return contentType === this.CONTENT_TYPE;
  }},

  /**
   * The modifier that should be used when the session would like to place the call on hold
   * @param {String} [sdp] The description that will be modified
   * @returns {Promise} Promise that resolves with modified SDP
   */
  holdModifier: {writable: true, value: function holdModifier (sdp) {
    if (!(/a=(sendrecv|sendonly|recvonly|inactive)/).test(sdp)) {
      sdp = sdp.replace(/(m=[^\r]*\r\n)/g, '$1a=sendonly\r\n');
    } else {
      sdp = sdp.replace(/a=sendrecv\r\n/g, 'a=sendonly\r\n');
      sdp = sdp.replace(/a=recvonly\r\n/g, 'a=inactive\r\n');
    }
    return SIP.Utils.Promise.resolve(sdp);
  }},

  /**
   * Set the remote description to the underlying media implementation
   * @param {String} sessionDescription The description provided by a SIP message to be set on the media implementation
   * @param {Object} [options] Options object to be used by getDescription
   * @param {MediaStreamConstraints} [options.constraints] MediaStreamConstraints https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints
   * @param {Object} [options.peerConnectionOptions] If this is set it will recreate the peer connection with the new options
   * @param {Array} [modifiers] Array with one time use description modifiers
   * @returns {Promise} Promise that resolves once the description is set
   */
  setDescription: {writable:true, value: function setDescription (sessionDescription, options, modifiers) {
    var self = this;

    if (this.setDescriptionLock) {
      // TODO: What do we want to do if we are locked?
      // TODO: This is weird because we want to block this, but the calling promise chain should only be called once.
      //       We do not want to reject the promise, because the failure case could cause a rejection to be sent, when
      //       really we should be accepting once the original promise completes. This is really a retransmission issue.
      return;
    }
    this.setDescriptionLock = true;
    options = options || {};
    if (options.peerConnectionOptions) {
      this.initPeerConnection(options.peerConnectionOptions);
    }

    // Merge passed constraints with saved constraints and save
    this.constraints = Object.assign(this.constraints, options.constraints);
    this.constraints = this.checkAndDefaultConstraints(this.constraints);

    modifiers = modifiers || [];
    if (!Array.isArray(modifiers)) {
      modifiers = [modifiers];
    }
    modifiers = modifiers.concat(this.modifiers);

    return SIP.Utils.reducePromises(modifiers, sessionDescription)
      .catch(function modifierError(e) {
        self.logger.error("The modifiers did not resolve successfully");
        self.logger.error(e);
        self.setDescriptionLock = false;
        throw e;
      })
      .then(function(sdp) {
        var rawDescription = {
          type: self.hasOffer('local') ? 'answer' : 'offer',
          sdp: sdp
        };
        // TODO: This emit does not match what we are doing for getDescription... kind of re: modifier
        self.emit('setDescription', rawDescription);

        return self.peerConnection.setRemoteDescription(new self.WebRTC.RTCSessionDescription(rawDescription));
      })
      .catch(function setRemoteDescriptionError(e) {
        self.emit('peerConnection-setRemoteDescriptionFailed', e);
        self.setDescriptionLock = false;
        throw e;
      })
      .then(function setRemoteDescriptionSuccess() {
        if (self.peerConnection.getRemoteStreams) {
          self.emit('setRemoteDescription', self.peerConnection.getRemoteStreams());
        } else {
          // TODO: Shim this correctly for Safari
          self.emit('setRemoteDescription', self.peerConnection.getSenders());
        }
        self.emit('confirmed', self);
        self.setDescriptionLock = false;
      });
  }},

  // Internal functions
  createOfferOrAnswer: {writable: true, value: function createOfferOrAnswer () {
    var self = this;
    var methodName;
    var pc = this.peerConnection;
    // TODO: Lock?
    // TODO: We need to lock on the setRemoteDescription so that it cannot be done 2x

    methodName = self.hasOffer('remote') ? 'createAnswer' : 'createOffer';

    return pc[methodName]()
      .catch(function methodError(e) {
        self.emit('peerConnection-' + methodName + 'Failed', e);
        throw e;
      })
      .then(function(sdp) {
        console.log(sdp);
        return pc.setLocalDescription(sdp);
      })
      .catch(function localDescError(e) {
        self.emit('peerConnection-SetLocalDescriptionFailed', e);
        throw e;
      })
      .then(function onSetLocalDescriptionSuccess() {
        var deferred = SIP.Utils.defer();
        if (pc.iceGatheringState === 'complete' && (pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed')) {
          deferred.resolve();
        } else {
          // TODO: Is this a race condition
          self.onIceCompleted.promise.then(deferred.resolve);
        }
        return  deferred.promise;
      })
      .then(function readySuccess() {
        var localDescription = self.peerConnection.localDescription;
        // This returns unmodified SDP from getDescription
        self.emit('getDescription', localDescription);
        return localDescription.sdp;
      })
      .catch(function createOfferOrAnswerError (e) {
        self.logger.error(e);
        // TODO: Not sure if this is correct
        throw new SIP.Exceptions.GetDescriptionError(e);
      });
  }},

  addDefaultIceServers: {writable: true, value: function addDefaultIceServers (rtcConfiguration) {
    if (!rtcConfiguration.iceServers) {
      rtcConfiguration.iceServers = [{urls: 'stun:stun.l.google.com:19302'}];
    }
    return rtcConfiguration;
  }},

  checkAndDefaultConstraints: {writable: true, value: function checkAndDefaultConstraints (constraints) {
    var defaultConstraints = {audio: true, video: true};
    constraints = constraints || defaultConstraints;
    // Empty object check
    if (Object.keys(constraints).length === 0 && constraints.constructor === Object) {
      return defaultConstraints;
    }
    return constraints;
  }},

  initPeerConnection: {writable: true, value: function initPeerConnection(options) {
    var self = this;
    options.rtcConfiguration = options.rtcConfiguration || {};
    options.rtcConfiguration = this.addDefaultIceServers(options.rtcConfiguration);

    this.onIceCompleted = SIP.Utils.defer();
    this.onIceCompleted.promise.then(function(pc) {
      self.emit('iceGatheringComplete', pc);
      if (self.iceCheckingTimer) {
        SIP.Timers.clearTimeout(self.iceCheckingTimer);
        self.iceCheckingTimer = null;
      }
    });

    if (this.peerConnection) {
      this.peerConnection.close();
    }

    this.peerConnection = new this.WebRTC.RTCPeerConnection(options.rtcConfiguration);

    this.peerConnection.ontrack = function(e) {
      self.logger.log('track added ' + e.track.id);
      self.emit('addTrack', e);
    };

    // TODO: There is no remove track listener
    this.peerConnection.onremovestream = function(e) {
      self.logger.log('stream removed: '+ e.stream.id);
    };

    this.startIceCheckingTimer = function () {
      if (!self.iceCheckingTimer) {
        self.iceCheckingTimer = SIP.Timers.setTimeout(function() {
          self.logger.log('RTCIceChecking Timeout Triggered after ' + options.iceCheckingTimeout + ' milliseconds');
          self.onIceCompleted.resolve(this);
        }.bind(this.peerConnection), options.iceCheckingTimeout);
      }
    };

    this.peerConnection.onicecandidate = function(e) {
      self.emit('iceCandidate', e);
      if (e.candidate) {
        self.logger.log('ICE candidate received: '+ (e.candidate.candidate === null ? null : e.candidate.candidate.trim()));
        self.startIceCheckingTimer();
      } else {
        self.onIceCompleted.resolve(this);
      }
    };

    this.peerConnection.onicegatheringstatechange = function () {
      self.logger.log('RTCIceGatheringState changed: ' + this.iceGatheringState);
      if (this.iceGatheringState === 'gathering') {
        self.emit('iceGathering', this);
      }
      if (this.iceGatheringState === 'complete') {
        self.onIceCompleted.resolve(this);
      }
    };

    this.peerConnection.oniceconnectionstatechange = function() {  //need e for commented out case
      var stateEvent;

      if (this.iceConnectionState === 'checking') {
        self.startIceCheckingTimer();
      }

      switch (this.iceConnectionState) {
      case 'new':
        stateEvent = 'iceConnection';
        break;
      case 'checking':
        stateEvent = 'iceConnectionChecking';
        break;
      case 'connected':
        stateEvent = 'iceConnectionConnected';
        break;
      case 'completed':
        stateEvent = 'iceConnectionCompleted';
        break;
      case 'failed':
        stateEvent = 'iceConnectionFailed';
        break;
      case 'disconnected':
        stateEvent = 'iceConnectionDisconnected';
        break;
      case 'closed':
        stateEvent = 'iceConnectionClosed';
        break;
      default:
        self.logger.warn('Unknown iceConnection state:', this.iceConnectionState);
        return;
      }
      self.emit(stateEvent, this);

      //Bria state changes are always connected -> disconnected -> connected on accept, so session gets terminated
      //normal calls switch from failed to connected in some cases, so checking for failed and terminated
      /*if (this.iceConnectionState === 'failed') {
        self.session.terminate({
        cause: SIP.C.causes.RTP_TIMEOUT,
        status_code: 200,
        reason_phrase: SIP.C.causes.RTP_TIMEOUT
      });
      } else if (e.currentTarget.iceGatheringState === 'complete' && this.iceConnectionState !== 'closed') {
      self.onIceCompleted(this);
      }*/
    };
  }},

  acquire: {writable: true, value: function acquire (constraints) {
    // Default audio & video to true
    constraints = this.checkAndDefaultConstraints(constraints);

    return new SIP.Utils.Promise(function(resolve, reject) {
      /*
       * Make the call asynchronous, so that ICCs have a chance
       * to define callbacks to `userMediaRequest`
       */
      this.emit('userMediaRequest', constraints);

      var emitThenCall = function(eventName, callback) {
        var callbackArgs = Array.prototype.slice.call(arguments, 2);
        // Emit with all of the arguments from the real callback.
        var newArgs = [eventName].concat(callbackArgs);
        this.emit.apply(this, newArgs);
        return callback.apply(null, callbackArgs);
      }.bind(this);

      if (constraints.audio || constraints.video) {
        this.WebRTC.getUserMedia(constraints)
        .then(
          emitThenCall.bind(this, 'userMedia', function(streams) { resolve(streams); }),
          emitThenCall.bind(this, 'userMediaFailed', function(e) {
            reject(e);
            throw e;
          })
        );
      } else {
        // Local streams were explicitly excluded.
        resolve([]);
      }
    }.bind(this));
  }},

  hasOffer: {writable: true, value: function hasOffer (where) {
    var offerState = 'have-' + where + '-offer';
    return this.peerConnection.signalingState === offerState;
  }}
});

return SessionDescriptionHandler;
};
<|endoftext|>


<commit_before>const header = document.querySelector(".header-nav")
const headerHeight = header.offsetHeight
const scrolledClass = "shadow-weak"

var addOrRemoveClass = function() {
	if (window.scrollY >= headerHeight) {
		header.classList.add(scrolledClass)
	} else {
		header.classList.remove(scrolledClass)
	}
}
// On scroll and load, add the class specified
window.addEventListener('scroll', addOrRemoveClass());
window.addEventListener('load', addOrRemoveClass());<commit_msg>Properly applies sticky header<commit_after>const header = document.querySelector(".header-nav")
const headerHeight = header.offsetHeight
const scrolledClass = "shadow-weak"

var addOrRemoveClass = function() {
	if (window.scrollY >= headerHeight) {
		header.classList.add(scrolledClass)
	} else {
		header.classList.remove(scrolledClass)
	}
}
// On scroll and load, add the class specified
window.addEventListener('scroll', addOrRemoveClass);
window.addEventListener('load', addOrRemoveClass);
<|endoftext|>


<commit_before>'use strict';

const AbstractTrackerAdapter = require('../../../classes/abstract-tracker-adapter');
const QueryResponse = require('./query-response');
const kickAssTorrent = require('kat-cr');

/**
 * KickAssTorrent adapter
 */
class TrackerAdapter extends AbstractTrackerAdapter {
	constructor() {
		super();

		this._url = 'http://kat.cr';
		this._adaptee = kickAssTorrent;
	}

	/**
	 * @override
	 */
	_searchQueryRequest(params) {
		return new Promise((resolve, reject) => {
			this._adaptee({
				search: params.query,
				page: 1
			}).then(results => {
				resolve(results);
			}).catch(err => {
				reject(err);
			});
		});

	}

	/**
	 * @override
	 */
	_createQueryResponse(responseData) {
		return new Promise((resolve, reject) => {
			let queryResponse = new QueryResponse(responseData);
			queryResponse.on('init:complete', response => {
				resolve(response);
			});
			queryResponse.on('init:error', err => {
				reject(err);
			});
		});
	}
}

module.exports = TrackerAdapter;
<commit_msg>Refactored<commit_after>'use strict';

const AbstractTrackerAdapter = require('../../../classes/abstract-tracker-adapter');
const SearchResponse = require('./query-response');
const kickAssTorrent = require('kat-cr');

/**
 * KickAssTorrent adapter
 */
class TrackerAdapter extends AbstractTrackerAdapter {
	constructor() {
		super();

		this._url = 'http://kat.cr';
		this._searchResponse = new SearchResponse();
		this._adaptee = kickAssTorrent;
	}

	/**
	 * @override
	 */
	_searchRequest(queryString) {
		return new Promise((resolve, reject) => {
			this._adaptee({
				search: queryString,
				page: 1
			}).then(results => {
				resolve(results);
			}).catch(err => {
				reject(err);
			});
		});

	}
}

module.exports = TrackerAdapter;
<|endoftext|>


<commit_before>define([
  'angular',
  'lodash',
  'kbn',
  'moment',
  './queryCtrl',
],
function (angular, _, kbn) {
  'use strict';

  var module = angular.module('grafana.services');

  module.factory('OpenTSDBDatasource', function($q, $http, templateSrv) {

    function OpenTSDBDatasource(datasource) {
      this.type = 'opentsdb';
      this.editorSrc = 'app/features/opentsdb/partials/query.editor.html';
      this.url = datasource.url;
      this.name = datasource.name;
      this.supportMetrics = true;
    }

    // Called once per panel (graph)
    OpenTSDBDatasource.prototype.query = function(options) {
      var start = convertToTSDBTime(options.range.from);
      var end = convertToTSDBTime(options.range.to);
      var qs = [];

      if (options.interval.match(/\.[0-9]+s/)) {
        options.interval = parseFloat(options.interval)*1000 + "ms";
      }
      _.each(options.targets, function(target) {
        qs.push(convertTargetToQuery(target, options.interval));
      });

      var queries = _.compact(qs);

      // No valid targets, return the empty result to save a round trip.
      if (_.isEmpty(queries)) {
        var d = $q.defer();
        d.resolve({ data: [] });
        return d.promise;
      }

      var groupByTags = {};
      _.each(queries, function(query) {
        _.each(query.tags, function(val, key) {
          groupByTags[key] = true;
        });
      });

      return this.performTimeSeriesQuery(queries, start, end)
        .then(_.bind(function(response) {
          var result = _.map(response.data, _.bind(function(metricData, index) {
            return transformMetricData(metricData, groupByTags, this.targets[index]);
          }, this));
          return { data: result };
        }, options));
    };

    OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
      var reqBody = {
        start: start,
        queries: queries
      };

      // Relative queries (e.g. last hour) don't include an end time
      if (end) {
        reqBody.end = end;
      }

      var options = {
        method: 'POST',
        url: this.url + '/api/query',
        data: reqBody
      };

      return $http(options);
    };

    OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) {
      var options = {
        method: 'GET',
        url: this.url + '/api/suggest',
        params: {
          type: type,
          q: query
        }
      };
      return $http(options).then(function(result) {
        return result.data;
      });
    };

    function transformMetricData(md, groupByTags, options) {
      var dps = [],
          tagData = [],
          metricLabel = null;

      if (!_.isEmpty(md.tags)) {
        _.each(_.pairs(md.tags), function(tag) {
          if (_.has(groupByTags, tag[0])) {
            tagData.push(tag[0] + "=" + tag[1]);
          }
        });
      }

      metricLabel = createMetricLabel(md.metric, tagData, options);

      // TSDB returns datapoints has a hash of ts => value.
      // Can't use _.pairs(invert()) because it stringifies keys/values
      _.each(md.dps, function (v, k) {
        dps.push([v, k * 1000]);
      });

      return { target: metricLabel, datapoints: dps };
    }

    function createMetricLabel(metric, tagData, options) {
      if (!_.isUndefined(options) && options.alias) {
        return options.alias;
      }

      if (!_.isEmpty(tagData)) {
        metric += "{" + tagData.join(", ") + "}";
      }

      return metric;
    }

    function convertTargetToQuery(target, interval) {
      if (!target.metric) {
        return null;
      }

      var query = {
        metric: templateSrv.replace(target.metric),
        aggregator: "avg"
      };

      if (target.aggregator) {
        query.aggregator = templateSrv.replace(target.aggregator);
      }

      if (target.shouldComputeRate) {
        query.rate = true;
        query.rateOptions = {
          counter: !!target.isCounter
        };

        if (target.counterMax && target.counterMax.length) {
          query.rateOptions.counterMax = parseInt(target.counterMax);
        }

        if (target.counterResetValue && target.counterResetValue.length) {
          query.rateOptions.resetValue = parseInt(target.counterResetValue);
        }
      }

      if (!target.disableDownsampling) {
        var buf =  target.downsampleInterval || interval;
        query.downsample = templateSrv.replace(buf) + "-" + target.downsampleAggregator;
      }

      query.tags = angular.copy(target.tags);
      if(query.tags){
        for(var key in query.tags){
          query.tags[key] = templateSrv.replace(query.tags[key]);
        }
      }

      return query;
    }

    function convertToTSDBTime(date) {
      if (date === 'now') {
        return null;
      }

      date = kbn.parseDate(date);

      return date.getTime();
    }

    return OpenTSDBDatasource;
  });

});
<commit_msg>OpenTSDB: Fixed issue with auto interval variable going below 1s, Fixes #1325<commit_after>define([
  'angular',
  'lodash',
  'kbn',
  'moment',
  './queryCtrl',
],
function (angular, _, kbn) {
  'use strict';

  var module = angular.module('grafana.services');

  module.factory('OpenTSDBDatasource', function($q, $http, templateSrv) {

    function OpenTSDBDatasource(datasource) {
      this.type = 'opentsdb';
      this.editorSrc = 'app/features/opentsdb/partials/query.editor.html';
      this.url = datasource.url;
      this.name = datasource.name;
      this.supportMetrics = true;
    }

    // Called once per panel (graph)
    OpenTSDBDatasource.prototype.query = function(options) {
      var start = convertToTSDBTime(options.range.from);
      var end = convertToTSDBTime(options.range.to);
      var qs = [];

      _.each(options.targets, function(target) {
        qs.push(convertTargetToQuery(target, options.interval));
      });

      var queries = _.compact(qs);

      // No valid targets, return the empty result to save a round trip.
      if (_.isEmpty(queries)) {
        var d = $q.defer();
        d.resolve({ data: [] });
        return d.promise;
      }

      var groupByTags = {};
      _.each(queries, function(query) {
        _.each(query.tags, function(val, key) {
          groupByTags[key] = true;
        });
      });

      return this.performTimeSeriesQuery(queries, start, end)
        .then(_.bind(function(response) {
          var result = _.map(response.data, _.bind(function(metricData, index) {
            return transformMetricData(metricData, groupByTags, this.targets[index]);
          }, this));
          return { data: result };
        }, options));
    };

    OpenTSDBDatasource.prototype.performTimeSeriesQuery = function(queries, start, end) {
      var reqBody = {
        start: start,
        queries: queries
      };

      // Relative queries (e.g. last hour) don't include an end time
      if (end) {
        reqBody.end = end;
      }

      var options = {
        method: 'POST',
        url: this.url + '/api/query',
        data: reqBody
      };

      return $http(options);
    };

    OpenTSDBDatasource.prototype.performSuggestQuery = function(query, type) {
      var options = {
        method: 'GET',
        url: this.url + '/api/suggest',
        params: {
          type: type,
          q: query
        }
      };
      return $http(options).then(function(result) {
        return result.data;
      });
    };

    function transformMetricData(md, groupByTags, options) {
      var dps = [],
          tagData = [],
          metricLabel = null;

      if (!_.isEmpty(md.tags)) {
        _.each(_.pairs(md.tags), function(tag) {
          if (_.has(groupByTags, tag[0])) {
            tagData.push(tag[0] + "=" + tag[1]);
          }
        });
      }

      metricLabel = createMetricLabel(md.metric, tagData, options);

      // TSDB returns datapoints has a hash of ts => value.
      // Can't use _.pairs(invert()) because it stringifies keys/values
      _.each(md.dps, function (v, k) {
        dps.push([v, k * 1000]);
      });

      return { target: metricLabel, datapoints: dps };
    }

    function createMetricLabel(metric, tagData, options) {
      if (!_.isUndefined(options) && options.alias) {
        return options.alias;
      }

      if (!_.isEmpty(tagData)) {
        metric += "{" + tagData.join(", ") + "}";
      }

      return metric;
    }

    function convertTargetToQuery(target, interval) {
      if (!target.metric) {
        return null;
      }

      var query = {
        metric: templateSrv.replace(target.metric),
        aggregator: "avg"
      };

      if (target.aggregator) {
        query.aggregator = templateSrv.replace(target.aggregator);
      }

      if (target.shouldComputeRate) {
        query.rate = true;
        query.rateOptions = {
          counter: !!target.isCounter
        };

        if (target.counterMax && target.counterMax.length) {
          query.rateOptions.counterMax = parseInt(target.counterMax);
        }

        if (target.counterResetValue && target.counterResetValue.length) {
          query.rateOptions.resetValue = parseInt(target.counterResetValue);
        }
      }

      if (!target.disableDownsampling) {
        interval =  templateSrv.replace(target.downsampleInterval || interval);

        if (interval.match(/\.[0-9]+s/)) {
          interval = parseFloat(interval)*1000 + "ms";
        }

        query.downsample = interval + "-" + target.downsampleAggregator;
      }

      query.tags = angular.copy(target.tags);
      if(query.tags){
        for(var key in query.tags){
          query.tags[key] = templateSrv.replace(query.tags[key]);
        }
      }

      return query;
    }

    function convertToTSDBTime(date) {
      if (date === 'now') {
        return null;
      }

      date = kbn.parseDate(date);

      return date.getTime();
    }

    return OpenTSDBDatasource;
  });

});
<|endoftext|>


<commit_before>import {
	get,
	set,
	setProperties,
	computed,
	inject,
	run,
	observer,
	on,
	Service
} from "ember";
import {
	files,
	notification
} from "config";
import nwWindow, {
	setMinimized,
	setVisibility
} from "nwjs/Window";
import { getMenu as getTrayMenu } from "nwjs/Tray";
import ChannelSettingsMixin from "mixins/ChannelSettingsMixin";
import { mapBy } from "utils/ember/recordArrayMethods";
import Logger from "utils/Logger";
import {
	isWin,
	tmpdir
} from "utils/node/platform";
import resolvePath from "utils/node/resolvePath";
import mkdirp from "utils/node/fs/mkdirp";
import download from "utils/node/fs/download";
import clearfolder from "utils/node/fs/clearfolder";
import { show as showNotification } from "utils/Notification";

const { and } = computed;
const { service } = inject;
const { cancel, debounce, later } = run;
const {
	cache: {
		"dir": cacheDir,
		"time": cacheTime
	},
	fails: {
		"requests": failsRequests,
		"channels": failsChannels
	},
	interval: {
		"request": intervalSuccess,
		"retry": intervalRetry,
		"error": intervalError
	}
} = notification;
const { icons: { "big": bigIcon } } = files;

const { logDebug } = new Logger( "NotificationService" );

const iconGroup = isWin
	? resolvePath( "%NWJSAPPPATH%", bigIcon )
	: resolvePath( bigIcon );
const cacheTmpDir = tmpdir( cacheDir );

class StreamCache {
	constructor( stream ) {
		this.id    = get( stream, "id" );
		this.since = get( stream, "created_at" );
		this.fails = 0;
	}

	/**
	 * @param {TwitchStream[]} streams
	 * @returns {Number}
	 */
	findStreamIndex( streams ) {
		for ( let id = this.id, i = 0, l = streams.length; i < l; i++ ) {
			if ( get( streams[ i ], "id" ) === id ) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * @param {TwitchStream} stream
	 * @returns {Boolean}
	 */
	isNotNewer( stream ) {
		return this.since >= get( stream, "created_at" );
	}
}

export default Service.extend( ChannelSettingsMixin, {
	auth: service(),
	chat: service(),
	routing: service( "-routing" ),
	settings: service(),
	store: service(),
	streaming: service(),

	model : [],
	notifs: [],
	_first: true,
	_tries: 0,
	_next : null,
	_error: false,

	// automatically start polling once the user is logged in and has notifications enabled
	enabled: and( "auth.session.isLoggedIn", "settings.notify_enabled" ),
	// notifications disabled via tray item
	// don't link this property with `enabled` (observe both instead)
	paused : false,
	running: computed( "enabled", "paused", function() {
		return get( this, "enabled" ) && !get( this, "paused" );
	}),
	error  : and( "running", "_error" ),

	enabledObserver: observer( "running", function() {
		if ( get( this, "running" ) ) {
			this.start();
		} else {
			this.reset();
		}
	}).on( "init" ),

	statusText: computed( "enabled", "paused", "error", function() {
		let status = !get( this, "enabled" )
			? "disabled"
			: get( this, "paused" )
			? "paused"
			: get( this, "error" )
			? "offline"
			: "enabled";

		return `Desktop notifications are ${status}`;
	}),

	/**
	 * Add a newly followed channel to the channel list cache
	 * so it doesn't pop up a new notification on the next query
	 */
	_userHasFollowedChannel: on( "init", function() {
		const store   = get( this, "store" );
		const follows = store.modelFor( "twitchChannelFollowed" );
		const adapter = store.adapterFor( "twitchChannelFollowed" );

		adapter.on( "createRecord", ( store, type, snapshot ) => {
			if ( !get( this, "running" ) ) { return; }
			if ( type !== follows ) { return; }

			let id = snapshot.id;
			// is the followed channel online?
			store.findRecord( "twitchStream", id, { reload: true } )
				.then( stream => {
					/** @type {StreamCache[]} */
					let model = get( this, "model" );
					if ( model.find( item => item.id === id ) ) { return; }
					model.push( new StreamCache( stream ) );
				});
		});
	}),

	_setWindowBadgeLabel() {
		let label;
		if ( !get( this, "running" ) || !get( this, "settings.notify_badgelabel" ) ) {
			label = "";

		} else {
			let num = get( this, "model.length" );
			label = String( num );
		}
		// update badge label or remove it
		nwWindow.setBadgeLabel( label );
	},

	_windowBadgeLabelObserver: observer(
		"running",
		"settings.notify_badgelabel",
		"model.[]",
		function() {
			debounce( this, "_setWindowBadgeLabel", 500 );
		}
	),

	_setupTrayItem: on( "init", function() {
		let item = null;
		const menu = getTrayMenu();

		const createTrayItem = () => {
			let enabled = get( this, "enabled" );
			if ( !enabled ) {
				if ( item ) {
					menu.items.removeObject( item );
				}
				set( this, "paused", false );
				return;
			}

			item = {
				type   : "checkbox",
				label  : "Pause notifications",
				tooltip: "Quickly toggle desktop notifications",
				checked: get( this, "paused" ),
				click  : item => {
					set( this, "paused", item.checked );
				}
			};

			menu.items.unshiftObject( item );
		};

		createTrayItem();
		this.addObserver( "enabled", createTrayItem );
	}),

	reset() {
		let next = get( this, "_next" );
		if ( next ) {
			cancel( next );
		}

		setProperties( this, {
			model : [],
			_first: true,
			_tries: 0,
			_error: false,
			_next : null
		});
	},

	start() {
		this.reset();

		// collect garbage once at the beginning
		this.gc_icons()
			// then start
			.then( () => this.check() );
	},

	check() {
		if ( !get( this, "running" ) ) { return; }

		get( this, "store" ).query( "twitchStreamFollowed", {
			limit: 100
		})
			.then( mapBy( "stream" ) )
			.then( streams => this.queryCallback( streams ) )
			.then( streams => this.stripDisabledChannels( streams ) )
			.then( streams => this.prepareNotifications( streams ) )
			.then( () => this.success() )
			.catch( () => this.failure() );
	},

	success() {
		// query again
		let next = later( this, this.check, intervalSuccess );

		setProperties( this, {
			_error: false,
			_next : next,
			_tries: 0
		});
	},

	failure() {
		let tries = get( this, "_tries" );
		let interval;

		// did we reach the retry limit yet?
		if ( ++tries > failsRequests ) {
			// reset notification state
			this.reset();
			// let the user know that there was an error...
			set( this, "_error", true );
			// ...but keep going
			interval = intervalError;

		} else {
			set( this, "_tries", tries );
			// immediately retry (with a slight delay)
			interval = intervalRetry;
		}

		let next = later( this, this.check, interval );
		set( this, "_next", next );
	},

	queryCallback( streams ) {
		/** @type {StreamCache[]} */
		let model = get( this, "model" );

		// figure out which streams are new
		for ( let item, idx, i = 0, l = model.length; i < l; i++ ) {
			item = model[ i ];

			// "indexOfBy"
			// streams are ordered, so most of the time the first item matches
			idx = item.findStreamIndex( streams );

			if ( idx !== -1 ) {
				// existing stream found:
				// has the stream still the same creation date?
				if ( item.isNotNewer( streams[ idx ] ) ) {
					// stream hasn't changed:
					// remove stream from record array
					streams.removeAt( idx, 1 );
					// reset fails counter
					item.fails = 0;
					// keep the item (is not new)
					continue;
				}

			} else {
				// stream not found (may be offline):
				// increase fails counter
				if ( ++item.fails <= failsChannels ) {
					// keep the item (has not reached failure limit yet)
					continue;
				}
			}

			// remove item from the model array
			model.removeAt( i, 1 );
			--i;
			--l;
		}

		// add new streams afterwards
		model.pushObjects( streams.map( stream => new StreamCache( stream ) ) );

		// just fill the cache and return an empty array of new streams on the first run
		if ( get( this, "_first" ) ) {
			set( this, "_first", false );
			return [];
		}

		return streams;
	},

	stripDisabledChannels( streams ) {
		let all = get( this, "settings.notify_all" );

		let promiseStreamSettings = streams.map( stream => {
			let name = get( stream, "channel.name" );
			return this.loadChannelSettings( name )
				.then( settings => ({ stream, settings }) );
		});

		return Promise.all( promiseStreamSettings )
			.then( streams => streams
				.filter( data => {
					let enabled = get( data.settings, "notify_enabled" );
					return all === true
						// include all, exclude disabled
						? enabled !== false
						// exclude all, include enabled
						: enabled === true;
				})
				.map( obj => obj.stream )
			);
	},

	prepareNotifications( streams ) {
		if ( !streams.length ) { return; }

		// merge multiple notifications and show a single one
		if ( streams.length > 1 && get( this, "settings.notify_grouping" ) ) {
			return this.showNotificationGroup( streams );

		// show all notifications
		} else {
			// download all channel icons first and save them into a local temp dir...
			return mkdirp( cacheTmpDir )
				.then( iconTempDir =>
					Promise.all( streams.map( stream => {
						let logo = get( stream, "channel.logo" );

						return download( logo, iconTempDir )
							// set the local channel logo on the stream record
							.then( file => set( stream, "logo", file ) )
							.then( () => stream );
					}) )
				)
				.then( streams => streams.forEach(
					stream => this.showNotificationSingle( stream )
				) );
		}
	},

	/**
	 * Show multiple streams as one notification
	 * @param {TwitchStream[]} streams
	 */
	showNotificationGroup( streams ) {
		let settings = get( this, "settings.notify_click_group" );

		this.showNotification({
			title  : "Some followed channels have started streaming",
			message: streams.map( stream => ({
				title  : get( stream, "channel.display_name" ),
				message: get( stream, "channel.status" ) || ""
			}) ),
			icon   : iconGroup,
			click  : () => this.notificationClick( settings, streams ),
			settings
		});
	},

	/**
	 * Show a notification for each stream
	 * @param {TwitchStream} stream
	 */
	showNotificationSingle( stream ) {
		let settings = get( this, "settings.notify_click" );
		let name = get( stream, "channel.display_name" );

		this.showNotification({
			title  : `${name} has started streaming`,
			message: get( stream, "channel.status" ) || "",
			icon   : get( stream, "logo" ) || get( stream, "channel.logo" ),
			click  : () => this.notificationClick( settings, [ stream ] ),
			settings
		});
	},

	/**
	 * Notfication click callback
	 * @param {Number} settings
	 * @param {TwitchStream[]} streams
	 */
	notificationClick( settings, streams ) {
		// always restore the window
		if ( settings !== 0 && get( this, "settings.notify_click_restore" ) ) {
			setMinimized( false );
			setVisibility( true );
		}

		logDebug( "Notification click", () => ({
			settings,
			streams: streams.mapBy( "id" )
		}) );

		let streaming = get( this, "streaming" );

		switch ( settings ) {
			// followed streams menu
			case 1:
				get( this, "routing" ).transitionTo( "user.followedStreams" );
				break;

			// launch stream
			case 2:
				streams.forEach( stream => streaming.startStream( stream ) );
				break;

			// launch stream + chat
			case 3:
				streams.forEach( stream => {
					streaming.startStream( stream );
					// don't open the chat twice (startStream may open chat already)
					if ( get( this, "settings.gui_openchat" ) ) { return; }
					let chat    = get( this, "chat" );
					let channel = get( stream, "channel.name" );
					chat.open( channel )
						.catch(function() {});
				});
				break;
		}
	},

	showNotification( data ) {
		let provider = get( this, "settings.notify_provider" );

		showNotification( provider, data, provider !== "auto" )
			.catch(function() {});
	},

	gc_icons() {
		return clearfolder( cacheTmpDir, cacheTime )
			// always resolve
			.catch(function() {});
	}
});
<commit_msg>Focus window when clicking a notification<commit_after>import {
	get,
	set,
	setProperties,
	computed,
	inject,
	run,
	observer,
	on,
	Service
} from "ember";
import {
	files,
	notification
} from "config";
import nwWindow, {
	setMinimized,
	setVisibility,
	setFocused
} from "nwjs/Window";
import { getMenu as getTrayMenu } from "nwjs/Tray";
import ChannelSettingsMixin from "mixins/ChannelSettingsMixin";
import { mapBy } from "utils/ember/recordArrayMethods";
import Logger from "utils/Logger";
import {
	isWin,
	tmpdir
} from "utils/node/platform";
import resolvePath from "utils/node/resolvePath";
import mkdirp from "utils/node/fs/mkdirp";
import download from "utils/node/fs/download";
import clearfolder from "utils/node/fs/clearfolder";
import { show as showNotification } from "utils/Notification";

const { and } = computed;
const { service } = inject;
const { cancel, debounce, later } = run;
const {
	cache: {
		"dir": cacheDir,
		"time": cacheTime
	},
	fails: {
		"requests": failsRequests,
		"channels": failsChannels
	},
	interval: {
		"request": intervalSuccess,
		"retry": intervalRetry,
		"error": intervalError
	}
} = notification;
const { icons: { "big": bigIcon } } = files;

const { logDebug } = new Logger( "NotificationService" );

const iconGroup = isWin
	? resolvePath( "%NWJSAPPPATH%", bigIcon )
	: resolvePath( bigIcon );
const cacheTmpDir = tmpdir( cacheDir );

class StreamCache {
	constructor( stream ) {
		this.id    = get( stream, "id" );
		this.since = get( stream, "created_at" );
		this.fails = 0;
	}

	/**
	 * @param {TwitchStream[]} streams
	 * @returns {Number}
	 */
	findStreamIndex( streams ) {
		for ( let id = this.id, i = 0, l = streams.length; i < l; i++ ) {
			if ( get( streams[ i ], "id" ) === id ) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * @param {TwitchStream} stream
	 * @returns {Boolean}
	 */
	isNotNewer( stream ) {
		return this.since >= get( stream, "created_at" );
	}
}

export default Service.extend( ChannelSettingsMixin, {
	auth: service(),
	chat: service(),
	routing: service( "-routing" ),
	settings: service(),
	store: service(),
	streaming: service(),

	model : [],
	notifs: [],
	_first: true,
	_tries: 0,
	_next : null,
	_error: false,

	// automatically start polling once the user is logged in and has notifications enabled
	enabled: and( "auth.session.isLoggedIn", "settings.notify_enabled" ),
	// notifications disabled via tray item
	// don't link this property with `enabled` (observe both instead)
	paused : false,
	running: computed( "enabled", "paused", function() {
		return get( this, "enabled" ) && !get( this, "paused" );
	}),
	error  : and( "running", "_error" ),

	enabledObserver: observer( "running", function() {
		if ( get( this, "running" ) ) {
			this.start();
		} else {
			this.reset();
		}
	}).on( "init" ),

	statusText: computed( "enabled", "paused", "error", function() {
		let status = !get( this, "enabled" )
			? "disabled"
			: get( this, "paused" )
			? "paused"
			: get( this, "error" )
			? "offline"
			: "enabled";

		return `Desktop notifications are ${status}`;
	}),

	/**
	 * Add a newly followed channel to the channel list cache
	 * so it doesn't pop up a new notification on the next query
	 */
	_userHasFollowedChannel: on( "init", function() {
		const store   = get( this, "store" );
		const follows = store.modelFor( "twitchChannelFollowed" );
		const adapter = store.adapterFor( "twitchChannelFollowed" );

		adapter.on( "createRecord", ( store, type, snapshot ) => {
			if ( !get( this, "running" ) ) { return; }
			if ( type !== follows ) { return; }

			let id = snapshot.id;
			// is the followed channel online?
			store.findRecord( "twitchStream", id, { reload: true } )
				.then( stream => {
					/** @type {StreamCache[]} */
					let model = get( this, "model" );
					if ( model.find( item => item.id === id ) ) { return; }
					model.push( new StreamCache( stream ) );
				});
		});
	}),

	_setWindowBadgeLabel() {
		let label;
		if ( !get( this, "running" ) || !get( this, "settings.notify_badgelabel" ) ) {
			label = "";

		} else {
			let num = get( this, "model.length" );
			label = String( num );
		}
		// update badge label or remove it
		nwWindow.setBadgeLabel( label );
	},

	_windowBadgeLabelObserver: observer(
		"running",
		"settings.notify_badgelabel",
		"model.[]",
		function() {
			debounce( this, "_setWindowBadgeLabel", 500 );
		}
	),

	_setupTrayItem: on( "init", function() {
		let item = null;
		const menu = getTrayMenu();

		const createTrayItem = () => {
			let enabled = get( this, "enabled" );
			if ( !enabled ) {
				if ( item ) {
					menu.items.removeObject( item );
				}
				set( this, "paused", false );
				return;
			}

			item = {
				type   : "checkbox",
				label  : "Pause notifications",
				tooltip: "Quickly toggle desktop notifications",
				checked: get( this, "paused" ),
				click  : item => {
					set( this, "paused", item.checked );
				}
			};

			menu.items.unshiftObject( item );
		};

		createTrayItem();
		this.addObserver( "enabled", createTrayItem );
	}),

	reset() {
		let next = get( this, "_next" );
		if ( next ) {
			cancel( next );
		}

		setProperties( this, {
			model : [],
			_first: true,
			_tries: 0,
			_error: false,
			_next : null
		});
	},

	start() {
		this.reset();

		// collect garbage once at the beginning
		this.gc_icons()
			// then start
			.then( () => this.check() );
	},

	check() {
		if ( !get( this, "running" ) ) { return; }

		get( this, "store" ).query( "twitchStreamFollowed", {
			limit: 100
		})
			.then( mapBy( "stream" ) )
			.then( streams => this.queryCallback( streams ) )
			.then( streams => this.stripDisabledChannels( streams ) )
			.then( streams => this.prepareNotifications( streams ) )
			.then( () => this.success() )
			.catch( () => this.failure() );
	},

	success() {
		// query again
		let next = later( this, this.check, intervalSuccess );

		setProperties( this, {
			_error: false,
			_next : next,
			_tries: 0
		});
	},

	failure() {
		let tries = get( this, "_tries" );
		let interval;

		// did we reach the retry limit yet?
		if ( ++tries > failsRequests ) {
			// reset notification state
			this.reset();
			// let the user know that there was an error...
			set( this, "_error", true );
			// ...but keep going
			interval = intervalError;

		} else {
			set( this, "_tries", tries );
			// immediately retry (with a slight delay)
			interval = intervalRetry;
		}

		let next = later( this, this.check, interval );
		set( this, "_next", next );
	},

	queryCallback( streams ) {
		/** @type {StreamCache[]} */
		let model = get( this, "model" );

		// figure out which streams are new
		for ( let item, idx, i = 0, l = model.length; i < l; i++ ) {
			item = model[ i ];

			// "indexOfBy"
			// streams are ordered, so most of the time the first item matches
			idx = item.findStreamIndex( streams );

			if ( idx !== -1 ) {
				// existing stream found:
				// has the stream still the same creation date?
				if ( item.isNotNewer( streams[ idx ] ) ) {
					// stream hasn't changed:
					// remove stream from record array
					streams.removeAt( idx, 1 );
					// reset fails counter
					item.fails = 0;
					// keep the item (is not new)
					continue;
				}

			} else {
				// stream not found (may be offline):
				// increase fails counter
				if ( ++item.fails <= failsChannels ) {
					// keep the item (has not reached failure limit yet)
					continue;
				}
			}

			// remove item from the model array
			model.removeAt( i, 1 );
			--i;
			--l;
		}

		// add new streams afterwards
		model.pushObjects( streams.map( stream => new StreamCache( stream ) ) );

		// just fill the cache and return an empty array of new streams on the first run
		if ( get( this, "_first" ) ) {
			set( this, "_first", false );
			return [];
		}

		return streams;
	},

	stripDisabledChannels( streams ) {
		let all = get( this, "settings.notify_all" );

		let promiseStreamSettings = streams.map( stream => {
			let name = get( stream, "channel.name" );
			return this.loadChannelSettings( name )
				.then( settings => ({ stream, settings }) );
		});

		return Promise.all( promiseStreamSettings )
			.then( streams => streams
				.filter( data => {
					let enabled = get( data.settings, "notify_enabled" );
					return all === true
						// include all, exclude disabled
						? enabled !== false
						// exclude all, include enabled
						: enabled === true;
				})
				.map( obj => obj.stream )
			);
	},

	prepareNotifications( streams ) {
		if ( !streams.length ) { return; }

		// merge multiple notifications and show a single one
		if ( streams.length > 1 && get( this, "settings.notify_grouping" ) ) {
			return this.showNotificationGroup( streams );

		// show all notifications
		} else {
			// download all channel icons first and save them into a local temp dir...
			return mkdirp( cacheTmpDir )
				.then( iconTempDir =>
					Promise.all( streams.map( stream => {
						let logo = get( stream, "channel.logo" );

						return download( logo, iconTempDir )
							// set the local channel logo on the stream record
							.then( file => set( stream, "logo", file ) )
							.then( () => stream );
					}) )
				)
				.then( streams => streams.forEach(
					stream => this.showNotificationSingle( stream )
				) );
		}
	},

	/**
	 * Show multiple streams as one notification
	 * @param {TwitchStream[]} streams
	 */
	showNotificationGroup( streams ) {
		let settings = get( this, "settings.notify_click_group" );

		this.showNotification({
			title  : "Some followed channels have started streaming",
			message: streams.map( stream => ({
				title  : get( stream, "channel.display_name" ),
				message: get( stream, "channel.status" ) || ""
			}) ),
			icon   : iconGroup,
			click  : () => this.notificationClick( settings, streams ),
			settings
		});
	},

	/**
	 * Show a notification for each stream
	 * @param {TwitchStream} stream
	 */
	showNotificationSingle( stream ) {
		let settings = get( this, "settings.notify_click" );
		let name = get( stream, "channel.display_name" );

		this.showNotification({
			title  : `${name} has started streaming`,
			message: get( stream, "channel.status" ) || "",
			icon   : get( stream, "logo" ) || get( stream, "channel.logo" ),
			click  : () => this.notificationClick( settings, [ stream ] ),
			settings
		});
	},

	/**
	 * Notfication click callback
	 * @param {Number} settings
	 * @param {TwitchStream[]} streams
	 */
	notificationClick( settings, streams ) {
		// always restore the window
		if ( settings !== 0 && get( this, "settings.notify_click_restore" ) ) {
			setMinimized( false );
			setVisibility( true );
			setFocused( true );
		}

		logDebug( "Notification click", () => ({
			settings,
			streams: streams.mapBy( "id" )
		}) );

		let streaming = get( this, "streaming" );

		switch ( settings ) {
			// followed streams menu
			case 1:
				get( this, "routing" ).transitionTo( "user.followedStreams" );
				break;

			// launch stream
			case 2:
				streams.forEach( stream => streaming.startStream( stream ) );
				break;

			// launch stream + chat
			case 3:
				streams.forEach( stream => {
					streaming.startStream( stream );
					// don't open the chat twice (startStream may open chat already)
					if ( get( this, "settings.gui_openchat" ) ) { return; }
					let chat    = get( this, "chat" );
					let channel = get( stream, "channel.name" );
					chat.open( channel )
						.catch(function() {});
				});
				break;
		}
	},

	showNotification( data ) {
		let provider = get( this, "settings.notify_provider" );

		showNotification( provider, data, provider !== "auto" )
			.catch(function() {});
	},

	gc_icons() {
		return clearfolder( cacheTmpDir, cacheTime )
			// always resolve
			.catch(function() {});
	}
});
<|endoftext|>


<commit_before>const gulp = require('gulp')
const $ = require('gulp-load-plugins')()
const when = require('gulp-if')
const browserify = require('browserify')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')
const babel = require('babelify')

const config = require('../gulp.config')
const transform = [ babel ]
const vendors = config.scriptVendors
const production = config.production
const destination = config.theme + '/assets'

gulp.task('vendor:scripts', () => {
  const b = browserify({
    debug: !production
  })

  // require all libs specified in vendors array
  vendors.forEach(lib => {
    b.require(lib)
  })

  return b.bundle()
    .pipe(source('vendor.js'))
    .pipe(buffer())
    .pipe(when(!production, $.sourcemaps.init({ loadMaps: true })))
    .pipe(when(!production, $.sourcemaps.write()))
    // All production stuff here
    // Rename file to .min and uglify that stuff
    .pipe(when(production, $.uglify())).on('error', config.onError)
    .pipe(gulp.dest(destination))
})

gulp.task('main:scripts', () => {
  return browserify({
    entries: config.src.scripts + '/app.js',
    debug: !production
  })
    .external(vendors)
    .transform(transform)
    .bundle().on('error', config.onError)
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(when(!production, $.sourcemaps.init({ loadMaps: true })))
    .pipe(when(!production, $.sourcemaps.write()))
    // All production stuff here
    // Rename file to .min and uglify that stuff
    .pipe(when(production, $.rename({suffix: '.min'})))
    .pipe(when(production, $.uglify())).on('error', config.onError)
    .pipe(gulp.dest(destination))
})

gulp.task('scripts', gulp.parallel('vendor:scripts', 'main:scripts'))
<commit_msg>removing min js from bundle<commit_after>const gulp = require('gulp')
const $ = require('gulp-load-plugins')()
const when = require('gulp-if')
const browserify = require('browserify')
const source = require('vinyl-source-stream')
const buffer = require('vinyl-buffer')
const babel = require('babelify')

const config = require('../gulp.config')
const transform = [ babel ]
const vendors = config.scriptVendors
const production = config.production
const destination = config.theme + '/assets'

gulp.task('vendor:scripts', () => {
  const b = browserify({
    debug: !production
  })

  // require all libs specified in vendors array
  vendors.forEach(lib => {
    b.require(lib)
  })

  return b.bundle()
    .pipe(source('vendor.js'))
    .pipe(buffer())
    .pipe(when(!production, $.sourcemaps.init({ loadMaps: true })))
    .pipe(when(!production, $.sourcemaps.write()))
    // All production stuff here
    // Rename file to .min and uglify that stuff
    .pipe(when(production, $.uglify())).on('error', config.onError)
    .pipe(gulp.dest(destination))
})

gulp.task('main:scripts', () => {
  return browserify({
    entries: config.src.scripts + '/app.js',
    debug: !production
  })
    .external(vendors)
    .transform(transform)
    .bundle().on('error', config.onError)
    .pipe(source('app.js'))
    .pipe(buffer())
    .pipe(when(!production, $.sourcemaps.init({ loadMaps: true })))
    .pipe(when(!production, $.sourcemaps.write()))
    // All production stuff here
    // Rename file to .min and uglify that stuff
    .pipe(when(production, $.uglify())).on('error', config.onError)
    .pipe(gulp.dest(destination))
})

gulp.task('scripts', gulp.parallel('vendor:scripts', 'main:scripts'))
<|endoftext|>