<commit_before>// Copyright 2002-2013, University of Colorado Boulder

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the 'contract' between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function( require ) {
  'use strict';

  // Includes
  var Vector2 = require( 'DOT/Vector2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Bucket = require( 'PHETCOMMON/model/Bucket' );

  function SphereBucket( options ) {
    Bucket.call( this, options );
    this._sphereRadius = options.sphereRadius || 10;
    this._usableWidthProportion = options.usableWidthProportion || 1.0;

    // Empirically determined, for positioning particles inside the bucket.
    this._verticalParticleOffset = options.verticalParticleOffset === undefined ? -this._sphereRadius * 0.4 : options.verticalParticleOffset;
    this._particles = [];
  }

  // Inherit from base type.
  inherit( Bucket, SphereBucket );

  // NOTE: beware, 2nd argument boolean flag is the opposite from the Java implementation!
  SphereBucket.prototype.addParticleFirstOpen = function( particle, animate ) {
    particle.destination = this._getFirstOpenLocation();
    this._addParticle( particle, animate );
  };

  SphereBucket.prototype.addParticleNearestOpen = function( particle, animate ) {
    particle.destination = this._getNearestOpenLocation( particle.destination );
    this._addParticle( particle, animate );
  };

  SphereBucket.prototype._addParticle = function( particle, animate ) {
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this._particles.push( particle );
    var thisBucket = this;
    var particleRemovedListener = function( userControlled ) {
      thisBucket.removeParticle( particle );
      particle.userControlledProperty.unlink( particleRemovedListener );
      delete particle.bucketRemovalListener;
    };
    particle.userControlledProperty.lazyLink( particleRemovedListener );
    particle.bucketRemovalListener = particleRemovedListener; // Attach to the particle to aid unlinking in some cases.
  };

  SphereBucket.prototype.removeParticle = function( particle, skipLayout ) {
    if ( this._particles.indexOf( particle ) === -1 ) {
      console.log( 'Error: Attempt to remove particle not contained in bucket, ignoring.' );
      return;
    }
    this._particles = _.without( this._particles, particle );
    if ( !skipLayout ) {
      this.relayoutBucketParticles();
    }
  };

  SphereBucket.prototype.containsParticle = function( particle ) {
    return this._particles.indexOf( particle ) !== -1;
  };

  SphereBucket.prototype.extractClosestParticle = function( location ) {
    var closestParticle = null;
    this._particles.forEach( function( particle ) {
      if ( closestParticle === null || closestParticle.position.distance( location ) > particle.position.distance( location ) ) {
        closestParticle = particle;
      }
    } );
    if ( closestParticle !== null ) {
      // The particle is removed by setting 'userControlled' to true.  This
      // relies on the listener that was added when the particle was placed
      // into the bucket.
      closestParticle.userControlled = true;
    }
    return closestParticle;
  };

  SphereBucket.prototype.getParticleList = function() { return this._particles; };

  SphereBucket.prototype.reset = function() {
    this._particles.forEach( function( particle ) {
      // Remove listeners that are watching for removal from bucket.
      if ( typeof( particle.bucketRemovalListener ) === 'function' ) {
        particle.userControlledProperty.unlink( particle.bucketRemovalListener );
        delete particle.bucketRemovalListener;
      }
    } );
    this._particles.length = 0;
  };

  SphereBucket.prototype._isPositionOpen = function( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this._particles.length; i++ ) {
      var particle = this._particles[ i ];
      if ( particle.destination.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  };

  SphereBucket.prototype._getFirstOpenLocation = function() {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.size.width * this._usableWidthProportion - 2 * this._sphereRadius;
    var offsetFromBucketEdge = ( this.size.width - usableWidth ) / 2 + this._sphereRadius;
    var numParticlesInLayer = Math.floor( usableWidth / ( this._sphereRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this._sphereRadius,
        this._getYPositionForLayer( row ) );
      if ( this._isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this._sphereRadius;
          if ( numParticlesInLayer === 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this._sphereRadius;
          }
        }
      }
    }
    return openLocation;
  };

  SphereBucket.prototype._getLayerForYPosition = function( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.position.y + this._verticalParticleOffset ) ) / ( this._sphereRadius * 2 * 0.866 ) ) );
  };

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype._getNearestOpenLocation = function( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this._particles, function( particle ) {
      var layer = self._getLayerForYPosition( particle.destination.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var usableWidth = this.size.width * this._usableWidthProportion - 2 * this._sphereRadius;
    var offsetFromBucketEdge = ( this.size.width - usableWidth ) / 2 + this._sphereRadius;
    var numParticlesInLayer = Math.floor( usableWidth / ( this._sphereRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this._sphereRadius,
          this._getYPositionForLayer( layer ) );
        if ( this._isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer === 0 || this.countSupportingParticles( testPosition ) === 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this._sphereRadius;
      if ( numParticlesInLayer === 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this._sphereRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position ) ) {
        // This location is closer.
        closestOpenLocation = location;
      }
    } );
    return closestOpenLocation;
  };

  SphereBucket.prototype._getYPositionForLayer = function( layer ) {
    return this.position.y + this._verticalParticleOffset + layer * this._sphereRadius * 2 * 0.866;
  };

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype._isDangling = function( particle ) {
    var onBottomRow = particle.destination.y === this.position.y + this._verticalParticleOffset;
    return !onBottomRow && this.countSupportingParticles( particle.destination ) < 2;
  };

  SphereBucket.prototype.countSupportingParticles = function( position ) {
    var count = 0;
    for ( var i = 0; i < this._particles.length; i++ ) {
      var p = this._particles[i];
      if ( p.destination.y < position.y && // Must be in a lower layer
           p.destination.distance( position ) < this._sphereRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  };

  SphereBucket.prototype.relayoutBucketParticles = function() {
    var particleMoved;
    do {
      for ( var i = 0; i < this._particles.length; i++ ) {
        particleMoved = false;
        var particle = this._particles[i];
        if ( this._isDangling( particle ) ) {
          particle.destination = this._getNearestOpenLocation( particle.destination );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  };

  return SphereBucket;
} );
<commit_msg>cleanArray usage<commit_after>// Copyright 2002-2013, University of Colorado Boulder

/*
 * A bucket that can be used to store spherical objects.  Manages the addition
 * and removal of the spheres, and stacks them as the are added, and manages
 * the stack as spheres are removed.
 *
 * This expects the spheres to have certain properties, please inspect the
 * code to understand the 'contract' between the bucket and the spheres.
 *
 * @author John Blanco
 */
define( function( require ) {
  'use strict';

  // Includes
  var cleanArray = require( 'PHET_CORE/cleanArray' );
  var Vector2 = require( 'DOT/Vector2' );
  var inherit = require( 'PHET_CORE/inherit' );
  var Bucket = require( 'PHETCOMMON/model/Bucket' );

  function SphereBucket( options ) {
    Bucket.call( this, options );
    this._sphereRadius = options.sphereRadius || 10;
    this._usableWidthProportion = options.usableWidthProportion || 1.0;

    // Empirically determined, for positioning particles inside the bucket.
    this._verticalParticleOffset = options.verticalParticleOffset === undefined ? -this._sphereRadius * 0.4 : options.verticalParticleOffset;
    this._particles = [];
  }

  // Inherit from base type.
  inherit( Bucket, SphereBucket );

  // NOTE: beware, 2nd argument boolean flag is the opposite from the Java implementation!
  SphereBucket.prototype.addParticleFirstOpen = function( particle, animate ) {
    particle.destination = this._getFirstOpenLocation();
    this._addParticle( particle, animate );
  };

  SphereBucket.prototype.addParticleNearestOpen = function( particle, animate ) {
    particle.destination = this._getNearestOpenLocation( particle.destination );
    this._addParticle( particle, animate );
  };

  SphereBucket.prototype._addParticle = function( particle, animate ) {
    if ( !animate ) {
      particle.position = particle.destination;
    }
    this._particles.push( particle );
    var thisBucket = this;
    var particleRemovedListener = function( userControlled ) {
      thisBucket.removeParticle( particle );
      particle.userControlledProperty.unlink( particleRemovedListener );
      delete particle.bucketRemovalListener;
    };
    particle.userControlledProperty.lazyLink( particleRemovedListener );
    particle.bucketRemovalListener = particleRemovedListener; // Attach to the particle to aid unlinking in some cases.
  };

  SphereBucket.prototype.removeParticle = function( particle, skipLayout ) {
    if ( this._particles.indexOf( particle ) === -1 ) {
      console.log( 'Error: Attempt to remove particle not contained in bucket, ignoring.' );
      return;
    }
    this._particles = _.without( this._particles, particle );
    if ( !skipLayout ) {
      this.relayoutBucketParticles();
    }
  };

  SphereBucket.prototype.containsParticle = function( particle ) {
    return this._particles.indexOf( particle ) !== -1;
  };

  SphereBucket.prototype.extractClosestParticle = function( location ) {
    var closestParticle = null;
    this._particles.forEach( function( particle ) {
      if ( closestParticle === null || closestParticle.position.distance( location ) > particle.position.distance( location ) ) {
        closestParticle = particle;
      }
    } );
    if ( closestParticle !== null ) {
      // The particle is removed by setting 'userControlled' to true.  This
      // relies on the listener that was added when the particle was placed
      // into the bucket.
      closestParticle.userControlled = true;
    }
    return closestParticle;
  };

  SphereBucket.prototype.getParticleList = function() { return this._particles; };

  SphereBucket.prototype.reset = function() {
    this._particles.forEach( function( particle ) {
      // Remove listeners that are watching for removal from bucket.
      if ( typeof( particle.bucketRemovalListener ) === 'function' ) {
        particle.userControlledProperty.unlink( particle.bucketRemovalListener );
        delete particle.bucketRemovalListener;
      }
    } );
    cleanArray( this._particles );
  };

  SphereBucket.prototype._isPositionOpen = function( position ) {
    var positionOpen = true;
    for ( var i = 0; i < this._particles.length; i++ ) {
      var particle = this._particles[ i ];
      if ( particle.destination.equals( position ) ) {
        positionOpen = false;
        break;
      }
    }
    return positionOpen;
  };

  SphereBucket.prototype._getFirstOpenLocation = function() {
    var openLocation = Vector2.ZERO;
    var usableWidth = this.size.width * this._usableWidthProportion - 2 * this._sphereRadius;
    var offsetFromBucketEdge = ( this.size.width - usableWidth ) / 2 + this._sphereRadius;
    var numParticlesInLayer = Math.floor( usableWidth / ( this._sphereRadius * 2 ) );
    var row = 0;
    var positionInLayer = 0;
    var found = false;
    while ( !found ) {
      var testLocation = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this._sphereRadius,
        this._getYPositionForLayer( row ) );
      if ( this._isPositionOpen( testLocation ) ) {
        // We found a location that is open.
        openLocation = testLocation;
        found = true;
      }
      else {
        positionInLayer++;
        if ( positionInLayer >= numParticlesInLayer ) {
          // Move to the next layer.
          row++;
          positionInLayer = 0;
          numParticlesInLayer--;
          offsetFromBucketEdge += this._sphereRadius;
          if ( numParticlesInLayer === 0 ) {
            // This algorithm doesn't handle the situation where
            // more particles are added than can be stacked into
            // a pyramid of the needed size, but so far it hasn't
            // needed to.  If this requirement changes, the
            // algorithm will need to change too.
            numParticlesInLayer = 1;
            offsetFromBucketEdge -= this._sphereRadius;
          }
        }
      }
    }
    return openLocation;
  };

  SphereBucket.prototype._getLayerForYPosition = function( yPosition ) {
    return Math.abs( Math.round( ( yPosition - ( this.position.y + this._verticalParticleOffset ) ) / ( this._sphereRadius * 2 * 0.866 ) ) );
  };

  /*
   * Get the nearest open location to the provided current location.  This
   * is used for particle stacking.
   *
   * @param location
   * @return
   */
  SphereBucket.prototype._getNearestOpenLocation = function( position ) {
    // Determine the highest occupied layer.  The bottom layer is 0.
    var highestOccupiedLayer = 0;
    var self = this;
    _.each( this._particles, function( particle ) {
      var layer = self._getLayerForYPosition( particle.destination.y );
      if ( layer > highestOccupiedLayer ) {
        highestOccupiedLayer = layer;
      }
    } );

    // Make a list of all open locations in the occupied layers.
    var openLocations = [];
    var usableWidth = this.size.width * this._usableWidthProportion - 2 * this._sphereRadius;
    var offsetFromBucketEdge = ( this.size.width - usableWidth ) / 2 + this._sphereRadius;
    var numParticlesInLayer = Math.floor( usableWidth / ( this._sphereRadius * 2 ) );

    // Loop, searching for open positions in the particle stack.
    for ( var layer = 0; layer <= highestOccupiedLayer + 1; layer++ ) {

      // Add all open locations in the current layer.
      for ( var positionInLayer = 0; positionInLayer < numParticlesInLayer; positionInLayer++ ) {
        var testPosition = new Vector2( this.position.x - this.size.width / 2 + offsetFromBucketEdge + positionInLayer * 2 * this._sphereRadius,
          this._getYPositionForLayer( layer ) );
        if ( this._isPositionOpen( testPosition ) ) {

          // We found a location that is unoccupied.
          if ( layer === 0 || this.countSupportingParticles( testPosition ) === 2 ) {
            // This is a valid open location.
            openLocations.push( testPosition );
          }
        }
      }

      // Adjust variables for the next layer.
      numParticlesInLayer--;
      offsetFromBucketEdge += this._sphereRadius;
      if ( numParticlesInLayer === 0 ) {
        // If the stacking pyramid is full, meaning that there are
        // no locations that are open within it, this algorithm
        // classifies the locations directly above the top of the
        // pyramid as being open.  This would result in a stack
        // of particles with a pyramid base.  So far, this hasn't
        // been a problem, but this limitation may limit
        // reusability of this algorithm.
        numParticlesInLayer = 1;
        offsetFromBucketEdge -= this._sphereRadius;
      }
    }

    // Find the closest open location to the provided current location.
    // Only the X-component is used for this determination, because if
    // the Y-component is used the particles often appear to fall sideways
    // when released above the bucket, which just looks weird.
    var closestOpenLocation = openLocations[0] || Vector2.ZERO;

    _.each( openLocations, function( location ) {
      if ( location.distance( position ) < closestOpenLocation.distance( position ) ) {
        // This location is closer.
        closestOpenLocation = location;
      }
    } );
    return closestOpenLocation;
  };

  SphereBucket.prototype._getYPositionForLayer = function( layer ) {
    return this.position.y + this._verticalParticleOffset + layer * this._sphereRadius * 2 * 0.866;
  };

  /*
   * Determine whether a particle is 'dangling', i.e. hanging above an open
   * space in the stack of particles.  Dangling particles should fall.
   */
  SphereBucket.prototype._isDangling = function( particle ) {
    var onBottomRow = particle.destination.y === this.position.y + this._verticalParticleOffset;
    return !onBottomRow && this.countSupportingParticles( particle.destination ) < 2;
  };

  SphereBucket.prototype.countSupportingParticles = function( position ) {
    var count = 0;
    for ( var i = 0; i < this._particles.length; i++ ) {
      var p = this._particles[i];
      if ( p.destination.y < position.y && // Must be in a lower layer
           p.destination.distance( position ) < this._sphereRadius * 3 ) {
        // Must be a supporting particle.
        count++;
      }
    }
    return count;
  };

  SphereBucket.prototype.relayoutBucketParticles = function() {
    var particleMoved;
    do {
      for ( var i = 0; i < this._particles.length; i++ ) {
        particleMoved = false;
        var particle = this._particles[i];
        if ( this._isDangling( particle ) ) {
          particle.destination = this._getNearestOpenLocation( particle.destination );
          particleMoved = true;
          break;
        }
      }
    } while ( particleMoved );
  };

  return SphereBucket;
} );
<|endoftext|>


<commit_before>/* Main app file for CEL project
 * Mostly stolen from requirejs examples: http://requirejs.org/docs/api.html
 * See http://requirejs.org/docs/api.html for details
 */

define(['jquery', 'Leaflet', '../lib/response', 'Handlebars', 'bootstrap',
        'jquery-ui', 'jquery-cookie', CEL.serverVars.gmapRequire],
function($, L, Response, Handlebars) {
    'use strict';

    var breakpoints = {
        mobile: 420,
        tablet: 767,
        desktop: 1024,
		desktopalt: 1140
    };

    var isTouchscreen = ('ontouchstart' in document.documentElement);

    // Hide the address bar on mobile browsers
    // Solution from: http://mobile.tutsplus.com/tutorials/mobile-web-apps/remove-address-bar/
    function hideAddressBar() {
        if(!window.location.hash) {
            if(document.height < window.outerHeight) {
                document.body.style.height = (window.outerHeight + 50) + 'px';
            }
            setTimeout(function() {
                window.scrollTo(0, 1);
            }, 50);
        }
    }
    if (isTouchscreen) {
        $(window).on('load', function() {
            if(!window.pageYOffset) {
                hideAddressBar();
            }
        }).on('orientationchange', hideAddressBar);
    }

    var gaTrackEvent = function($elt) {
        var category = CEL.serverVars.language + '/' + $elt.data('ga-category'),
            action = $elt.data('ga-action'),
            opt_label = $elt.data('ga-label'),
            opt_value = $elt.data('ga-value'),
            opt_noninteraction = $elt.data('ga-noninteraction'),
            data = ['_trackEvent', category, action];
        // optional parameters
        data.push(opt_label || undefined);
        data.push(opt_value || undefined);
        data.push(!!(opt_noninteraction));
        _gaq.push(data);
    };

    $(document).ready(function() {

        // Remove css tooltips when on a touchscreen device
        if (isTouchscreen) {
            $('[data-hint]').removeAttr('data-hint');
        }

        // set up ga tracking
        $('body').on('click', '*.ga-track', function(e) {
            gaTrackEvent($(this));
        });

        // AUTOCOMPLETE

        // New Autocomplete Testing:

        var $autocomplete = $('.autocomplete-searchbox');

        // JQuery $().autocomplete() function handles all interaction with
        // the input and composing the dropdown
        var autocomplete = $autocomplete.autocomplete({
            minLength: 1,
            source: function(request, response) {
                getAutocompletePlaces(request, response);
            },
            select: function(event, ui) {
                var place_id = ui.item.place_id;
                selectPlace(place_id);
            },
            focus: function(event, ui) {
                var selection = ui.item
                $autocomplete.data({
                    label: selection.label,
                    place_id: selection.place_id
                });
                console.log($autocomplete.data());
            }
        });

        // Calls the Places API to populate the dropdown
        // Returns array of possible matches

        function getAutocompletePlaces(request, response) {

            var input_term = 'Chicago IL ' + request.term;
            var service = new google.maps.places.AutocompleteService();
            service.getPlacePredictions({
                input: input_term,
                types: ['geocode'],
                componentRestrictions: {
                    country: 'us'
                }
            }, function(predictions, status) {
                var cleanedResults = [];
                if (predictions === null) {
                    cleanedResults = [];
                } else {
                    cleanedResults = predictions.map(function(obj) {
                        var rObj = {};
                        rObj['label'] = obj.description;
                        rObj['place_id'] = obj.place_id;
                        return rObj;
                    });
                    var likelyResult = cleanedResults[0];
                    $autocomplete.data({
                        label: likelyResult.label,
                        place_id: likelyResult.place_id
                    });
                    console.log($autocomplete.data());
                }
                response(cleanedResults);
            });
        }

        function selectPlace(place_id) {
            var geocoder = new google.maps.Geocoder();
            geocoder.geocode({'placeId': place_id}, function(results, status) {
                var result = results[0];
                var lat = result.geometry.location.lat()
                var lng = result.geometry.location.lng();
                redirectToMap(lat, lng)
            });
        }

        function redirectToMap(lat, lng) {
            window.location.href = getUrl(
                'browse',
                {
                    type: 'geo-latlng',
                    lat: lat,
                    lng: lng,
                    zoom: 14
                }
            );
            return;
        }

        // Enter Press
        $('.autocomplete-submit').on('click', function(e) {
            e.preventDefault();
            var place_id = $autocomplete.data().place_id;
            selectPlace(place_id);
        });

        // Button Press

        //
        // END AUTOCOMPLETE
        //
    });

    // Setup Response stuff
    var breakpointsArray = [
        0,
        breakpoints.mobile,
        breakpoints.tablet,
        breakpoints.desktop,
				breakpoints.desktopalt
    ];
    Response.create({ mode: 'markup', prefix: 'r', breakpoints: breakpointsArray });
    Response.create({ mode: 'src',  prefix: 'src', breakpoints: breakpointsArray });

    // Handlebars helpers

    /**
     * Helper for resolving static urls in handlebars templates
     * @param { URL to convert to static, same as argument for django static template function } url
     * @return { full static url }
     */
    Handlebars.registerHelper('static', function(url) {
        return CEL.serverVars.staticRoot + url;
    });

    /**
     * Helper for doing string equality
     * @param { first var to compare } a
     * @param { second var to compare } b
     * @return { boolean }
     */
    Handlebars.registerHelper('if_eq', function(a, b, opts) {
        if(a == b) {
            return opts.fn(this);
        } else {
            return opts.inverse(this);
        }
    });

    /**
     * Helper for doing string inequality
     * @param { first var to compare } a
     * @param { second var to compare } b
     * @return { boolean }
     */
    Handlebars.registerHelper('if_not_eq', function(a, b, opts) {
        if(a != b) {
            return opts.fn(this);
        } else {
            return opts.inverse(this);
        }
    });

    /**
     * Central api for getting urls for the app
     * @param { logical name of the endpoint } name
     * @param { Options for creating the url, depends on name } opts
     * @return { URL string for request }
     */
    var getUrl = function (name, opts) {
        var url = '';
        switch (name) {
            case 'origin':
                // IE < 9 doesn't define location.origin
                return window.location.origin ||
                    (window.location.protocol + "//" + window.location.host);
            case 'location-api':
                // requires opts.locations to be comma separated string or
                //      array of integers
                url = '/' + ($.cookie('django_language') || CEL.serverVars.default_language) +
                    '/api/location/';
                if (opts && opts.locations) {
                    url += opts.locations.toString() + '/';
                }
                return url;
            case 'neighborhood-api':
                return '/' + ($.cookie('django_language') || CEL.serverVars.default_language) +
                    '/api/neighborhood/';
            case 'neighborhoods-topo':
                return '/static/js/neighborhoods-topo.json';
            case 'neighborhoods-geojson':
                return '/static/js/neighborhoods.json';
            case 'browse':
                if (!opts) {
                    return '/search/';
                }
                switch (opts.type) {
                    case 'latlng':
                        url = '/search/?lat=' + opts.lat + '&lng=' + opts.lng;
                        if (opts.zoom) {
                            url += '&zoom=' + opts.zoom;
                        }
                        return url;
                    case 'geo-latlng':
                        url = '/search/?geolat=' + opts.lat + '&geolng=' + opts.lng;
                        if (opts.label) {
                            url += '&label=' + opts.label;
                        }
                        return url;
                    case 'neighborhood':
                        return '/search/?neighborhood=' + opts.neighborhood;
                    case 'location':
                        return '/search/?location=' + opts.location;
                    default:
                        break;
                }
                break;
            case 'single-location':
                url = '/location/' + opts.location + '/';
                if (opts.slug) {
                    url += opts.slug + '/';
                }
                return url;
            case 'favorites':
                url = '/favorites/';
                if (opts && opts.locations) {
                    url += opts.locations.toString() + '/';
                }
                return url;
            case 'icon-school':
                return '/static/img/leaflet-icons/school.png';
            case 'icon-school-accredited':
                return '/static/img/leaflet-icons/school-accredited.png';
            case 'icon-school-starred':
                return '/static/img/leaflet-icons/school-starred.png';
            case 'icon-school-accredited-starred':
                return '/static/img/leaflet-icons/school-accredited-starred.png';
            case 'icon-center':
                return '/static/img/leaflet-icons/center.png';
            case 'icon-center-accredited':
                return '/static/img/leaflet-icons/center-accredited.png';
            case 'icon-center-starred':
                return '/static/img/leaflet-icons/center-starred.png';
            case 'icon-center-accredited-starred':
                return '/static/img/leaflet-icons/center-accredited-starred.png';
            case 'icon-geolocation':
                return '/static/img/leaflet-icons/geocode.png';
            case 'icon-enrollment':
                return '/static/img/leaflet-icons/appsite.png';
            case 'icon-quality':
                if (opts && opts.quality) {
                    return '/static/img/icons/' + opts.quality + '.png';
                } else {
                    throw 'getUrl::Invalid Parameter: icon-quality requires opts.quality';
                }
            default:
                break;
        }
        throw 'Unknown URL endpoint';
    };

    var slugify = function(text) {
      return text.toString().toLowerCase()
        .replace(/\s+/g, '-')           // Replace spaces with -
        .replace(/[^\w\-]+/g, '')       // Remove all non-word chars
        .replace(/\-\-+/g, '-')         // Replace multiple - with single -
        .replace(/^-+/, '')             // Trim - from start of text
        .replace(/-+$/, '');            // Trim - from end of text
    };

    // geolocation
    //
    // Navbar Geolocation
    if ('geolocation' in navigator) {
        $(document).ready(function() {
            $('.geolocation-button').bind('click', function(e) {
                e.preventDefault();
                navigator.geolocation.getCurrentPosition(function(position) {
                    window.location.href = getUrl(
                        'browse',
                        {
                            type: 'geo-latlng',
                            lat: position.coords.latitude,
                            lng: position.coords.longitude
                        }
                    );
                }, function(e) {
                    e.preventDefault();
                    alert(gettext('Please enable geolocation services.'));
                });
            });
        });
    } else {
        $('.geolocation-button').hide();
    }

    // Homepage Geolocation
    $(document).ready(function() {
        $('#locate-main').bind('click', function(e) {
            e.preventDefault();
            navigator.geolocation.getCurrentPosition(function(position) {
                window.location.href = getUrl(
                    'browse',
                    {
                        type: 'geo-latlng',
                        lat: position.coords.latitude,
                        lng: position.coords.longitude
                    }
                );
            }, function(e) {
                e.preventDefault();
                alert(gettext('Please enable geolocation services.'));
            });
        });
    });

    // Set up social sharing behavior
    // The options argument must be an object that contains both a url and a title,
    // which are used to construct the appropriate sharing links.
    $('#share-modal').on('init-modal', function(e, options) {
        // a lot of these urls won't work when passing 'localhost' urls, but once we
        // move to a publically accessible url, they will work just fine
        var templates = {
                mail: 'mailto:?body={{ url }}&subject={{ title }}',
                facebook: 'http://facebook.com/sharer.php?s=100&p[url]={{ url }}&p[title]={{ title }}',
                twitter: 'https://twitter.com/share?text={{ title }}%20{{ url }}',

                // looks like google recently broke their permalink that allows adding text:
                // 'https://m.google.com/app/plus/x/?v=compose&content={{ title }}%20{{ url }}'
                // until they fix it, the following will work, but will just share the url
                gplus: 'https://plus.google.com/share?url={{ url }}'
            },
            $modal = $(this),
            $text = $('#social-text');

        // fill the input box in with the url
        $text.attr('href', options.url).text(options.url);

        // a url is generated for each template defined in 'templates'.
        // to add a new service, create a link with the id 'social-[service]',
        // and add a template to the 'templates' object.
        $.each(templates, function (service, template) {
            $('#social-' + service).attr('href', Handlebars.compile(template)(options));
        });

        // display the modal
        $modal.modal('show');
    });

    return {
        getUrl: getUrl,

        slugify: slugify,

        breakpoints: breakpoints,

        isTouchscreen: isTouchscreen,

        gaTrackEvent: gaTrackEvent,

        // Stolen from _.js v1.5.1
        // https://github.com/jashkenas/underscore/blob/dc5a3fa0133b7000c36ba76a413139c63f646789/underscore.js
        // See project root for license
        //
        // Returns a function, that, as long as it continues to be invoked, will not
        // be triggered. The function will be called after it stops being called for
        // N milliseconds. If `immediate` is passed, trigger the function on the
        // leading edge, instead of the trailing.
        debounce: function(func, wait, immediate) {
            var result,
                timeout = null;
            return function() {
                var context = this,
                    args = arguments,
                    later = function() {
                        timeout = null;
                        if (!immediate) result = func.apply(context, args);
                    },
                    callNow = immediate && !timeout;
                    clearTimeout(timeout);
                    timeout = setTimeout(later, wait);
                    if (callNow) {
                        result = func.apply(context, args);
                    }

                return result;
            };
        }
    };
});
<commit_msg>Add enter-handling for partial query<commit_after>/* Main app file for CEL project
 * Mostly stolen from requirejs examples: http://requirejs.org/docs/api.html
 * See http://requirejs.org/docs/api.html for details
 */

define(['jquery', 'Leaflet', '../lib/response', 'Handlebars', 'bootstrap',
        'jquery-ui', 'jquery-cookie', CEL.serverVars.gmapRequire],
function($, L, Response, Handlebars) {
    'use strict';

    var breakpoints = {
        mobile: 420,
        tablet: 767,
        desktop: 1024,
		desktopalt: 1140
    };

    var isTouchscreen = ('ontouchstart' in document.documentElement);

    // Hide the address bar on mobile browsers
    // Solution from: http://mobile.tutsplus.com/tutorials/mobile-web-apps/remove-address-bar/
    function hideAddressBar() {
        if(!window.location.hash) {
            if(document.height < window.outerHeight) {
                document.body.style.height = (window.outerHeight + 50) + 'px';
            }
            setTimeout(function() {
                window.scrollTo(0, 1);
            }, 50);
        }
    }
    if (isTouchscreen) {
        $(window).on('load', function() {
            if(!window.pageYOffset) {
                hideAddressBar();
            }
        }).on('orientationchange', hideAddressBar);
    }

    var gaTrackEvent = function($elt) {
        var category = CEL.serverVars.language + '/' + $elt.data('ga-category'),
            action = $elt.data('ga-action'),
            opt_label = $elt.data('ga-label'),
            opt_value = $elt.data('ga-value'),
            opt_noninteraction = $elt.data('ga-noninteraction'),
            data = ['_trackEvent', category, action];
        // optional parameters
        data.push(opt_label || undefined);
        data.push(opt_value || undefined);
        data.push(!!(opt_noninteraction));
        _gaq.push(data);
    };

    $(document).ready(function() {

        // Remove css tooltips when on a touchscreen device
        if (isTouchscreen) {
            $('[data-hint]').removeAttr('data-hint');
        }

        // set up ga tracking
        $('body').on('click', '*.ga-track', function(e) {
            gaTrackEvent($(this));
        });

        // AUTOCOMPLETE

        // New Autocomplete Testing:

        var $autocomplete = $('.autocomplete-searchbox');

        // JQuery $().autocomplete() function handles all interaction with
        // the input and composing the dropdown
        var autocomplete = $autocomplete.autocomplete({
            minLength: 1,
            source: function(request, response) {
                getAutocompletePlaces(request, response);
            },
            select: function(event, ui) {
                var place_id = ui.item.place_id;
                selectPlace(place_id);
            },
            focus: function(event, ui) {
                var selection = ui.item
                $autocomplete.data({
                    label: selection.label,
                    place_id: selection.place_id
                });
                console.log($autocomplete.data());
            }
        });

        // Calls the Places API to populate the dropdown
        // Returns array of possible matches

        function getAutocompletePlaces(request, response) {

            var input_term = 'Chicago IL ' + request.term;
            var service = new google.maps.places.AutocompleteService();
            service.getPlacePredictions({
                input: input_term,
                types: ['geocode'],
                componentRestrictions: {
                    country: 'us'
                }
            }, function(predictions, status) {
                var cleanedResults = [];
                if (predictions === null) {
                    cleanedResults = [];
                } else {
                    cleanedResults = predictions.map(function(obj) {
                        var rObj = {};
                        rObj['label'] = obj.description;
                        rObj['place_id'] = obj.place_id;
                        return rObj;
                    });
                    var likelyResult = cleanedResults[0];
                    $autocomplete.data({
                        label: likelyResult.label,
                        place_id: likelyResult.place_id
                    });
                    console.log($autocomplete.data());
                }
                response(cleanedResults);
            });
        }

        function selectPlace(place_id) {
            var geocoder = new google.maps.Geocoder();
            geocoder.geocode({'placeId': place_id}, function(results, status) {
                var result = results[0];
                var lat = result.geometry.location.lat()
                var lng = result.geometry.location.lng();
                redirectToMap(lat, lng)
            });
        }

        function redirectToMap(lat, lng) {
            window.location.href = getUrl(
                'browse',
                {
                    type: 'geo-latlng',
                    lat: lat,
                    lng: lng,
                    zoom: 14
                }
            );
            return;
        }

        // Button Press
        $('.autocomplete-submit').on('click', function(e) {
            e.preventDefault();
            var place_id = $autocomplete.data().place_id;
            selectPlace(place_id);
        });

        // Enter Press
        $autocomplete.keypress(function (e) {
            if (e.keyCode == 13) {
                var place_id = $autocomplete.data().place_id;
                selectPlace(place_id);
            }
        });

        //
        // END AUTOCOMPLETE
        //
    });

    // Setup Response stuff
    var breakpointsArray = [
        0,
        breakpoints.mobile,
        breakpoints.tablet,
        breakpoints.desktop,
				breakpoints.desktopalt
    ];
    Response.create({ mode: 'markup', prefix: 'r', breakpoints: breakpointsArray });
    Response.create({ mode: 'src',  prefix: 'src', breakpoints: breakpointsArray });

    // Handlebars helpers

    /**
     * Helper for resolving static urls in handlebars templates
     * @param { URL to convert to static, same as argument for django static template function } url
     * @return { full static url }
     */
    Handlebars.registerHelper('static', function(url) {
        return CEL.serverVars.staticRoot + url;
    });

    /**
     * Helper for doing string equality
     * @param { first var to compare } a
     * @param { second var to compare } b
     * @return { boolean }
     */
    Handlebars.registerHelper('if_eq', function(a, b, opts) {
        if(a == b) {
            return opts.fn(this);
        } else {
            return opts.inverse(this);
        }
    });

    /**
     * Helper for doing string inequality
     * @param { first var to compare } a
     * @param { second var to compare } b
     * @return { boolean }
     */
    Handlebars.registerHelper('if_not_eq', function(a, b, opts) {
        if(a != b) {
            return opts.fn(this);
        } else {
            return opts.inverse(this);
        }
    });

    /**
     * Central api for getting urls for the app
     * @param { logical name of the endpoint } name
     * @param { Options for creating the url, depends on name } opts
     * @return { URL string for request }
     */
    var getUrl = function (name, opts) {
        var url = '';
        switch (name) {
            case 'origin':
                // IE < 9 doesn't define location.origin
                return window.location.origin ||
                    (window.location.protocol + "//" + window.location.host);
            case 'location-api':
                // requires opts.locations to be comma separated string or
                //      array of integers
                url = '/' + ($.cookie('django_language') || CEL.serverVars.default_language) +
                    '/api/location/';
                if (opts && opts.locations) {
                    url += opts.locations.toString() + '/';
                }
                return url;
            case 'neighborhood-api':
                return '/' + ($.cookie('django_language') || CEL.serverVars.default_language) +
                    '/api/neighborhood/';
            case 'neighborhoods-topo':
                return '/static/js/neighborhoods-topo.json';
            case 'neighborhoods-geojson':
                return '/static/js/neighborhoods.json';
            case 'browse':
                if (!opts) {
                    return '/search/';
                }
                switch (opts.type) {
                    case 'latlng':
                        url = '/search/?lat=' + opts.lat + '&lng=' + opts.lng;
                        if (opts.zoom) {
                            url += '&zoom=' + opts.zoom;
                        }
                        return url;
                    case 'geo-latlng':
                        url = '/search/?geolat=' + opts.lat + '&geolng=' + opts.lng;
                        if (opts.label) {
                            url += '&label=' + opts.label;
                        }
                        return url;
                    case 'neighborhood':
                        return '/search/?neighborhood=' + opts.neighborhood;
                    case 'location':
                        return '/search/?location=' + opts.location;
                    default:
                        break;
                }
                break;
            case 'single-location':
                url = '/location/' + opts.location + '/';
                if (opts.slug) {
                    url += opts.slug + '/';
                }
                return url;
            case 'favorites':
                url = '/favorites/';
                if (opts && opts.locations) {
                    url += opts.locations.toString() + '/';
                }
                return url;
            case 'icon-school':
                return '/static/img/leaflet-icons/school.png';
            case 'icon-school-accredited':
                return '/static/img/leaflet-icons/school-accredited.png';
            case 'icon-school-starred':
                return '/static/img/leaflet-icons/school-starred.png';
            case 'icon-school-accredited-starred':
                return '/static/img/leaflet-icons/school-accredited-starred.png';
            case 'icon-center':
                return '/static/img/leaflet-icons/center.png';
            case 'icon-center-accredited':
                return '/static/img/leaflet-icons/center-accredited.png';
            case 'icon-center-starred':
                return '/static/img/leaflet-icons/center-starred.png';
            case 'icon-center-accredited-starred':
                return '/static/img/leaflet-icons/center-accredited-starred.png';
            case 'icon-geolocation':
                return '/static/img/leaflet-icons/geocode.png';
            case 'icon-enrollment':
                return '/static/img/leaflet-icons/appsite.png';
            case 'icon-quality':
                if (opts && opts.quality) {
                    return '/static/img/icons/' + opts.quality + '.png';
                } else {
                    throw 'getUrl::Invalid Parameter: icon-quality requires opts.quality';
                }
            default:
                break;
        }
        throw 'Unknown URL endpoint';
    };

    var slugify = function(text) {
      return text.toString().toLowerCase()
        .replace(/\s+/g, '-')           // Replace spaces with -
        .replace(/[^\w\-]+/g, '')       // Remove all non-word chars
        .replace(/\-\-+/g, '-')         // Replace multiple - with single -
        .replace(/^-+/, '')             // Trim - from start of text
        .replace(/-+$/, '');            // Trim - from end of text
    };

    // geolocation
    //
    // Navbar Geolocation
    if ('geolocation' in navigator) {
        $(document).ready(function() {
            $('.geolocation-button').bind('click', function(e) {
                e.preventDefault();
                navigator.geolocation.getCurrentPosition(function(position) {
                    window.location.href = getUrl(
                        'browse',
                        {
                            type: 'geo-latlng',
                            lat: position.coords.latitude,
                            lng: position.coords.longitude
                        }
                    );
                }, function(e) {
                    e.preventDefault();
                    alert(gettext('Please enable geolocation services.'));
                });
            });
        });
    } else {
        $('.geolocation-button').hide();
    }

    // Homepage Geolocation
    $(document).ready(function() {
        $('#locate-main').bind('click', function(e) {
            e.preventDefault();
            navigator.geolocation.getCurrentPosition(function(position) {
                window.location.href = getUrl(
                    'browse',
                    {
                        type: 'geo-latlng',
                        lat: position.coords.latitude,
                        lng: position.coords.longitude
                    }
                );
            }, function(e) {
                e.preventDefault();
                alert(gettext('Please enable geolocation services.'));
            });
        });
    });

    // Set up social sharing behavior
    // The options argument must be an object that contains both a url and a title,
    // which are used to construct the appropriate sharing links.
    $('#share-modal').on('init-modal', function(e, options) {
        // a lot of these urls won't work when passing 'localhost' urls, but once we
        // move to a publically accessible url, they will work just fine
        var templates = {
                mail: 'mailto:?body={{ url }}&subject={{ title }}',
                facebook: 'http://facebook.com/sharer.php?s=100&p[url]={{ url }}&p[title]={{ title }}',
                twitter: 'https://twitter.com/share?text={{ title }}%20{{ url }}',

                // looks like google recently broke their permalink that allows adding text:
                // 'https://m.google.com/app/plus/x/?v=compose&content={{ title }}%20{{ url }}'
                // until they fix it, the following will work, but will just share the url
                gplus: 'https://plus.google.com/share?url={{ url }}'
            },
            $modal = $(this),
            $text = $('#social-text');

        // fill the input box in with the url
        $text.attr('href', options.url).text(options.url);

        // a url is generated for each template defined in 'templates'.
        // to add a new service, create a link with the id 'social-[service]',
        // and add a template to the 'templates' object.
        $.each(templates, function (service, template) {
            $('#social-' + service).attr('href', Handlebars.compile(template)(options));
        });

        // display the modal
        $modal.modal('show');
    });

    return {
        getUrl: getUrl,

        slugify: slugify,

        breakpoints: breakpoints,

        isTouchscreen: isTouchscreen,

        gaTrackEvent: gaTrackEvent,

        // Stolen from _.js v1.5.1
        // https://github.com/jashkenas/underscore/blob/dc5a3fa0133b7000c36ba76a413139c63f646789/underscore.js
        // See project root for license
        //
        // Returns a function, that, as long as it continues to be invoked, will not
        // be triggered. The function will be called after it stops being called for
        // N milliseconds. If `immediate` is passed, trigger the function on the
        // leading edge, instead of the trailing.
        debounce: function(func, wait, immediate) {
            var result,
                timeout = null;
            return function() {
                var context = this,
                    args = arguments,
                    later = function() {
                        timeout = null;
                        if (!immediate) result = func.apply(context, args);
                    },
                    callNow = immediate && !timeout;
                    clearTimeout(timeout);
                    timeout = setTimeout(later, wait);
                    if (callNow) {
                        result = func.apply(context, args);
                    }

                return result;
            };
        }
    };
});
<|endoftext|>


<commit_before>/**
 * tags: Technically, it only holds all availiable tags that cml has.
*/
module.exports = {
  attrs: ['c-if', 'c-else', 'c-else-if', 'c-for', 'c-for-index', 'c-for-item', 'c-model', 'c-text', 'c-show', 'c-bind', 'c-catch', 'c-key', 'c-animation'],
  tags: [
    'template',
    'view',
    'text',
    'block',
    'scroller',
    'list',
    'cell',
    'image',
    'switch',
    'video',
    'input',
    'textarea',
    'richtext',
    'button',
    'radio',
    'checkbox',
    'page',
    'router-view',
    'slot',
    'aside',
    'col',
    'container',
    'foot',
    'head',
    'main',
    'row',
    'carousel',
    'carousel-item'
  ]
}
<commit_msg>平台组件白名单添加cover-view组件名<commit_after>/**
 * tags: Technically, it only holds all availiable tags that cml has.
*/
module.exports = {
  attrs: ['c-if', 'c-else', 'c-else-if', 'c-for', 'c-for-index', 'c-for-item', 'c-model', 'c-text', 'c-show', 'c-bind', 'c-catch', 'c-key', 'c-animation'],
  tags: [
    'template',
    'view',
    'text',
    'block',
    'scroller',
    'list',
    'cell',
    'image',
    'switch',
    'video',
    'input',
    'textarea',
    'richtext',
    'button',
    'radio',
    'checkbox',
    'page',
    'router-view',
    'cover-view',
    'slot',
    'aside',
    'col',
    'container',
    'foot',
    'head',
    'main',
    'row',
    'carousel',
    'carousel-item'
  ]
}
<|endoftext|>


<commit_before>/*
 * Copyright 2020 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useCallback, useEffect, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { CSSTransition } from 'react-transition-group';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { ReactComponent as PlayIcon } from '../../icons/play.svg';
import { ReactComponent as PauseIcon } from '../../icons/pause.svg';
import { useStory } from '../../app/story';
import StoryPropTypes from '../../types';

const PLAY_BUTTON_SIZE = 48;
const PLAY_ABOVE_BREAKPOINT_WIDTH = 108;
const PLAY_ABOVE_BREAKPOINT_HEIGHT = 120;

const Controls = styled.div`
  position: absolute;
  z-index: 2;
  left: ${({ x }) => `${x}px`};
  top: ${({ y }) => `${y}px`};
  width: ${({ width }) => `${width}px`};
  height: ${({ height }) => `${height}px`};
`;

const ButtonWrapper = styled.div.attrs({ role: 'button', tabIndex: -1 })`
  position: absolute;
  top: ${({ isAbove }) => (isAbove ? `-52px` : `50%`)};
  left: 50%;
  transform: translate(-50%, -50%);
  cursor: pointer;
  pointer-events: initial;
  width: ${PLAY_BUTTON_SIZE}px;
  height: ${PLAY_BUTTON_SIZE}px;

  opacity: ${({ isAbove }) => (isAbove ? 1 : 0)};
  &.button-enter {
    opacity: 0;
  }
  &.button-enter-active,
  &.button-enter-done {
    opacity: 1;
    transition: opacity 100ms;
  }
  &.button-exit {
    opacity: 1;
  }
  &.button-exit-active,
  &.button-exit-done {
    opacity: 0;
    transition: opacity 100ms;
  }
`;

const Play = styled(PlayIcon)`
  width: 100%;
  height: 100%;
  opacity: 0.84;
  filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.2));
`;
const Pause = styled(PauseIcon)`
  width: 100%;
  height: 100%;
  opacity: 0.84;
  filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.2));
`;

function VideoControls({
  box,
  isSelected,
  isSingleElement,
  isEditing,
  isTransforming,
  elementRef,
  element,
}) {
  const isPlayAbove =
    element.width < PLAY_ABOVE_BREAKPOINT_WIDTH ||
    element.height < PLAY_ABOVE_BREAKPOINT_HEIGHT;
  const [hovering, setHovering] = useState(false);
  const [showControls, setShowControls] = useState(true);
  const [isPlaying, setIsPlaying] = useState(!isTransforming);
  const { id } = element;
  const {
    state: { selectedElementIds },
  } = useStory();
  const isElementSelected = selectedElementIds.includes(id);
  const getVideoNode = useCallback(
    () => document.getElementById(`video-${id}`),
    [id]
  );

  useEffect(() => {
    const videoNode = getVideoNode();
    if (!isElementSelected || isEditing) {
      if (videoNode) {
        videoNode.pause();
        videoNode.currentTime = 0;
      }
      setIsPlaying(false);
      setShowControls(true);
    }
    const syncTimer = setTimeout(() => {
      if (isElementSelected && videoNode && !videoNode.paused) {
        setIsPlaying(true);
      }
    }, 0);
    return () => clearTimeout(syncTimer);
  }, [getVideoNode, id, isElementSelected, isEditing]);

  useEffect(() => {
    const videoNode = getVideoNode();
    if (videoNode && !videoNode.paused && isTransforming) {
      videoNode.pause();
      setIsPlaying(false);
    }
  }, [getVideoNode, isTransforming]);

  useEffect(() => {
    const videoNode = getVideoNode();
    if (!videoNode) {
      return undefined;
    }

    const onVideoEnd = () => {
      videoNode.currentTime = 0;
      setIsPlaying(false);
      setShowControls(true);
    };
    videoNode.addEventListener('ended', onVideoEnd);
    return () => videoNode.removeEventListener('ended', onVideoEnd);
  }, [getVideoNode, id]);

  const [checkShowControls] = useDebouncedCallback(() => {
    if (!isPlayAbove) {
      setShowControls(!isPlaying);
    }
  }, 2000);
  const [checkMouseInBBox] = useDebouncedCallback((evt) => {
    const node = elementRef.current;
    if (!node) {
      return;
    }
    const elementBBox = node.getBoundingClientRect();
    const isHovering =
      evt.clientX >= elementBBox.left &&
      evt.clientX <= elementBBox.right &&
      evt.clientY >= elementBBox.top &&
      evt.clientY <= elementBBox.bottom;
    setHovering(isHovering);
    setShowControls(true);
    checkShowControls();
  }, 10);

  useEffect(() => {
    document.addEventListener('pointermove', checkMouseInBBox);
    return () => {
      document.removeEventListener('pointermove', checkMouseInBBox);
    };
  }, [checkMouseInBBox]);

  if (!isSelected || isTransforming || !isSingleElement || isEditing) {
    return null;
  }

  const handlePlayPause = (evt) => {
    evt.stopPropagation();
    const videoNode = getVideoNode();
    if (!videoNode) {
      return;
    }

    if (isPlaying) {
      videoNode.pause();
      setIsPlaying(false);
      setShowControls(true);
    } else {
      const playPromise = videoNode.play();
      if (playPromise) {
        playPromise.then(() => setIsPlaying(true)).catch(() => {});
      }
    }
  };

  const buttonTitle = isPlaying
    ? __('Click to pause', 'web-stories')
    : __('Click to play', 'web-stories');
  const TransitionWrapper = isPlayAbove
    ? ({ children }) =>
{children}

    : CSSTransition;

  return (
    <Controls {...box}>
      {showControls && (
        <TransitionWrapper
          in={hovering}
          appear
          classNames="button"
          timeout={100}
        >
          <ButtonWrapper
            title={buttonTitle}
            aria-pressed={isPlaying}
            key="wrapper"
            onMouseDown={handlePlayPause}
            isAbove={isPlayAbove}
          >
            {isPlaying ? <Pause /> : <Play />}
          </ButtonWrapper>
        </TransitionWrapper>
      )}
    </Controls>
  );
}

VideoControls.propTypes = {
  box: StoryPropTypes.box.isRequired,
  isSelected: PropTypes.bool.isRequired,
  isSingleElement: PropTypes.bool.isRequired,
  isEditing: PropTypes.bool.isRequired,
  isTransforming: PropTypes.bool.isRequired,
  elementRef: PropTypes.object.isRequired,
  element: StoryPropTypes.element.isRequired,
};

export default VideoControls;
<commit_msg>Remove unnecessary isElementSelected<commit_after>/*
 * Copyright 2020 Google LLC
 *
 * 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
 *
 *     https://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.
 */

/**
 * External dependencies
 */
import PropTypes from 'prop-types';
import styled from 'styled-components';
import { useCallback, useEffect, useState } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { CSSTransition } from 'react-transition-group';

/**
 * WordPress dependencies
 */
import { __ } from '@wordpress/i18n';

/**
 * Internal dependencies
 */
import { ReactComponent as PlayIcon } from '../../icons/play.svg';
import { ReactComponent as PauseIcon } from '../../icons/pause.svg';
import StoryPropTypes from '../../types';

const PLAY_BUTTON_SIZE = 48;
const PLAY_ABOVE_BREAKPOINT_WIDTH = 108;
const PLAY_ABOVE_BREAKPOINT_HEIGHT = 120;

const Controls = styled.div`
  position: absolute;
  z-index: 2;
  left: ${({ x }) => `${x}px`};
  top: ${({ y }) => `${y}px`};
  width: ${({ width }) => `${width}px`};
  height: ${({ height }) => `${height}px`};
`;

const ButtonWrapper = styled.div.attrs({ role: 'button', tabIndex: -1 })`
  position: absolute;
  top: ${({ isAbove }) => (isAbove ? `-52px` : `50%`)};
  left: 50%;
  transform: translate(-50%, -50%);
  cursor: pointer;
  pointer-events: initial;
  width: ${PLAY_BUTTON_SIZE}px;
  height: ${PLAY_BUTTON_SIZE}px;

  opacity: ${({ isAbove }) => (isAbove ? 1 : 0)};
  &.button-enter {
    opacity: 0;
  }
  &.button-enter-active,
  &.button-enter-done {
    opacity: 1;
    transition: opacity 100ms;
  }
  &.button-exit {
    opacity: 1;
  }
  &.button-exit-active,
  &.button-exit-done {
    opacity: 0;
    transition: opacity 100ms;
  }
`;

const Play = styled(PlayIcon)`
  width: 100%;
  height: 100%;
  opacity: 0.84;
  filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.2));
`;
const Pause = styled(PauseIcon)`
  width: 100%;
  height: 100%;
  opacity: 0.84;
  filter: drop-shadow(0px 0px 10px rgba(0, 0, 0, 0.2));
`;

function VideoControls({
  box,
  isSelected,
  isSingleElement,
  isEditing,
  isTransforming,
  elementRef,
  element,
}) {
  const isPlayAbove =
    element.width < PLAY_ABOVE_BREAKPOINT_WIDTH ||
    element.height < PLAY_ABOVE_BREAKPOINT_HEIGHT;
  const [hovering, setHovering] = useState(false);
  const [showControls, setShowControls] = useState(true);
  const [isPlaying, setIsPlaying] = useState(!isTransforming);
  const { id } = element;
  const getVideoNode = useCallback(
    () => document.getElementById(`video-${id}`),
    [id]
  );

  useEffect(() => {
    const videoNode = getVideoNode();
    if (!isSelected || isEditing) {
      if (videoNode) {
        videoNode.pause();
        videoNode.currentTime = 0;
      }
      setIsPlaying(false);
      setShowControls(true);
    }
    const syncTimer = setTimeout(() => {
      if (isSelected && videoNode && !videoNode.paused) {
        setIsPlaying(true);
      }
    }, 0);
    return () => clearTimeout(syncTimer);
  }, [getVideoNode, id, isSelected, isEditing]);

  useEffect(() => {
    const videoNode = getVideoNode();
    if (videoNode && !videoNode.paused && isTransforming) {
      videoNode.pause();
      setIsPlaying(false);
    }
  }, [getVideoNode, isTransforming]);

  useEffect(() => {
    const videoNode = getVideoNode();
    if (!videoNode) {
      return undefined;
    }

    const onVideoEnd = () => {
      videoNode.currentTime = 0;
      setIsPlaying(false);
      setShowControls(true);
    };
    videoNode.addEventListener('ended', onVideoEnd);
    return () => videoNode.removeEventListener('ended', onVideoEnd);
  }, [getVideoNode, id]);

  const [checkShowControls] = useDebouncedCallback(() => {
    if (!isPlayAbove) {
      setShowControls(!isPlaying);
    }
  }, 2000);
  const [checkMouseInBBox] = useDebouncedCallback((evt) => {
    const node = elementRef.current;
    if (!node) {
      return;
    }
    const elementBBox = node.getBoundingClientRect();
    const isHovering =
      evt.clientX >= elementBBox.left &&
      evt.clientX <= elementBBox.right &&
      evt.clientY >= elementBBox.top &&
      evt.clientY <= elementBBox.bottom;
    setHovering(isHovering);
    setShowControls(true);
    checkShowControls();
  }, 10);

  useEffect(() => {
    document.addEventListener('pointermove', checkMouseInBBox);
    return () => {
      document.removeEventListener('pointermove', checkMouseInBBox);
    };
  }, [checkMouseInBBox]);

  if (!isSelected || isTransforming || !isSingleElement || isEditing) {
    return null;
  }

  const handlePlayPause = (evt) => {
    evt.stopPropagation();
    const videoNode = getVideoNode();
    if (!videoNode) {
      return;
    }

    if (isPlaying) {
      videoNode.pause();
      setIsPlaying(false);
      setShowControls(true);
    } else {
      const playPromise = videoNode.play();
      if (playPromise) {
        playPromise.then(() => setIsPlaying(true)).catch(() => {});
      }
    }
  };

  const buttonTitle = isPlaying
    ? __('Click to pause', 'web-stories')
    : __('Click to play', 'web-stories');
  const TransitionWrapper = isPlayAbove
    ? ({ children }) =>
{children}

    : CSSTransition;

  return (
    <Controls {...box}>
      {showControls && (
        <TransitionWrapper
          in={hovering}
          appear
          classNames="button"
          timeout={100}
        >
          <ButtonWrapper
            title={buttonTitle}
            aria-pressed={isPlaying}
            key="wrapper"
            onMouseDown={handlePlayPause}
            isAbove={isPlayAbove}
          >
            {isPlaying ? <Pause /> : <Play />}
          </ButtonWrapper>
        </TransitionWrapper>
      )}
    </Controls>
  );
}

VideoControls.propTypes = {
  box: StoryPropTypes.box.isRequired,
  isSelected: PropTypes.bool.isRequired,
  isSingleElement: PropTypes.bool.isRequired,
  isEditing: PropTypes.bool.isRequired,
  isTransforming: PropTypes.bool.isRequired,
  elementRef: PropTypes.object.isRequired,
  element: StoryPropTypes.element.isRequired,
};

export default VideoControls;
<|endoftext|>


<commit_before>import React from 'react'
import { ShellScreen, Type } from '@blockstack/ui'
import PropTypes from 'prop-types'

const SeedInitial = ({ next, loading, placeholder, ...rest }) => {
  const props = {
    title: {
      children: (
        <React.Fragment>Save your Secret Recovery Key</React.Fragment>
      )
    },
    content: {
      grow: 1,
      children: (
        <React.Fragment>
          <Type.p pt={2}>
            Your Secret Recovery Key is the most reliable way to access your
            Blockstack ID.
          </Type.p>
          <Type.p>
            Blockstack cannot reset your key. Save your key wherever you keep
            important, secret information.
          </Type.p>
        </React.Fragment>
      )
    },
    actions: {
      items: [
        {
          label: 'Record Secret Recovery Key',
          onClick: () => next(),
          primary: true,
          loading,
          disabled: loading,
          placeholder
        }
      ]
    }
  }
  return <ShellScreen {...rest} {...props} />
}

SeedInitial.propTypes = {
  next: PropTypes.func,
  loading: PropTypes.bool,
  placeholder: PropTypes.node
}

export default SeedInitial
<commit_msg>~~Record~~<commit_after>import React from 'react'
import { ShellScreen, Type } from '@blockstack/ui'
import PropTypes from 'prop-types'

const SeedInitial = ({ next, loading, placeholder, ...rest }) => {
  const props = {
    title: {
      children: (
        <React.Fragment>Save your Secret Recovery Key</React.Fragment>
      )
    },
    content: {
      grow: 1,
      children: (
        <React.Fragment>
          <Type.p pt={2}>
            Your Secret Recovery Key is the most reliable way to access your
            Blockstack ID.
          </Type.p>
          <Type.p>
            Blockstack cannot reset your key. Save your key wherever you keep
            important, secret information.
          </Type.p>
        </React.Fragment>
      )
    },
    actions: {
      items: [
        {
          label: 'Secret Recovery Key',
          onClick: () => next(),
          primary: true,
          loading,
          disabled: loading,
          placeholder
        }
      ]
    }
  }
  return <ShellScreen {...rest} {...props} />
}

SeedInitial.propTypes = {
  next: PropTypes.func,
  loading: PropTypes.bool,
  placeholder: PropTypes.node
}

export default SeedInitial
<|endoftext|>


<commit_before>ficsClient.factory("ActivityNotifier", ["$interval", function($interval) {
  return ActivityNotifier;

  function ActivityNotifier(data) {
    var interval;

    Object.defineProperty(this, "active", {
      get: function() {
        return data.active;
      },

      set: function(newVal) {
        if (newVal && data.activate) {
          data.activate();

          if (data.update) {
            interval = $interval(data.activate.bind(data), 60000);
          }
        } else if (!newVal && data.deactivate) {
          if (!_.isUndefined(interval)) {
            $interval.cancel(interval);
            interval = undefined;
          }

          data.deactivate();
        }

        data.active = newVal;
      }
    });

    this.notify = function() {
      return !data.active && data.notify && data.notify();
    };
  };
}]);
<commit_msg>split concerns from conditional<commit_after>ficsClient.factory("ActivityNotifier", ["$interval", function($interval) {
  return ActivityNotifier;

  function ActivityNotifier(data) {
    var interval;

    Object.defineProperty(this, "active", {
      get: function() {
        return data.active;
      },

      set: function(newVal) {
        if (newVal && data.activate) {
          data.activate();

          if (data.update) {
            interval = $interval(data.activate.bind(data), 60000);
          }
        } else if (!newVal) {
          if (!_.isUndefined(interval)) {
            $interval.cancel(interval);
            interval = undefined;
          }

          if (data.deactivate) {
            data.deactivate();
          }
        }

        data.active = newVal;
      }
    });

    this.notify = function() {
      return !data.active && data.notify && data.notify();
    };
  };
}]);
<|endoftext|>


<commit_before>(function () { "use strict";

var _ = require('lodash');
var util = require('util');
var assert = require('assert');

var buscomponent = require('./stbuscomponent.js');

function Chats () {
	Chats.super_.apply(this, arguments);
}

util.inherits(Chats, buscomponent.BusComponent);

Chats.prototype.getChats = buscomponent.provideQT('client-chat-get', function(query, ctx, cb) {
	var whereString = '';
	var params = [];

	if (!query.endpoints || !query.endpoints.length) {
		if (!query.chatid || parseInt(query.chatid) != query.chatid)
			return cb('format-error');

		whereString += ' chatid = ?';
		params.push(query.chatid);
	} else {
		if (query.chatid)
			return cb('format-error');

		var containsOwnChats = false;
		for (var i = 0; i < query.endpoints.length; ++i) {
			var uid = query.endpoints[i];
			containsOwnChats = containsOwnChats || (uid == ctx.user.id);
			if (parseInt(uid) != uid)
				return cb('format-error');
		}

		if (!containsOwnChats && ctx.user)
			query.endpoints.push(ctx.user.id);

		var endpointsList = query.endpoints.join(',');
		var numEndpoints = query.endpoints.length;

		whereString +=
			' (SELECT COUNT(*) FROM chatmembers AS cm JOIN users ON users.id = cm.userid WHERE cm.chatid = c.chatid ' +
			'AND cm.userid IN (' +  endpointsList + ')) = ? ';

		params.push(numEndpoints);
	}

	ctx.query('SELECT chatid, eventid AS chatstartevent FROM chats AS c ' +
		'LEFT JOIN events ON events.targetid = c.chatid AND events.type = "chat-start" '+
		'WHERE ' + whereString + ' ' +
		'ORDER BY (SELECT MAX(time) FROM events AS msgs WHERE msgs.type="comment" AND msgs.targetid = chatstartevent) DESC LIMIT 1', params, function(chatlist) {
		((chatlist.length == 0) ? function(cont) {
			if (query.failOnMissing)
				return cont(null);

			if (ctx.getProperty('readonly'))
				return cb('server-readonly');

			ctx.query('INSERT INTO chats(creator) VALUE(?)', [ctx.user.id], function(res) {
				var members = [];
				var memberValues = [];
				for (var i = 0; i < query.endpoints.length; ++i) {
					members.push('(?, ?, UNIX_TIMESTAMP())');
					memberValues.push(res.insertId);
					memberValues.push(String(query.endpoints[i]));
				}

				ctx.query('INSERT INTO chatmembers(chatid, userid, jointime) VALUES ' + members.join(','), memberValues, function() {
					ctx.feed({
						type: 'chat-start',
						targetid: res.insertId,
						srcuser: ctx.user.id,
						noFollowers: true,
						feedusers: query.endpoints,
						json: {endpoints: query.endpoints}
					}, function(eventid) {
						cont({chatid: res.insertId, eventid: eventid});
					});
				});
			});
		} : function(cont) {
			cont(chatlist[0]);
		})(function(chat) {
			if (chat === null)
				return cb('chat-get-notfound');

			assert.notStrictEqual(chat.chatid, null);
			assert.notStrictEqual(chat.eventid, null);

			chat.endpoints = query.endpoints;

			if (query.noMessages)
				return cb('chat-get-success', chat);

			ctx.query('SELECT u.name AS username, u.id AS uid, url AS profilepic ' +
				'FROM chatmembers AS cm ' +
				'JOIN users AS u ON u.id = cm.userid ' +
				'LEFT JOIN httpresources ON httpresources.user = cm.userid AND httpresources.role = "profile.image" ' +
				'WHERE cm.chatid = ?', [chat.chatid], function(endpoints) {
				assert.ok(endpoints.length > 0);
				chat.endpoints = endpoints;

				var ownChatsIsEndpoint = false;
				for (var i = 0; i < chat.endpoints.length; ++i) {
					if (chat.endpoints[i].uid == ctx.user.id) {
						ownChatsIsEndpoint = true;
						break;
					}
				}

				if (!ownChatsIsEndpoint)
					return cb('chat-get-notfound');

				ctx.query('SELECT c.*,u.name AS username,u.id AS uid, url AS profilepic, trustedhtml ' +
					'FROM ecomments AS c ' +
					'LEFT JOIN users AS u ON c.commenter = u.id ' +
					'LEFT JOIN httpresources ON httpresources.user = c.commenter AND httpresources.role = "profile.image" ' +
					'WHERE c.eventid = ?', [chat.chatstartevent], function(comments) {
					chat.messages = comments;
					cb('chat-get-success', {chat: chat});
				});
			});
		});
	});
});

Chats.prototype.addChatsToChats = buscomponent.provideWQT('client-chat-adduser', function(query, ctx, cb) {
	var self = this;

	if (parseInt(query.userid) != query.userid || parseInt(query.chatid) != query.chatid)
		return cb('format-error');

	ctx.query('SELECT name FROM users WHERE id = ?', [query.userid], function(res) {
		if (res.length == 0)
			return cb('chat-adduser-user-notfound');

		assert.equal(res.length, 1);
		var username = res[0].name;

		self.getChats({
			chatid: query.chatid,
			failOnMissing: true
		}, ctx, function(status, chat) {
			switch (status) {
				case 'chat-get-notfound':
					return cb('chat-adduser-chat-notfound');
				case 'chat-get-success':
					break;
				default:
					return cb(status); // assume other error
			}

			ctx.query('INSERT INTO chatmembers (chatid, userid) VALUES (?, ?)', [query.chatid, query.userid], function(r) {
				var feedusers = _.pluck(chat.endpoints, 'uid');
				feedusers.push(query.userid);

				ctx.feed({
					type: 'chat-user-added',
					targetid: query.chatid,
					srcuser: ctx.user.id,
					noFollowers: true,
					feedusers: chat.endpoints,
					json: {addedChats: query.userid, addedChatsName: username, endpoints: chat.endpoints}
				});

				cb('chat-adduser-success');
			});
		});
	});
});

Chats.prototype.listAllChatss = buscomponent.provideQT('client-list-all-chats', function(query, ctx, cb) {
	ctx.query('SELECT c.chatid, c.creator, creator_u.name AS creatorname, u.id AS member, u.name AS membername, url AS profilepic, ' +
		'eventid AS chatstartevent ' +
		'FROM chatmembers AS cmi ' +
		'JOIN chats AS c ON c.chatid = cmi.chatid ' +
		'JOIN chatmembers AS cm ON cm.chatid = c.chatid ' +
		'JOIN users AS u ON cm.userid = u.id ' +
		'LEFT JOIN httpresources ON httpresources.user = u.id AND httpresources.role = "profile.image" ' +
		'JOIN users AS creator_u ON c.creator = creator_u.id ' +
		'JOIN events ON events.targetid = c.chatid AND events.type = "chat-start" ' +
		'WHERE cmi.userid = ?', [ctx.user.id], function(res) {
		var ret = {};

		for (var i = 0; i < res.length; ++i) {
			if (!ret[res[i].chatid]) {
				ret[res[i].chatid] = _.pick(res[i], 'chatid', 'creator', 'creatorname', 'chatstartevent');
				ret[res[i].chatid].members = [];
			}

			ret[res[i].chatid].members.push({id: res[i].member, name: res[i].membername, profilepic: res[i].profilepic});
		}

		cb('list-all-chats-success', {chats: ret});
	});
});

exports.Chats = Chats;

})();
<commit_msg><commit_after>(function () { "use strict";

var _ = require('lodash');
var util = require('util');
var assert = require('assert');

var buscomponent = require('./stbuscomponent.js');

/**
 * Provides methods for in-game chat messaging.
 *
 * @public
 * @module chats
 */

/**
 * Main object of the {@link module:chats} module
 *
 * @public
 * @constructor module:chats~Chats
 * @augments module:stbuscomponent~STBusComponent
 */
function Chats () {
	Chats.super_.apply(this, arguments);
}

util.inherits(Chats, buscomponent.BusComponent);

/**
 * Represents an ingame chat session.
 *
 * @typedef module:chats~Chat
 * @type object
 *
 * @property {int} chatid  A numerical id for this chat.
 * @property {int} chatstartevent  The chat event id. Use this for messaging (i.e.,
 *                                 commenting on this event).
 * @property {int[]} endpoints  The numerical ids of all chat participants.
 * @property {Comment[]} messages  A list of chat messages.
 */

/**
 * Fetch or create a specific chat.
 *
 * @param {?object[]} query.endpoints  Array of small user objects of the
 *                                     other participants in the chat.
 * @param {?module:user~UserEntryBase[]} query.members  Array of full user objects for the
 *                                                      participants.
 * @param {?int} query.chatid  The numerical chat id. Either this or query.endpoints
 *                             need to be set.
 * @param {?boolean} query.failOnMissing  If there is no such chat, do not create it.
 * @param {?boolean} query.noMessages  If set, do not load the chat messages.
 *
 * @return {object} Returns with chat-get-notfound,
 *                  chat-get-success or a common error code.
 *                  and populates .chat with a
 *                  {@link module:chats~Chat}
 *
 * @function c2s~get-chat
 */
Chats.prototype.getChats = buscomponent.provideQT('client-chat-get', function(query, ctx, cb) {
	var whereString = '';
	var params = [];

	if (!query.endpoints || !query.endpoints.length) {
		if (!query.chatid || parseInt(query.chatid) != query.chatid)
			return cb('format-error');

		whereString += ' chatid = ?';
		params.push(query.chatid);
	} else {
		if (query.chatid)
			return cb('format-error');

		var containsOwnChats = false;
		for (var i = 0; i < query.endpoints.length; ++i) {
			var uid = query.endpoints[i];
			containsOwnChats = containsOwnChats || (uid == ctx.user.id);
			if (parseInt(uid) != uid)
				return cb('format-error');
		}

		if (!containsOwnChats && ctx.user)
			query.endpoints.push(ctx.user.id);

		var endpointsList = query.endpoints.join(',');
		var numEndpoints = query.endpoints.length;

		whereString +=
			' (SELECT COUNT(*) FROM chatmembers AS cm JOIN users ON users.id = cm.userid WHERE cm.chatid = c.chatid ' +
			'AND cm.userid IN (' +  endpointsList + ')) = ? ';

		params.push(numEndpoints);
	}

	ctx.query('SELECT chatid, eventid AS chatstartevent FROM chats AS c ' +
		'LEFT JOIN events ON events.targetid = c.chatid AND events.type = "chat-start" '+
		'WHERE ' + whereString + ' ' +
		'ORDER BY (SELECT MAX(time) FROM events AS msgs WHERE msgs.type="comment" AND msgs.targetid = chatstartevent) DESC ' +
		'LIMIT 1', params, function(chatlist) {
		((chatlist.length == 0) ? function(cont) {
			if (query.failOnMissing || !query.endpoints)
				return cont(null);

			if (ctx.getProperty('readonly'))
				return cb('server-readonly');

			ctx.query('SELECT COUNT(*) AS c FROM users WHERE id IN (' + query.endpoints.join(',') + ')',
				[] , function(endpointUserCount) {
				if (endpointUserCount[0].c != query.endpoints.length)
					return cont(null);

			ctx.query('INSERT INTO chats(creator) VALUE(?)', [ctx.user.id], function(res) {
				var members = [];
				var memberValues = [];
				for (var i = 0; i < query.endpoints.length; ++i) {
					members.push('(?, ?, UNIX_TIMESTAMP())');
					memberValues.push(res.insertId);
					memberValues.push(String(query.endpoints[i]));
				}

				ctx.query('INSERT INTO chatmembers(chatid, userid, jointime) VALUES ' + members.join(','), memberValues, function() {
					ctx.feed({
						type: 'chat-start',
						targetid: res.insertId,
						srcuser: ctx.user.id,
						noFollowers: true,
						feedusers: query.endpoints,
						json: {endpoints: query.endpoints}
					}, function(eventid) {
						cont({chatid: res.insertId, eventid: eventid});
					});
				});
			});
			});
		} : function(cont) {
			cont(chatlist[0]);
		})(function(chat) {
			if (chat === null)
				return cb('chat-get-notfound');

			assert.notStrictEqual(chat.chatid, null);
			assert.notStrictEqual(chat.eventid, null);

			chat.endpoints = query.endpoints;

			if (query.noMessages)
				return cb('chat-get-success', chat);

			ctx.query('SELECT u.name AS username, u.id AS uid, url AS profilepic ' +
				'FROM chatmembers AS cm ' +
				'JOIN users AS u ON u.id = cm.userid ' +
				'LEFT JOIN httpresources ON httpresources.user = cm.userid AND httpresources.role = "profile.image" ' +
				'WHERE cm.chatid = ?', [chat.chatid], function(endpoints) {
				assert.ok(endpoints.length > 0);
				chat.endpoints = endpoints;

				var ownChatsIsEndpoint = false;
				for (var i = 0; i < chat.endpoints.length; ++i) {
					if (chat.endpoints[i].uid == ctx.user.id) {
						ownChatsIsEndpoint = true;
						break;
					}
				}

				if (!ownChatsIsEndpoint)
					return cb('chat-get-notfound');

				ctx.query('SELECT c.*,u.name AS username,u.id AS uid, url AS profilepic, trustedhtml ' +
					'FROM ecomments AS c ' +
					'LEFT JOIN users AS u ON c.commenter = u.id ' +
					'LEFT JOIN httpresources ON httpresources.user = c.commenter AND httpresources.role = "profile.image" ' +
					'WHERE c.eventid = ?', [chat.chatstartevent], function(comments) {
					chat.messages = comments;
					cb('chat-get-success', {chat: chat});
				});
			});
		});
	});
});

/**
 * Add a user to a specific chat.
 *
 * @param {int} userid  The numerical id of the user to be added.
 * @param {int} chatid  The numerical id of the target chat.
 *
 * @return {object} Returns with chat-adduser-notfound,
 *                  chat-adduser-success or a common error code.
 *
 * @function c2s~chat-adduser
 */
Chats.prototype.addChatsToChats = buscomponent.provideWQT('client-chat-adduser', function(query, ctx, cb) {
	var self = this;

	if (parseInt(query.userid) != query.userid || parseInt(query.chatid) != query.chatid)
		return cb('format-error');

	ctx.query('SELECT name FROM users WHERE id = ?', [query.userid], function(res) {
		if (res.length == 0)
			return cb('chat-adduser-user-notfound');

		assert.equal(res.length, 1);
		var username = res[0].name;

		self.getChats({
			chatid: query.chatid,
			failOnMissing: true
		}, ctx, function(status, chat) {
			switch (status) {
				case 'chat-get-notfound':
					return cb('chat-adduser-chat-notfound');
				case 'chat-get-success':
					break;
				default:
					return cb(status); // assume other error
			}

			ctx.query('INSERT INTO chatmembers (chatid, userid) VALUES (?, ?)', [query.chatid, query.userid], function(r) {
				var feedusers = _.pluck(chat.endpoints, 'uid');
				feedusers.push(query.userid);

				ctx.feed({
					type: 'chat-user-added',
					targetid: query.chatid,
					srcuser: ctx.user.id,
					noFollowers: true,
					feedusers: chat.endpoints,
					json: {addedChats: query.userid, addedChatsName: username, endpoints: chat.endpoints}
				});

				cb('chat-adduser-success');
			});
		});
	});
});

/**
 * List all chats for the current user.
 *
 * @return {object} Returns with list-all-chats-success
 *                  or a common error code and populates .chats
 *                  with an {@link module:chats~Chat}
 *
 * @function c2s~list-all-chats
 */
Chats.prototype.listAllChats = buscomponent.provideQT('client-list-all-chats', function(query, ctx, cb) {
	ctx.query('SELECT c.chatid, c.creator, creator_u.name AS creatorname, u.id AS member, u.name AS membername, url AS profilepic, ' +
		'eventid AS chatstartevent ' +
		'FROM chatmembers AS cmi ' +
		'JOIN chats AS c ON c.chatid = cmi.chatid ' +
		'JOIN chatmembers AS cm ON cm.chatid = c.chatid ' +
		'JOIN users AS u ON cm.userid = u.id ' +
		'LEFT JOIN httpresources ON httpresources.user = u.id AND httpresources.role = "profile.image" ' +
		'JOIN users AS creator_u ON c.creator = creator_u.id ' +
		'JOIN events ON events.targetid = c.chatid AND events.type = "chat-start" ' +
		'WHERE cmi.userid = ?', [ctx.user.id], function(res) {
		var ret = {};

		for (var i = 0; i < res.length; ++i) {
			if (!ret[res[i].chatid]) {
				ret[res[i].chatid] = _.pick(res[i], 'chatid', 'creator', 'creatorname', 'chatstartevent');
				ret[res[i].chatid].members = [];
			}

			ret[res[i].chatid].members.push({id: res[i].member, name: res[i].membername, profilepic: res[i].profilepic});
		}

		cb('list-all-chats-success', {chats: ret});
	});
});

exports.Chats = Chats;

})();
<|endoftext|>