<commit_before>/*global gladius, console */

document.addEventListener( "DOMContentLoaded", function( e ){

    var printd = function( div, str ) {
        document.getElementById( div ).innerHTML = str + '<p>';
    };
    var cleard = function( div ) {
        document.getElementById( div ).innerHTML = '';
    };

    var canvas = document.getElementById( "test-canvas" );
    var resources = {};

    var game = function( engine ) {
        var math = engine.math;

        var CubicVR = engine.graphics.target.context;

        var MotionService = engine.base.Service({
            type: 'Motion',
            schedule: {
                update: {
                    phase: engine.scheduler.phases.UPDATE
                }
            },
            time: engine.scheduler.simulationTime
        },
        function( options ) {
            var that = this;
            var service = this;
            options = options || {};

            this.update = function() {
                var updateEvent = new engine.core.Event({
                    type: 'Update',
                    queue: false,
                    data: {
                        delta: that.time.delta
                    }
                });
                for( var componentType in that.components ) {
                    for( var entityId in that.components[componentType] ) {
                        var component = that.components[componentType][entityId];
                        while( component.handleQueuedEvent() ) {}
                        updateEvent.dispatch( component );
                    }
                }
            };

            var Planar = engine.base.Component({
                type: 'Motion',
                depends: ['Transform']
            },
            function( options ) {
                var that = this;
                var _directions = {
                    UP: options.forward || math.Vector3( 0, 1, 0 ),
                    LEFT: options.right || math.Vector3( -1, 0, 0 ),
                    RIGHT: options.reverse || math.Vector3( 1, 0, 0 ),
                    DOWN: options.left || math.Vector3( 0, -1, 0 )
                };
                var _move = null;
                this.lastPosition = null;
                this.velocity = null;

                this.onMoveStart = function( event ) {
                    _move = event.data.direction;
                };

                this.onMoveStop = function( event ) {
                    _move = null;
                };

                this.onUpdate = function( event ) {
                    var transform = this.owner.find( 'Transform' );

                    if( _move ) {
                        var direction = math.vector3.multiply( _directions[_move], event.data.delta / 1000 );
                        transform.position = math.vector3.add( transform.position, direction );
                    }

                    that.velocity = math.vector2.subtract( transform.position, that.lastPosition );
                    that.lastPosition = transform.position;
                };

                // Boilerplate component registration; Lets our service know that we exist and want to do things
                this.onComponentOwnerChanged = function( e ){
                    if( e.data.previous === null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( this.owner === null && e.data.previous !== null ) {
                        service.unregisterComponent( e.data.previous.id, this );
                    }
                    that.lastPosition = this.owner.find( 'Transform' ).position;
                };

                this.onEntityManagerChanged = function( e ) {
                    if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                        service.unregisterComponent( this.owner.id, this );
                    }
                };
            });

            var _components = {
                Planar: Planar
            };
            Object.defineProperty( this, 'component', {
                get: function() {
                    return _components;
                }
            });
        });
        engine.motion = new MotionService();

        var CollisionService = engine.base.Service({
            type: 'Physics',
            schedule: {
                update: {
                    phase: engine.scheduler.phases.UPDATE
                }
            },
            depends: [ 'Motion' ],
            time: engine.scheduler.simulationTime
        },
        function( options ) {

            var that = this;
            var service = this;

            var AABB = engine.base.Component({
                type: 'Collision',
                depends: ['Transform']
            },
            function( options ) {
                this.halfWidth = options.halfWidth;
                this.halfHeight = options.halfHeight;

                // Boilerplate component registration; Lets our service know that we exist and want to do things
                this.onComponentOwnerChanged = function( e ){
                    if( e.data.previous === null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( this.owner === null && e.data.previous !== null ) {
                        service.unregisterComponent( e.data.previous.id, this );
                    }
                };

                this.onEntityManagerChanged = function( e ) {
                    if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                        service.unregisterComponent( this.owner.id, this );
                    }
                };
            });

            this.update = function() {

                for( var componentType in that.components ) {
                    for( var entityId in that.components[componentType] ) {
                        while( that.components[componentType][entityId].handleQueuedEvent() ) {}
                    }
                }

                var doCollision = function( component1, component2 ) {
                    var transform1 = component1.owner.find( 'Transform' ),
                        transform2 = component2.owner.find( 'Transform' );
                    var halfWidth1 = component1.halfWidth,
                        halfHeight1 = component1.halfHeight,
                        center1 = transform1.position;
                    var halfWidth2 = component2.halfWidth,
                        halfHeight2 = component2.halfHeight,
                        center2 = transform2.position;

                    var T = math.vector2.subtract( center2, center1 );  // vector from center of box1 to center of box2

                    // test x-axis
                    var proj_T_x = math.vector2.length( math.vector2.project( T, math.vector2.x ) );
                    var proj_AABB_x = halfWidth1 +
                                      math.vector2.length( math.vector2.project( math.vector2.multiply( math.vector2.x, halfWidth2 ), math.vector2.x ) ) +
                                      math.vector2.length( math.vector2.project( math.vector2.multiply( math.vector2.y, halfHeight2 ), math.vector2.x ) );
                    if( proj_T_x > proj_AABB_x ) return false;

                    // test y-axis
                    var proj_T_y = math.vector2.length( math.vector2.project( T, math.vector2.y ) );
                    var proj_AABB_y = halfHeight1 +
                                      math.vector2.length( math.vector2.project( math.vector2.multiply( math.vector2.x, halfWidth2 ), math.vector2.y ) ) +
                                      math.vector2.length( math.vector2.project( math.vector2.multiply( math.vector2.y, halfHeight2 ), math.vector2.y ) );
                    if( proj_T_y > proj_AABB_y ) return false;

                    // Resolve collision
                    var component1_velocity = component1.owner.find( 'Motion' ) ? component1.owner.find( 'Motion' ).velocity : math.Vector2( 0, 0 ),
                        component2_velocity = component2.owner.find( 'Motion' ) ? component2.owner.find( 'Motion' ).velocity : math.Vector2( 0, 0 );

                    var x_intersection = proj_AABB_x - proj_T_x;
                    var y_intersection = proj_AABB_y - proj_T_y;

                    var sum_distance_x = Math.abs( component1_velocity[0] ) + Math.abs( component2_velocity[0] );
                    var sum_distance_y = Math.abs( component1_velocity[1] ) + Math.abs( component2_velocity[1] );

                    if( !math.vector2.equal( component1_velocity, math.vector2.zero ) ) {
                        var component1_move_x_amount = sum_distance_x ? Math.abs(component1_velocity[0]/sum_distance_x) * x_intersection + 0.01 : 0;
                        var component1_move_x = math.vector2.normalize( component1_velocity )[0] * -1 * component1_move_x_amount;

                        var component1_move_y_amount = sum_distance_y ? Math.abs(component1_velocity[1]/sum_distance_y) * y_intersection + 0.01 : 0;
                        var component1_move_y = math.vector2.normalize( component1_velocity )[1] * -1 * component1_move_y_amount;

                        var component1_move = math.Vector3( component1_move_x, component1_move_y, 0 );

                        transform1.position = math.vector3.add(
                                transform1.position,
                                component1_move
                                );
                        var motion1 = component1.owner.find( 'Motion' );
                        if( motion1 ) {
                            motion1.lastPosition = math.Vector2( transform1.position );
                            motion1.velocity = math.vector2.add( motion1.velocity, component1_move );
                        }
                    }

                    if( !math.vector2.equal( component2_velocity, math.vector2.zero ) ) {
                        var component2_move_x_amount = sum_distance_x ? Math.abs(component2_velocity[0]/sum_distance_x) * x_intersection + 0.01 : 0;
                        var component2_move_x = math.vector2.normalize( component2_velocity )[0] * -1 * component2_move_x_amount;

                        var component2_move_y_amount = sum_distance_y ? Math.abs(component2_velocity[1]/sum_distance_y) * y_intersection + 0.01 : 0;
                        var component2_move_y = math.vector2.normalize( component2_velocity )[1] * -1 * component2_move_y_amount;

                        var component2_move = math.Vector3( component2_move_x, component2_move_y, 0 );

                        transform2.position = math.vector3.add(
                                transform2.position,
                                component2_move
                                );
                        var motion2 = component2.owner.find( 'Motion' );
                        if( motion2 ) {
                            motion2.lastPosition = math.Vector2( transform2.position );
                            motion2.velocity = math.vector2.zero;
                        }
                    }

                    // TD: decide what extra data is useful to report about the collision
                    return {};
                };

                // Build a list of components to check
                var collisionComponents = [];
                for( var collisionEntity in that.components.Collision ) {
                    collisionComponents.push( that.components.Collision[collisionEntity] );
                }

                // Test each component against each other component
                var detectCollision = function( component2 ) {
                    var collisionData = doCollision( component1, component2 );
                    if ( collisionData ) {
                        // Dispatch events to each entity naming the other entity as the target
                        new engine.core.Event({
                            type: 'Collision',
                            data: engine.lang.extend( { entity: component2.owner }, collisionData )
                        }).dispatch( component1.owner );
                        new engine.core.Event({
                            type: 'Collision',
                            data: engine.lang.extend( { entity: component1.owner }, collisionData )
                        }).dispatch( component2.owner );
                    }
                };
                while( collisionComponents.length > 0 ) {
                    var component1 = collisionComponents.shift();
                    collisionComponents.forEach( detectCollision );
                }

            };

            var _components = {
                    AABB: AABB
            };

            Object.defineProperty( this, 'component', {
                get: function() {
                    return _components;
                }
            });
        });
        engine.collision = new CollisionService();

        var PlayerComponent = engine.base.Component({
            type: 'Player',
            depends: ['Transform']    // We're going to be moving stuff
        },
        function( options ) {

            options = options || {};
            var that = this;

            // This is a hack so that this component will have its message
            // queue processed
            var service = engine.logic;

            this.onCollision = function( event ) {
                // console.log( that.owner.id, '<->', event.data.entity.id );
            };

            this.onUpdate = function( event ) {
            };

            // Boilerplate component registration; Lets our service know that we exist and want to do things
            this.onComponentOwnerChanged = function( e ){
                if( e.data.previous === null && this.owner !== null ) {
                    service.registerComponent( this.owner.id, this );
                }

                if( this.owner === null && e.data.previous !== null ) {
                    service.unregisterComponent( e.data.previous.id, this );
                }
            };

            this.onEntityManagerChanged = function( e ) {
                if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                    service.registerComponent( this.owner.id, this );
                }

                if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                    service.unregisterComponent( this.owner.id, this );
                }
            };

        });

        var run = function() {

            // Make a new space for our entities
            var space = new engine.core.Space();

            canvas = engine.graphics.target.element;

            // Make an obstacle that will collide with the player
            var obstacle = new space.Entity({
                name: 'obstacle',
                components: [
                             new engine.core.component.Transform(),
                             new engine.graphics.component.Model({
                                 mesh: resources.mesh,
                                 material: resources.material
                             }),
                             new engine.collision.component.AABB({
                                 halfWidth: 1,
                                 halfHeight: 1
                             })
                             ]
            });

            // Make a user-controllable object that can collide with the obstacle
            var player = new space.Entity({
                name: 'player',
                components: [
                             new engine.core.component.Transform({
                                 position: math.Vector3( 3, 1.5, 0 ),
                                 rotation: math.Vector3( 0, 0, 0 )
                             }),
                             new engine.graphics.component.Model({
                                 mesh: resources.mesh,
                                 material: resources.material
                             }),
                             new engine.input.component.Controller({
                                 onKey: function(e) {
                                     if( this.owner ) {
                                         // If we have an owner, dispatch a game event for it to enjoy
                                         switch( e.data.code ) {
                                         case 'W':
                                             new engine.core.Event({
                                                 type: e.data.state === 'down' ? 'MoveStart' : 'MoveStop',
                                                         data: {
                                                             direction: 'UP'
                                                         }
                                             }).dispatch( this.owner );
                                             break;
                                         case 'A':
                                             new engine.core.Event({
                                                 type: e.data.state === 'down' ? 'MoveStart' : 'MoveStop',
                                                         data: {
                                                             direction: 'LEFT'
                                                         }
                                             }).dispatch( this.owner );
                                             break;
                                         case 'S':
                                             new engine.core.Event({
                                                 type: e.data.state === 'down' ? 'MoveStart' : 'MoveStop',
                                                         data: {
                                                             direction: 'DOWN'
                                                         }
                                             }).dispatch( this.owner );
                                             break;
                                         case 'D':
                                             new engine.core.Event({
                                                 type: e.data.state === 'down' ? 'MoveStart' : 'MoveStop',
                                                         data: {
                                                             direction: 'RIGHT'
                                                         }
                                             }).dispatch( this.owner );
                                             break;

                                         }
                                     }
                                 }
                             }),
                             new engine.motion.component.Planar(),
                             new engine.collision.component.AABB({
                                 halfWidth: 1,
                                 halfHeight: 1
                             }),
                             new PlayerComponent()
                             ]
            });

            var camera = new space.Entity({
                name: 'camera',
                components: [
                             new engine.core.component.Transform({
                                 position: math.Vector3( 0, 2, 10 )
                             }),
                             new engine.graphics.component.Camera({
                                 active: true,
                                 width: canvas.width,
                                 height: canvas.height,
                                 fov: 60
                             }),
                             new engine.graphics.component.Light({ intensity: 50 })
                             ]
            });
            camera.find( 'Camera' ).target = obstacle.find( 'Transform' ).position;

            // Start the engine!
            engine.run();

        };

        engine.core.resource.get(
                [
                 {
                     type: engine.graphics.resource.Mesh,
                     url: 'procedural-mesh.js?size=2',
                     load: engine.core.resource.proceduralLoad,
                     onsuccess: function( mesh ) {
                         resources.mesh = mesh;
                     },
                     onfailure: function( error ) {
                     }
                 },
                 {
                     type: engine.graphics.resource.Material,
                     url: 'procedural-material.js',
                     load: engine.core.resource.proceduralLoad,
                     onsuccess: function( material ) {
                         resources.material = material;
                     },
                     onfailure: function( error ) {
                     }
                 }],
                 {
                    oncomplete: run
                 }
        );

    };

    gladius.create(
            {
                debug: true,
                services: {
                    graphics: {
                        src: 'graphics/service',
                        options: {
                            canvas: canvas
                        }
                    },
                    input: {
                        src: 'input/service',
                        options: {

                        }
                    },
                    logic: 'logic/game/service'
                }
            },
            game
    );

});
<commit_msg>Removed custom collision, will add box2d to replace.<commit_after>/*global gladius, console */

document.addEventListener( "DOMContentLoaded", function( e ){

    var printd = function( div, str ) {
        document.getElementById( div ).innerHTML = str + '<p>';
    };
    var cleard = function( div ) {
        document.getElementById( div ).innerHTML = '';
    };

    var canvas = document.getElementById( "test-canvas" );
    var resources = {};

    var game = function( engine ) {
        var math = engine.math;

        var CubicVR = engine.graphics.target.context;

        var MotionService = engine.base.Service({
            type: 'Motion',
            schedule: {
                update: {
                    phase: engine.scheduler.phases.UPDATE
                }
            },
            time: engine.scheduler.simulationTime
        },
        function( options ) {
            var that = this;
            var service = this;
            options = options || {};

            this.update = function() {
                var updateEvent = new engine.core.Event({
                    type: 'Update',
                    queue: false,
                    data: {
                        delta: that.time.delta
                    }
                });
                for( var componentType in that.components ) {
                    for( var entityId in that.components[componentType] ) {
                        var component = that.components[componentType][entityId];
                        while( component.handleQueuedEvent() ) {}
                        updateEvent.dispatch( component );
                    }
                }
            };

            var AutoPlanar = engine.base.Component({
                type: 'Motion',
                depends: ['Transform']
            },
            function( options ) {
                var that = this;
                var speed = options.speed || 1;
                var direction = math.Vector3( [1, 0, 0] );
                var displacement = 0;
                var maxDisplacement = 3;

                this.onUpdate = function( event ) {
                    var transform = this.owner.find( 'Transform' );

                    var iVelocity = math.vector3.multiply( direction, speed * event.data.delta / 1000 );
                    transform.position = math.vector3.add( transform.position, iVelocity );
                    displacement += iVelocity[0];
                    if( Math.abs( displacement ) > maxDisplacement ) {
                        direction = math.vector3.multiply( direction, -1 );
                    }
                };

                // Boilerplate component registration; Lets our service know that we exist and want to do things
                this.onComponentOwnerChanged = function( e ){
                    if( e.data.previous === null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( this.owner === null && e.data.previous !== null ) {
                        service.unregisterComponent( e.data.previous.id, this );
                    }
                };

                this.onEntityManagerChanged = function( e ) {
                    if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                        service.unregisterComponent( this.owner.id, this );
                    }
                };

            });

            var Planar = engine.base.Component({
                type: 'Motion',
                depends: ['Transform']
            },
            function( options ) {
                var that = this;
                var speed = options.speed || 1;
                var _directions = {
                    UP: math.Vector3( 0, 1, 0 ),
                    LEFT: math.Vector3( -1, 0, 0 ),
                    RIGHT: math.Vector3( 1, 0, 0 ),
                    DOWN: math.Vector3( 0, -1, 0 )
                };
                var _move = null;

                this.onMoveStart = function( event ) {
                    _move = event.data.direction;
                };

                this.onMoveStop = function( event ) {
                    _move = null;
                };

                this.onUpdate = function( event ) {
                    var transform = this.owner.find( 'Transform' );

                    if( _move ) {
                        var displacement = math.vector3.multiply( _directions[_move], speed * event.data.delta / 1000 );   // scaled motion using time delta
                        transform.position = math.vector3.add( transform.position, displacement );
                    }
                };

                // Boilerplate component registration; Lets our service know that we exist and want to do things
                this.onComponentOwnerChanged = function( e ){
                    if( e.data.previous === null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( this.owner === null && e.data.previous !== null ) {
                        service.unregisterComponent( e.data.previous.id, this );
                    }
                };

                this.onEntityManagerChanged = function( e ) {
                    if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                        service.registerComponent( this.owner.id, this );
                    }

                    if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                        service.unregisterComponent( this.owner.id, this );
                    }
                };
            });

            var _components = {
                Planar: Planar,
                AutoPlanar: AutoPlanar
            };
            Object.defineProperty( this, 'component', {
                get: function() {
                    return _components;
                }
            });
        });
        engine.motion = new MotionService();

        var CollisionService = engine.base.Service({
            type: 'Physics',
            schedule: {
                update: {
                    phase: engine.scheduler.phases.UPDATE
                }
            },
            depends: [ 'Motion' ],
            time: engine.scheduler.simulationTime
        },
        function( options ) {

            var that = this;
            var service = this;

            var AABB = engine.base.Component({
                type: 'Collision',
                depends: ['Transform']
            },
            function( options ) {
            });

            this.update = function() {

                for( var componentType in that.components ) {
                    for( var entityId in that.components[componentType] ) {
                        while( that.components[componentType][entityId].handleQueuedEvent() ) {}
                    }
                };

            };

            var _components = {
                    AABB: AABB
            };

            Object.defineProperty( this, 'component', {
                get: function() {
                    return _components;
                }
            });
        });
        engine.collision = new CollisionService();

        var PlayerComponent = engine.base.Component({
            type: 'Player',
            depends: ['Transform']    // We're going to be moving stuff
        },
        function( options ) {

            options = options || {};
            var that = this;

            // This is a hack so that this component will have its message
            // queue processed
            var service = engine.logic;

            this.onCollision = function( event ) {
                // console.log( that.owner.id, '<->', event.data.entity.id );
            };

            this.onUpdate = function( event ) {
            };

            // Boilerplate component registration; Lets our service know that we exist and want to do things
            this.onComponentOwnerChanged = function( e ){
                if( e.data.previous === null && this.owner !== null ) {
                    service.registerComponent( this.owner.id, this );
                }

                if( this.owner === null && e.data.previous !== null ) {
                    service.unregisterComponent( e.data.previous.id, this );
                }
            };

            this.onEntityManagerChanged = function( e ) {
                if( e.data.previous === null && e.data.current !== null && this.owner !== null ) {
                    service.registerComponent( this.owner.id, this );
                }

                if( e.data.previous !== null && e.data.current === null && this.owner !== null ) {
                    service.unregisterComponent( this.owner.id, this );
                }
            };

        });

        var run = function() {

            // Make a new space for our entities
            var space = new engine.core.Space();

            canvas = engine.graphics.target.element;

            // Make an obstacle that will collide with the player
            var obstacle = new space.Entity({
                name: 'obstacle',
                components: [
                             new engine.core.component.Transform(),
                             new engine.graphics.component.Model({
                                 mesh: resources.mesh,
                                 material: resources.material
                             }),
                             new engine.motion.component.AutoPlanar()
                             ]
            });

            // Make a user-controllable object that can collide with the obstacle
            var playerKeyHandler = function(e) {
                if( this.owner ) {
                    // If we have an owner, dispatch a game event for it to enjoy
                    var keyCode = e.data.code;
                    var keyState = e.data.state;
                    switch( keyCode ) {
                    case 'W':
                        new engine.core.Event({
                            type: keyState === 'down' ? 'MoveStart' : 'MoveStop',
                                    data: {
                                        direction: 'UP'
                                    }
                        }).dispatch( this.owner );
                        break;
                    case 'A':
                        new engine.core.Event({
                            type: keyState === 'down' ? 'MoveStart' : 'MoveStop',
                                    data: {
                                        direction: 'LEFT'
                                    }
                        }).dispatch( this.owner );
                        break;
                    case 'S':
                        new engine.core.Event({
                            type: keyState === 'down' ? 'MoveStart' : 'MoveStop',
                                    data: {
                                        direction: 'DOWN'
                                    }
                        }).dispatch( this.owner );
                        break;
                    case 'D':
                        new engine.core.Event({
                            type: keyState === 'down' ? 'MoveStart' : 'MoveStop',
                                    data: {
                                        direction: 'RIGHT'
                                    }
                        }).dispatch( this.owner );
                        break;

                    }
                }
            };
            var player = new space.Entity({
                name: 'player',
                components: [
                             new engine.core.component.Transform({
                                 position: math.Vector3( 3, 1.5, 0 ),
                                 rotation: math.Vector3( 0, 0, 0 )
                             }),
                             new engine.graphics.component.Model({
                                 mesh: resources.mesh,
                                 material: resources.material
                             }),
                             new engine.input.component.Controller({
                                 onKey: playerKeyHandler
                             }),
                             new engine.motion.component.Planar({ speed: 5 }),
                             new PlayerComponent()
                             ]
            });

            var camera = new space.Entity({
                name: 'camera',
                components: [
                             new engine.core.component.Transform({
                                 position: math.Vector3( 0, 2, 10 )
                             }),
                             new engine.graphics.component.Camera({
                                 active: true,
                                 width: canvas.width,
                                 height: canvas.height,
                                 fov: 60
                             }),
                             new engine.graphics.component.Light({ intensity: 50 })
                             ]
            });
            camera.find( 'Camera' ).target = obstacle.find( 'Transform' ).position;

            // Start the engine!
            engine.run();

        };

        engine.core.resource.get(
                [
                 {
                     type: engine.graphics.resource.Mesh,
                     url: 'procedural-mesh.js?size=2',
                     load: engine.core.resource.proceduralLoad,
                     onsuccess: function( mesh ) {
                         resources.mesh = mesh;
                     },
                     onfailure: function( error ) {
                     }
                 },
                 {
                     type: engine.graphics.resource.Material,
                     url: 'procedural-material.js',
                     load: engine.core.resource.proceduralLoad,
                     onsuccess: function( material ) {
                         resources.material = material;
                     },
                     onfailure: function( error ) {
                     }
                 }],
                 {
                    oncomplete: run
                 }
        );

    };

    gladius.create(
            {
                debug: true,
                services: {
                    graphics: {
                        src: 'graphics/service',
                        options: {
                            canvas: canvas
                        }
                    },
                    input: {
                        src: 'input/service',
                        options: {

                        }
                    },
                    logic: 'logic/game/service'
                }
            },
            game
    );

});
<|endoftext|>


<commit_before>// You need to bring in jQuery first in order for this to work
//
// Call it like this:
// pkTagahead(<?php echo json_encode(url_for("taggableComplete/complete")) ?>);
//
// Or similar. Now all of your input elements with the input-tag class
// automatically gain the typeahead suggestion feature.
//
// If you're not using Symfony and sfDoctrineActAsTaggablePlugin,
// pass your own URL that returns a
 containing
s with the
// FULL TEXT of what the ENTIRE tag string will be if the user takes
// that suggestion, with the new tag you're suggesting an  link
// to #. Then use CSS to hide (visibility: none) the part of the
//
 that is not in the . Don't introduce any extra whitespace.

function pkTagahead(tagaheadUrl)
{
  $(function() {
    function getKey(event)
    {
      // Portable keycodes sigh
      return event.keyCode ? event.keyCode : event.which;
    }
    function setClick(target)
    {
      $(target).find('a').click(function(event)
      {
        // span contains ul contains li contains a
        var span = this.parentNode.parentNode.parentNode;
        var input = $(span).data("tag-peer");
        // Get text from the li
        var parent = this.parentNode;
        $(input).val($(parent).text());
        $(input).focus();
        return false;
      });
    }
    // Add suggestions span (you'll need to style that)
    $('input.tag-input').after("

");
    // Each tag field remembers its suggestions span...
    $('input.tag-input').each(function()
    {
      $(this).data("tag-peer", $(this).next()[0]);
    });
    // And vice versa
    $('div.tag-suggestions').each(function()
    {
      $(this).data("tag-peer", $(this).prev()[0]);
    });
    // Now we can really throw down
    $('input.tag-input').keyup(function(event)
    {
      var key = getKey(event);
      // Tab key
      if (key == 9)
      {
        var peer = $(this).data("tag-peer");
        var suggestions = $(peer).find("li");
        if (suggestions.length)
        {
          $(this).val($(suggestions[0]).text());
          $(this).focus();
        }
        // In any case don't insert the tab
        return false;
      }
      else
      {
        // Trigger ajax update of suggestions
      }
    });
    $('input.tag-input').keypress(function(event)
    {
      // Firefox 2.0 mac is stubborn and only allows cancel here
      // (we will still get the keyup and do the real work there)
      var key = getKey(event);
      if (key == 9)
      {
        // Don't insert tabs, ever
        return false;
      }
    });
    var lastValues = {};
    setInterval(function()
    {
      // AJAX query for suggestions only when changes have taken place
      $('input.tag-input').each(function()
      {
        var last = $(this).data('tag-last');
        var value = $(this).val();
        var peer = $(this).data('tag-peer');
        if (last !== value)
        {
          $(this).data('tag-last', value);
          $.post(
            tagaheadUrl,
            {
              current: $(this).val()
            },
            function(data, textStatus)
            {
              $(peer).html(data);
              setClick(peer);
            }
          );
        }
      });
    }, 200);
  });
}

function aInlineTaggableWidget(selector, options)
{
	var typeaheadUrl = options['typeahead-url'];

	function makePopularLink(attributes, title, text)
	{
		var new_tag = $('');
		var new_link = $('');
		new_tag.html(""+text+"");
		new_tag.attr({ class: 'a-tag a-popular', title: title });
		new_tag.prepend(new_link);
		new_link.text(title);
		new_link.attr(attributes);
		return new_tag;
	}

	function makeRemoveLink(attributes, title, text)
	{
		var new_tag = $('');
		var new_link = $('');
		new_tag.html(""+title+"");
		new_tag.attr({ class:'a-tag' , title: title });
		new_link.text('Remove Tag');
		new_link.attr(attributes);
		new_link.attr({ class: 'a-btn icon a-close-small no-label no-bg', title: 'Remove' });
		new_link.prepend('');
		new_tag.append(new_link);
		return new_tag;
	}

	function trimExcessCommas(string)
	{
		string = string.replace(/(^,)|(, ?$)/g, '');
		string = string.replace(/(,,)|(, ,)/, ',');
		string = $.trim(string);

		return string;
	}

	$(selector).each(function()
	{
		var popularTags = options['popular-tags'];
		var existingTags = options['existing-tags'];

		// We don't want to display popular tags that we're already using
		var unusedPopulars = {};
		for (x in popularTags)
		{
			if (typeof(existingTags[x]) == 'undefined')
			{
				unusedPopulars[x] = popularTags[x];
			}
		}

		var popularsAttributes = {};
		var existingTagsAttributes = {};
		var existingDiv = $('
');
		var popularsDiv = $('
');

		// Establish the quick enhancement
		var tagInput = $(this);
		var typeAheadBox = $('<input />');
		typeAheadBox.attr('type', 'text');

		var addButton = $('');
		addButton.text('Add');
		addButton.attr({'href' : '#', 'class' : 'a-btn icon a-add add-tags-link', 'title' : 'Add these tags'});

		tagInput.hide();
		tagInput.parent().append(typeAheadBox);
		tagInput.parent().append(addButton);

		// Add a list of popular tags to be added
		function addTagsToForm(link)
		{
			var tag = link.attr('title');
			var value = tagInput.val() + ', ' + tag;

			value = trimExcessCommas(value);
			tagInput.val(value);

			link.remove();

			var new_link = makeRemoveLink(existingTagsAttributes, tag, tag + ' x');
			new_link.bind('click', function() { removeTagsFromForm($(this)); return false; });
			existingDiv.append(new_link);
		}

		// Add a list of tags that may be removed
		function removeTagsFromForm(link)
		{
			var tag = link.attr('title');
			var value = tagInput.val();

			value = value.replace(tag, '');
			value = trimExcessCommas(value);
			tagInput.val(value);

			link.remove();

			// As we have just removed it from the list, we want the real deal populars.
			if (typeof(popularTags[tag]) != 'undefined')
			{
				var linkLabel = popularTags[tag];
				var new_link = makePopularLink(existingTagsAttributes, tag, linkLabel);
				new_link.bind('click', function() { addTagsToForm($(this)); return false; });
				popularsDiv.append(new_link);
			}
		}

		// a maker function for tag containers
		function makeTagContainer(containerLabel, tagArray, linkAttributes, linkLabelType)
		{
			// Add a list of tags that may be removed
			var tagContainer = $('
');
			tagContainer.addClass('a-inline-taggable-widget-tag-container');
			var header = $('
');
			header.text(containerLabel);
			tagContainer.append(header);

			var attributes = {};
			for (x in tagArray)
			{
				var linkLabel = '';
				if (linkLabelType == 'add')
				{
					linkLabel = tagArray[x];
					var new_link = makePopularLink(linkAttributes, x, linkLabel);
					new_link.bind('click', function() { addTagsToForm($(this));  return false; });
				}
				else if (linkLabelType == 'remove')
				{
					linkLabel = 'x ' + x;
					var new_link = makeRemoveLink(linkAttributes, x, linkLabel);
					new_link.bind('click', function() { removeTagsFromForm($(this));  return false; });
				}
				tagContainer.append(new_link);
			}
			return tagContainer;
		}

		// Add the new tags to the existing form input.
		// If the user doesn't click Save changes or add... tough?
		function commitTagsToForm()
		{
			var value = tagInput.val() + ',' + typeAheadBox.val();
			value = trimExcessCommas(value);
			tagInput.val(value);
			typeAheadBox.val('');

			existingTags = {};
			var lp = value.split(',');
			for (x in lp)
			{
				existingTags[lp[x]] = lp[x];
			}

			existingDiv.html(makeTagContainer('Existing Tags', existingTags, existingTagsAttributes, 'remove').html());
			existingDiv.children('a').bind('click', function() { removeTagsFromForm($(this)); return false; });

			return false;
		}
		addButton.bind('click', function() { commitTagsToForm(); return false; });

		existingDiv = makeTagContainer('Existing Tags', existingTags, existingTagsAttributes, 'remove');
		existingDiv.children('a').bind('click', function() { removeTagsFromForm($(this));  return false; });
		tagInput.parent().prepend(existingDiv);

		popularsDiv = makeTagContainer('Popular Tags', unusedPopulars, popularsAttributes, 'add');
		popularsDiv.children('a').bind('click', function() { addTagsToForm($(this));  return false; });
		tagInput.parent().append(popularsDiv);

		// TypeAhead
		typeAheadBox.autocomplete(typeaheadUrl,
			{
				multiple: true
			});

		// Catch that enter key, baby!
		$(document).keyup(function(e)
			{
				if (e.keyCode == 13)
				{
					e.preventDefault();
				}
			});

		$(document).keypress(function(e)
			{
				if (e.keyCode == 13)
				{
					e.preventDefault();
				}
			});

		typeAheadBox.keyup(function(e)
			{
		 		if (e.keyCode == 13)
		 		{
		 			commitTagsToForm();
		 		}
		 	});
	});

}
<commit_msg>I added the ability to pass in a label for the 'Existing Tags'.<commit_after>// You need to bring in jQuery first in order for this to work
//
// Call it like this:
// pkTagahead(<?php echo json_encode(url_for("taggableComplete/complete")) ?>);
//
// Or similar. Now all of your input elements with the input-tag class
// automatically gain the typeahead suggestion feature.
//
// If you're not using Symfony and sfDoctrineActAsTaggablePlugin,
// pass your own URL that returns a
 containing
s with the
// FULL TEXT of what the ENTIRE tag string will be if the user takes
// that suggestion, with the new tag you're suggesting an  link
// to #. Then use CSS to hide (visibility: none) the part of the
//
 that is not in the . Don't introduce any extra whitespace.

function pkTagahead(tagaheadUrl)
{
  $(function() {
    function getKey(event)
    {
      // Portable keycodes sigh
      return event.keyCode ? event.keyCode : event.which;
    }
    function setClick(target)
    {
      $(target).find('a').click(function(event)
      {
        // span contains ul contains li contains a
        var span = this.parentNode.parentNode.parentNode;
        var input = $(span).data("tag-peer");
        // Get text from the li
        var parent = this.parentNode;
        $(input).val($(parent).text());
        $(input).focus();
        return false;
      });
    }
    // Add suggestions span (you'll need to style that)
    $('input.tag-input').after("

");
    // Each tag field remembers its suggestions span...
    $('input.tag-input').each(function()
    {
      $(this).data("tag-peer", $(this).next()[0]);
    });
    // And vice versa
    $('div.tag-suggestions').each(function()
    {
      $(this).data("tag-peer", $(this).prev()[0]);
    });
    // Now we can really throw down
    $('input.tag-input').keyup(function(event)
    {
      var key = getKey(event);
      // Tab key
      if (key == 9)
      {
        var peer = $(this).data("tag-peer");
        var suggestions = $(peer).find("li");
        if (suggestions.length)
        {
          $(this).val($(suggestions[0]).text());
          $(this).focus();
        }
        // In any case don't insert the tab
        return false;
      }
      else
      {
        // Trigger ajax update of suggestions
      }
    });
    $('input.tag-input').keypress(function(event)
    {
      // Firefox 2.0 mac is stubborn and only allows cancel here
      // (we will still get the keyup and do the real work there)
      var key = getKey(event);
      if (key == 9)
      {
        // Don't insert tabs, ever
        return false;
      }
    });
    var lastValues = {};
    setInterval(function()
    {
      // AJAX query for suggestions only when changes have taken place
      $('input.tag-input').each(function()
      {
        var last = $(this).data('tag-last');
        var value = $(this).val();
        var peer = $(this).data('tag-peer');
        if (last !== value)
        {
          $(this).data('tag-last', value);
          $.post(
            tagaheadUrl,
            {
              current: $(this).val()
            },
            function(data, textStatus)
            {
              $(peer).html(data);
              setClick(peer);
            }
          );
        }
      });
    }, 200);
  });
}

function aInlineTaggableWidget(selector, options)
{
	var typeaheadUrl = options['typeahead-url'];
	var tagsLabel = options['tagsLabel'];
	var popularTags = options['popular-tags'];
	var existingTags = options['existing-tags'];

	if (typeof(tagsLabel) == 'undefined')
	{
		tagsLabel = "Existing Tags";
	};

	function makePopularLink(attributes, title, text)
	{
		var new_tag = $('');
		var new_link = $('');
		new_tag.html(""+text+"");
		new_tag.attr({ class: 'a-tag a-popular', title: title });
		new_tag.prepend(new_link);
		new_link.text(title);
		new_link.attr(attributes);
		return new_tag;
	}

	function makeRemoveLink(attributes, title, text)
	{
		var new_tag = $('');
		var new_link = $('');
		new_tag.html(""+title+"");
		new_tag.attr({ class:'a-tag' , title: title });
		new_link.text('Remove Tag');
		new_link.attr(attributes);
		new_link.attr({ class: 'a-btn icon a-close-small no-label no-bg', title: 'Remove' });
		new_link.prepend('');
		new_tag.append(new_link);
		return new_tag;
	}

	function trimExcessCommas(string)
	{
		string = string.replace(/(^,)|(, ?$)/g, '');
		string = string.replace(/(,,)|(, ,)/, ',');
		string = $.trim(string);

		return string;
	}

	$(selector).each(function()
	{

		// We don't want to display popular tags that we're already using
		var unusedPopulars = {};
		for (x in popularTags)
		{
			if (typeof(existingTags[x]) == 'undefined')
			{
				unusedPopulars[x] = popularTags[x];
			}
		}

		var popularsAttributes = {};
		var existingTagsAttributes = {};
		var existingDiv = $('
');
		var popularsDiv = $('
');

		// Establish the quick enhancement
		var tagInput = $(this);
		var typeAheadBox = $('<input />');
		typeAheadBox.attr('type', 'text');

		var addButton = $('');
		addButton.text('Add');
		addButton.attr({'href' : '#', 'class' : 'a-btn icon a-add add-tags-link', 'title' : 'Add these tags'});

		tagInput.hide();
		tagInput.parent().append(typeAheadBox);
		tagInput.parent().append(addButton);

		// Add a list of popular tags to be added
		function addTagsToForm(link)
		{
			var tag = link.attr('title');
			var value = tagInput.val() + ', ' + tag;

			value = trimExcessCommas(value);
			tagInput.val(value);

			link.remove();

			var new_link = makeRemoveLink(existingTagsAttributes, tag, tag + ' x');
			new_link.children('a').bind('click', function() { removeTagsFromForm($(this).parent()); return false; });
			existingDiv.append(new_link);
		}

		// Add a list of tags that may be removed
		function removeTagsFromForm(link)
		{
			var tag = link.attr('title');
			var value = tagInput.val();

			value = value.replace(tag, '');
			value = trimExcessCommas(value);
			tagInput.val(value);

			link.remove();

			// As we have just removed it from the list, we want the real deal populars.
			if (typeof(popularTags[tag]) != 'undefined')
			{
				var linkLabel = popularTags[tag];
				var new_link = makePopularLink(existingTagsAttributes, tag, linkLabel);
				new_link.children('a').bind('click', function() { addTagsToForm($(this).parent()); return false; });
				popularsDiv.append(new_link);
			}
		}

		// a maker function for tag containers
		function makeTagContainer(containerLabel, tagArray, linkAttributes, linkLabelType)
		{
			// Add a list of tags that may be removed
			var tagContainer = $('
');
			tagContainer.addClass('a-inline-taggable-widget-tag-container');
			var header = $('
');
			header.text(containerLabel);
			tagContainer.append(header);

			var attributes = {};
			for (x in tagArray)
			{
				var linkLabel = '';
				if (linkLabelType == 'add')
				{
					tagContainer.addClass('a-popular-tags');

					linkLabel = tagArray[x];
					var new_link = makePopularLink(linkAttributes, x, linkLabel);
					new_link.children('a').bind('click', function() { addTagsToForm($(this).parent());  return false; });
				}
				else if (linkLabelType == 'remove')
				{
					tagContainer.addClass('a-existing-tags');

					linkLabel = 'x ' + x;
					var new_link = makeRemoveLink(linkAttributes, x, linkLabel);
					new_link.children('a').bind('click', function() { removeTagsFromForm($(this).parent());  return false; });
				}
				tagContainer.append(new_link);
			}
			return tagContainer;
		}

		// Add the new tags to the existing form input.
		// If the user doesn't click Save changes or add... tough?
		function commitTagsToForm()
		{
			var value = tagInput.val() + ',' + typeAheadBox.val();
			value = trimExcessCommas(value);
			tagInput.val(value);
			typeAheadBox.val('');

			existingTags = {};
			var lp = value.split(',');
			for (x in lp)
			{
				existingTags[lp[x]] = lp[x];
			}

			existingDiv.html(makeTagContainer(tagsLabel, existingTags, existingTagsAttributes, 'remove').html());
			existingDiv.children('a').bind('click', function() { removeTagsFromForm($(this)); return false; });

			return false;
		}
		addButton.bind('click', function() { commitTagsToForm(); return false; });

		existingDiv = makeTagContainer(tagsLabel, existingTags, existingTagsAttributes, 'remove');
		existingDiv.children('a').bind('click', function() { removeTagsFromForm($(this));  return false; });
		tagInput.parent().prepend(existingDiv);

		popularsDiv = makeTagContainer('Popular Tags', unusedPopulars, popularsAttributes, 'add');
		popularsDiv.children('a').bind('click', function() { addTagsToForm($(this));  return false; });
		tagInput.parent().append(popularsDiv);

		// TypeAhead
		typeAheadBox.autocomplete(typeaheadUrl, {	multiple: true });

		// Catch that enter key, baby!
		$(document).keyup(function(e)
			{
				if (e.keyCode == 13)
				{
					e.preventDefault();
				}
			});

		$(document).keypress(function(e)
			{
				if (e.keyCode == 13)
				{
					e.preventDefault();
				}
			});

		typeAheadBox.keyup(function(e)
			{
		 		if (e.keyCode == 13)
		 		{
		 			commitTagsToForm();
		 		}
		 	});
	});

}
<|endoftext|>


<commit_before>// @flow
// An infinite scrolling chat list. Using react-virtualized which doens't really handle this case out of the box
// We control which set of messages we render in our state object
// We load that in in our constructor, after you stop scrolling or if we get an update and we're not currently scrolling

import LoadingMore from './messages/loading-more'
import {TextPopupMenu} from './messages/popup'
import React, {Component} from 'react'
import ReactDOM from 'react-dom'
import SidePanel from './side-panel/index.desktop'
import _ from 'lodash'
import messageFactory from './messages'
import shallowEqual from 'shallowequal'
import {AutoSizer, CellMeasurer, List as VirtualizedList, defaultCellMeasurerCellSizeCache} from 'react-virtualized'
import {ProgressIndicator} from '../../common-adapters'
import {globalColors, globalStyles} from '../../styles'

import type {List} from 'immutable'
import type {Message, MessageID} from '../../constants/chat'
import type {Props} from './list'

type State = {
  isLockedToBottom: boolean,
  isScrolling: boolean,
  messages: List<Message>,
  scrollTop: number,
  selectedMessageID?: MessageID,
}

class ConversationList extends Component<void, Props, State> {
  _cellCache: any;
  _cellMeasurer: any;
  _list: any;
  state: State;
  _toRemeasure: Array<number>;
  _lastWidth: ?number;

  constructor (props: Props) {
    super(props)

    this.state = {
      isLockedToBottom: true,
      isScrolling: false,
      messages: props.messages,
      scrollTop: 0,
    }

    this._cellCache = new CellSizeCache(this._indexToID)
    this._toRemeasure = []
  }

  _indexToID = index => {
    // loader message
    if (index === 0) {
      return 0
    } else {
      // minus one because loader message is there
      const messageIndex = index - 1
      const message = this.state.messages.get(messageIndex)
      const id = message && message.key
      if (id == null) {
        console.warn('id is null for index:', messageIndex)
      }
      return id
    }
  }

  shouldComponentUpdate (nextProps: Props, nextState: State) {
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
  }

  componentWillUnmount () {
    // Stop any throttled/debounced functions
    this._onScroll.cancel()
    this._recomputeListDebounced.cancel()
    this._onScrollSettled.cancel()
  }

  componentWillUpdate (nextProps: Props, nextState: State) {
    // If a message has moved from pending to sent, tell the List to discard
    // heights for it (which will re-render it and everything after it)
    // TODO this doesn't work for things that take a bit to load (imgs)
    if (this._toRemeasure.length) {
      this._toRemeasure.forEach(item => {
        this._cellCache.clearRowHeight(item)
        this._list && this._list.recomputeRowHeights(item)
      })
      this._toRemeasure = []
    }
  }

  componentDidUpdate (prevProps: Props, prevState: State) {
    if (!this.state.isLockedToBottom && this.state.messages !== prevState.messages && prevState.messages.count() > 1) {
      // Figure out how many new items we have
      const prependedCount = this.state.messages.indexOf(prevState.messages.first())
      if (prependedCount !== -1) {
        // Measure the new items so we can adjust our scrollTop so your position doesn't jump
        const scrollTop = this.state.scrollTop + _.range(0, prependedCount)
          .map(index => this._cellMeasurer.getRowHeight({index: index + 1})) // +1 since 0 is the loading message
          .reduce((total, height) => total + height, 0)

        // Disabling eslint as we normally don't want to call setState in a componentDidUpdate in case you infinitely re-render
        this.setState({scrollTop}) // eslint-disable-line react/no-did-update-set-state
      }
    }
  }

  componentWillReceiveProps (nextProps: Props) {
    if (this.props.selectedConversation !== nextProps.selectedConversation) {
      this.setState({isLockedToBottom: true})
      this._recomputeList(true)
    }

    // If we're not scrolling let's update our internal messages
    if (!this.state.isScrolling) {
      this._invalidateChangedMessages(nextProps)
      this.setState({
        messages: nextProps.messages,
      })
    }
  }

  _invalidateChangedMessages (props: Props) {
    this.state.messages.forEach((item, index) => {
      const oldMessage = props.messages.get(index, {})

      if (item.type === 'Text' && oldMessage.type === 'Text' && item.messageState !== oldMessage.messageState) {
        this._toRemeasure.push(index + 1)
      }

      if (item.type === 'Attachment' && oldMessage.type === 'Attachment' && item.previewPath !== oldMessage.previewPath) {
        this._toRemeasure.push(index + 1)
      }
    })
  }

  _onScrollSettled = _.debounce(() => {
    // If we've stopped scrolling let's update our internal messages
    this._invalidateChangedMessages(this.props)
    this.setState({
      isScrolling: false,
      ...(this.state.messages !== this.props.messages ? {messages: this.props.messages} : null),
    })
  }, 1000)

  _onScroll = _.throttle(({clientHeight, scrollHeight, scrollTop}) => {
    // Do nothing if we haven't really loaded anything
    if (!clientHeight) {
      return
    }

    // At the top, load more messages. Action handles loading state and if there's actually any more
    if (scrollTop === 0) {
      this.props.onLoadMoreMessages()
    }

    const isLockedToBottom = scrollTop + clientHeight === scrollHeight
    this.setState({
      isLockedToBottom,
      isScrolling: true,
      scrollTop,
    })

    // This is debounced so it resets the call
    this._onScrollSettled()
  }, 100)

  _showPopup (message: Message, event: any) {
    if (message.type !== 'Text') return
    this.setState({
      selectedMessageID: message.messageID,
    })

    const clientRect = event.target.getBoundingClientRect()
    // Position next to button (client rect)
    // TODO: Measure instead of pixel math
    const x = clientRect.left - 205
    let y = clientRect.top - (message.followState === 'You' ? 200 : 116)
    if (y < 10) y = 10
    const popupComponent = <TextPopupMenu
      message={message}
      onEditMessage={this.props.onEditMessage}
      onDeleteMessage={this.props.onDeleteMessage}
      onHidden={() => {
        ReactDOM.unmountComponentAtNode(document.getElementById('popupContainer'))
        this.setState({
          selectedMessageID: undefined,
        })
      }}
      style={{position: 'absolute', top: y, left: x}}
      />
    const container = document.getElementById('popupContainer')
    ReactDOM.render(popupComponent, container)
  }

  _onAction = (message, event) => {
    this._showPopup(message, event)
  }

  _rowRenderer = ({index, key, style, isScrolling}: {index: number, key: string, style: Object, isScrolling: boolean}) => {
    if (index === 0) {
      return <LoadingMore style={style} key={key || index} hasMoreItems={this.props.moreToLoad} />
    }

    const message = this.state.messages.get(index - 1)
    const prevMessage = this.state.messages.get(index - 2)
    const isFirstMessage = index - 1 === 0
    const skipMsgHeader = (message.author != null && prevMessage && prevMessage.type === 'Text' && prevMessage.author === message.author)
    const isSelected = message.messageID != null && this.state.selectedMessageID === message.messageID
    const isFirstNewMessage = message.messageID != null && this.props.firstNewMessageID ? this.props.firstNewMessageID === message.messageID : false
    // TODO: We need to update the message component selected status
    // when showing popup, which isn't currently working.

    return messageFactory(message, isFirstMessage || !skipMsgHeader, index, key, isFirstNewMessage, style, isScrolling, this._onAction, isSelected, this.props.onLoadAttachment, this.props.onOpenInFileUI, this.props.onOpenInPopup, this.props.onRetryMessage)
  }

  _recomputeListDebounced = _.debounce(() => {
    this._recomputeList()
  }, 300)

  _recomputeList () {
    this._cellCache.clearAllRowHeights()
    this._list && this._list.recomputeRowHeights()
  }

  render () {
    if (!this.props.validated) {
      return (

          <ProgressIndicator style={{width: 20}} />

      )
    }
    const messageCount = this.state.messages.count()
    const countWithLoading = messageCount + 1 // Loading row on top always
    let scrollToIndex = this.state.isLockedToBottom ? countWithLoading - 1 : undefined
    let scrollTop = scrollToIndex ? undefined : this.state.scrollTop

    // We need to use both visibility and opacity css properties for the
    // action button hide/show on hover.
    // We use opacity because it shows/hides the button immediately on
    // hover, while visibility has slight lag.
    // We use visibility so that the action button content isn't copied
    // during copy/paste actions since user-select isn't working in
    // Chrome.
    const realCSS = `
    .message {
      background-color: transparent;
    }
    .message .action-button {
      visibility: hidden;
      opacity: 0;
    }
    .message:hover {
      background-color: ${globalColors.black_05};
    }
    .message:hover .action-button {
      visibility: visible;
      opacity: 1;
    }
    `

    return (

        <AutoSizer
          onResize={({width}) => {
            if (width !== this._lastWidth) {
              this._lastWidth = width
              this._recomputeListDebounced()
            }
          }} >
          {({height, width}) => {
            return <CellMeasurer
              cellRenderer={({rowIndex, ...rest}) => this._rowRenderer({index: rowIndex, ...rest})}
              columnCount={1}
              ref={r => { this._cellMeasurer = r }}
              cellSizeCache={this._cellCache}
              rowCount={countWithLoading}
              width={width} >
              {({getRowHeight}) => {
                return <VirtualizedList
                  style={{outline: 'none'}}
                  height={height}
                  ref={r => { this._list = r }}
                  width={width}
                  onScroll={this._onScroll}
                  scrollTop={scrollTop}
                  scrollToIndex={scrollToIndex}
                  rowCount={countWithLoading}
                  rowHeight={getRowHeight}
                  columnWidth={width}
                  rowRenderer={this._rowRenderer} />
              }}
            </CellMeasurer>
          }}
        </AutoSizer>
        {this.props.sidePanelOpen &&

          <SidePanel {...this.props} />

}

    )
  }
}

class CellSizeCache extends defaultCellMeasurerCellSizeCache {
  _indexToID: (index: number) => ?number;

  constructor (indexToID) {
    super({uniformColumnWidth: true, uniformRowHeight: false})
    this._indexToID = indexToID
  }

  getRowHeight (index) {
    return super.getRowHeight(this._indexToID(index))
  }

  getColumnWidth (index) {
    return super.getColumnWidth(this._indexToID(index))
  }

  setRowHeight (index, height) {
    super.setRowHeight(this._indexToID(index), height)
  }

  setColumnWidth (index, width) {
    super.setColumnWidth(this._indexToID(index), width)
  }

  clearRowHeight (index) {
    super.clearRowHeight(this._indexToID(index))
  }

  clearColumnWidth (index) {
    super.clearColumnWidth(this._indexToID(index))
  }
}

export default ConversationList
<commit_msg>Footer row for padding (#5308)<commit_after>// @flow
// An infinite scrolling chat list. Using react-virtualized which doens't really handle this case out of the box
// We control which set of messages we render in our state object
// We load that in in our constructor, after you stop scrolling or if we get an update and we're not currently scrolling

import LoadingMore from './messages/loading-more'
import {TextPopupMenu} from './messages/popup'
import React, {Component} from 'react'
import ReactDOM from 'react-dom'
import SidePanel from './side-panel/index.desktop'
import _ from 'lodash'
import messageFactory from './messages'
import shallowEqual from 'shallowequal'
import {AutoSizer, CellMeasurer, List as VirtualizedList, defaultCellMeasurerCellSizeCache} from 'react-virtualized'
import {Box, ProgressIndicator} from '../../common-adapters'
import {globalColors, globalStyles} from '../../styles'

import type {List} from 'immutable'
import type {Message, MessageID} from '../../constants/chat'
import type {Props} from './list'

type State = {
  isLockedToBottom: boolean,
  isScrolling: boolean,
  messages: List<Message>,
  scrollTop: number,
  selectedMessageID?: MessageID,
}

class ConversationList extends Component<void, Props, State> {
  _cellCache: any;
  _cellMeasurer: any;
  _list: any;
  state: State;
  _toRemeasure: Array<number>;
  _lastWidth: ?number;

  constructor (props: Props) {
    super(props)

    this.state = {
      isLockedToBottom: true,
      isScrolling: false,
      messages: props.messages,
      scrollTop: 0,
    }

    this._cellCache = new CellSizeCache(this._indexToID)
    this._toRemeasure = []
  }

  _indexToID = index => {
    if (index === 0) {
      // loader
      return 0
    } else if (index === this.state.messages.count() + 1) {
      // footer
      return -1
    } else {
      // minus one because loader message is there
      const messageIndex = index - 1
      const message = this.state.messages.get(messageIndex)
      const id = message && message.key
      if (id == null) {
        console.warn('id is null for index:', messageIndex)
      }
      return id
    }
  }

  shouldComponentUpdate (nextProps: Props, nextState: State) {
    return !shallowEqual(this.props, nextProps) || !shallowEqual(this.state, nextState)
  }

  componentWillUnmount () {
    // Stop any throttled/debounced functions
    this._onScroll.cancel()
    this._recomputeListDebounced.cancel()
    this._onScrollSettled.cancel()
  }

  componentWillUpdate (nextProps: Props, nextState: State) {
    // If a message has moved from pending to sent, tell the List to discard
    // heights for it (which will re-render it and everything after it)
    // TODO this doesn't work for things that take a bit to load (imgs)
    if (this._toRemeasure.length) {
      this._toRemeasure.forEach(item => {
        this._cellCache.clearRowHeight(item)
        this._list && this._list.recomputeRowHeights(item)
      })
      this._toRemeasure = []
    }
  }

  componentDidUpdate (prevProps: Props, prevState: State) {
    if (!this.state.isLockedToBottom && this.state.messages !== prevState.messages && prevState.messages.count() > 1) {
      // Figure out how many new items we have
      const prependedCount = this.state.messages.indexOf(prevState.messages.first())
      if (prependedCount !== -1) {
        // Measure the new items so we can adjust our scrollTop so your position doesn't jump
        const scrollTop = this.state.scrollTop + _.range(0, prependedCount)
          .map(index => this._cellMeasurer.getRowHeight({index: index + 1})) // +1 since 0 is the loading message
          .reduce((total, height) => total + height, 0)

        // Disabling eslint as we normally don't want to call setState in a componentDidUpdate in case you infinitely re-render
        this.setState({scrollTop}) // eslint-disable-line react/no-did-update-set-state
      }
    }
  }

  componentWillReceiveProps (nextProps: Props) {
    if (this.props.selectedConversation !== nextProps.selectedConversation) {
      this.setState({isLockedToBottom: true})
      this._recomputeList(true)
    }

    // If we're not scrolling let's update our internal messages
    if (!this.state.isScrolling) {
      this._invalidateChangedMessages(nextProps)
      this.setState({
        messages: nextProps.messages,
      })
    }
  }

  _invalidateChangedMessages (props: Props) {
    this.state.messages.forEach((item, index) => {
      const oldMessage = props.messages.get(index, {})

      if (item.type === 'Text' && oldMessage.type === 'Text' && item.messageState !== oldMessage.messageState) {
        this._toRemeasure.push(index + 1)
      }

      if (item.type === 'Attachment' && oldMessage.type === 'Attachment' && item.previewPath !== oldMessage.previewPath) {
        this._toRemeasure.push(index + 1)
      }
    })
  }

  _onScrollSettled = _.debounce(() => {
    // If we've stopped scrolling let's update our internal messages
    this._invalidateChangedMessages(this.props)
    this.setState({
      isScrolling: false,
      ...(this.state.messages !== this.props.messages ? {messages: this.props.messages} : null),
    })
  }, 1000)

  _onScroll = _.throttle(({clientHeight, scrollHeight, scrollTop}) => {
    // Do nothing if we haven't really loaded anything
    if (!clientHeight) {
      return
    }

    // At the top, load more messages. Action handles loading state and if there's actually any more
    if (scrollTop === 0) {
      this.props.onLoadMoreMessages()
    }

    // Lock to bottom if we are close to the bottom
    const isLockedToBottom = scrollTop + clientHeight >= scrollHeight - 20
    this.setState({
      isLockedToBottom,
      isScrolling: true,
      scrollTop,
    })

    // This is debounced so it resets the call
    this._onScrollSettled()
  }, 100)

  _showPopup (message: Message, event: any) {
    if (message.type !== 'Text') return
    this.setState({
      selectedMessageID: message.messageID,
    })

    const clientRect = event.target.getBoundingClientRect()
    // Position next to button (client rect)
    // TODO: Measure instead of pixel math
    const x = clientRect.left - 205
    let y = clientRect.top - (message.followState === 'You' ? 200 : 116)
    if (y < 10) y = 10
    const popupComponent = <TextPopupMenu
      message={message}
      onEditMessage={this.props.onEditMessage}
      onDeleteMessage={this.props.onDeleteMessage}
      onHidden={() => {
        ReactDOM.unmountComponentAtNode(document.getElementById('popupContainer'))
        this.setState({
          selectedMessageID: undefined,
        })
      }}
      style={{position: 'absolute', top: y, left: x}}
      />
    const container = document.getElementById('popupContainer')
    ReactDOM.render(popupComponent, container)
  }

  _onAction = (message, event) => {
    this._showPopup(message, event)
  }

  _rowRenderer = ({index, key, style, isScrolling}: {index: number, key: string, style: Object, isScrolling: boolean}) => {
    if (index === 0) {
      return <LoadingMore style={style} key={key || index} hasMoreItems={this.props.moreToLoad} />
    }
    if (index === this.state.messages.count() + 1) {
      return <Box key={'footer'} style={{height: 20}} />
    }

    const message = this.state.messages.get(index - 1)
    const prevMessage = this.state.messages.get(index - 2)
    const isFirstMessage = index - 1 === 0
    const skipMsgHeader = (message.author != null && prevMessage && prevMessage.type === 'Text' && prevMessage.author === message.author)
    const isSelected = message.messageID != null && this.state.selectedMessageID === message.messageID
    const isFirstNewMessage = message.messageID != null && this.props.firstNewMessageID ? this.props.firstNewMessageID === message.messageID : false
    // TODO: We need to update the message component selected status
    // when showing popup, which isn't currently working.

    return messageFactory(message, isFirstMessage || !skipMsgHeader, index, key, isFirstNewMessage, style, isScrolling, this._onAction, isSelected, this.props.onLoadAttachment, this.props.onOpenInFileUI, this.props.onOpenInPopup, this.props.onRetryMessage)
  }

  _recomputeListDebounced = _.debounce(() => {
    this._recomputeList()
  }, 300)

  _recomputeList () {
    this._cellCache.clearAllRowHeights()
    this._list && this._list.recomputeRowHeights()
  }

  render () {
    if (!this.props.validated) {
      return (

          <ProgressIndicator style={{width: 20}} />

      )
    }
    const rowCount = this.state.messages.count() + 2 // Loading row on top always and footer row
    let scrollToIndex = this.state.isLockedToBottom ? rowCount - 1 : undefined
    let scrollTop = scrollToIndex ? undefined : this.state.scrollTop

    // We need to use both visibility and opacity css properties for the
    // action button hide/show on hover.
    // We use opacity because it shows/hides the button immediately on
    // hover, while visibility has slight lag.
    // We use visibility so that the action button content isn't copied
    // during copy/paste actions since user-select isn't working in
    // Chrome.
    const realCSS = `
    .message {
      background-color: transparent;
    }
    .message .action-button {
      visibility: hidden;
      opacity: 0;
    }
    .message:hover {
      background-color: ${globalColors.black_05};
    }
    .message:hover .action-button {
      visibility: visible;
      opacity: 1;
    }
    `

    return (

        <AutoSizer
          onResize={({width}) => {
            if (width !== this._lastWidth) {
              this._lastWidth = width
              this._recomputeListDebounced()
            }
          }} >
          {({height, width}) => {
            return <CellMeasurer
              cellRenderer={({rowIndex, ...rest}) => this._rowRenderer({index: rowIndex, ...rest})}
              columnCount={1}
              ref={r => { this._cellMeasurer = r }}
              cellSizeCache={this._cellCache}
              rowCount={rowCount}
              width={width} >
              {({getRowHeight}) => {
                return <VirtualizedList
                  style={{outline: 'none'}}
                  height={height}
                  ref={r => { this._list = r }}
                  width={width}
                  onScroll={this._onScroll}
                  scrollTop={scrollTop}
                  scrollToIndex={scrollToIndex}
                  rowCount={rowCount}
                  rowHeight={getRowHeight}
                  columnWidth={width}
                  rowRenderer={this._rowRenderer} />
              }}
            </CellMeasurer>
          }}
        </AutoSizer>
        {this.props.sidePanelOpen &&

          <SidePanel {...this.props} />

}

    )
  }
}

class CellSizeCache extends defaultCellMeasurerCellSizeCache {
  _indexToID: (index: number) => ?number;

  constructor (indexToID) {
    super({uniformColumnWidth: true, uniformRowHeight: false})
    this._indexToID = indexToID
  }

  getRowHeight (index) {
    return super.getRowHeight(this._indexToID(index))
  }

  getColumnWidth (index) {
    return super.getColumnWidth(this._indexToID(index))
  }

  setRowHeight (index, height) {
    super.setRowHeight(this._indexToID(index), height)
  }

  setColumnWidth (index, width) {
    super.setColumnWidth(this._indexToID(index), width)
  }

  clearRowHeight (index) {
    super.clearRowHeight(this._indexToID(index))
  }

  clearColumnWidth (index) {
    super.clearColumnWidth(this._indexToID(index))
  }
}

export default ConversationList
<|endoftext|>


<commit_before>import history from '../configuration/history';
import client, { getAccessToken, setAccessToken } from '../api';
import {
    CHECK_LOGGED_STATUS,
    LOGIN_START,
    LOGIN_SUCCESS,
} from '../constants';

export const checkLoggedStatus = () => ({
    type: CHECK_LOGGED_STATUS,
});

export const loginRequest = () => ({
    type: LOGIN_START,
});

export const loginSuccess = () => ({
    type: LOGIN_SUCCESS,
});

export const getUserStatus = () => dispatch => {
    dispatch(checkLoggedStatus());

    if (getAccessToken()) {
        dispatch(loginSuccess());
    } else {
        history.push('/login');
    }
};

export const loginUser = userData => dispatch => {
    dispatch(loginRequest());

    return client.post('login', userData)
        .then((response) => {
            setAccessToken(response.data.token);
            history.push('/');
            dispatch(getUserStatus());
        })
        .catch((error) => {
            // eslint-disable-next-line
            console.log('FETCH_ENDPOINT_ERROR', 'Infernal server error', error);
        });
};
<commit_msg>add async/await to userSession.actions<commit_after>import history from '../configuration/history';
import client, { getAccessToken, setAccessToken } from '../api';
import {
    CHECK_LOGGED_STATUS,
    LOGIN_START,
    LOGIN_SUCCESS,
} from '../constants';

export const checkLoggedStatus = () => ({
    type: CHECK_LOGGED_STATUS,
});

export const loginRequest = () => ({
    type: LOGIN_START,
});

export const loginSuccess = () => ({
    type: LOGIN_SUCCESS,
});

export const getUserStatus = () => dispatch => {
    dispatch(checkLoggedStatus());

    if (getAccessToken()) {
        dispatch(loginSuccess());
    } else {
        history.push('/login');
    }
};

export const loginUser = userData => async dispatch => {
    dispatch(loginRequest());

    try {
        const response = await client.post('login', userData);

        setAccessToken(response.data.token);
        history.push('/');
        dispatch(getUserStatus());
    } catch (error) {
        console.log('FETCH_ENDPOINT_ERROR', 'Infernal server error', error);
    }
};
<|endoftext|>