<commit_before>/*jshint node: true */
/*jslint browser:true */
/*jshint esnext: true */

"use strict";

const marked = require("marked");
const React = require("react");
const ReactDOM = require("react-dom");

const UserBlock = require("../src/client/UserBlock.js");

const electron = require("electron");
const ipc = electron.ipcRenderer;

const messages = document.getElementById("messages");
const user_container = document.getElementById("user_container");
const user_info = document.getElementById("user_info");
const user_dropdown = document.getElementById("user_dropdown");
const new_message = document.getElementById("new_message");

const userSteamID = document.location.hash.substr(1);
let userPersona;

const renderer = new marked.Renderer();

// renderer.paragraph = text => {
//   console.log(text);
//   return `<span>${text}</span>`;
// };

// markedRenderer.link = (href, title, text) => {
//   // maybe embed images here?
// };

function flashFrame(active) {
  try {
    electron.remote.getCurrentWindow().flashFrame(active);
  } catch (error) {
    console.log("it is a mystery");
    console.log(error);
  }
}

class Message extends React.Component {
  renderMessage(text) {
    let atoms = [];
    let index = 0;

    function find(what) {
      let where = text.indexOf(what, index);

      if (where == -1) {
        return index.length;
      }

      return where;
    }

    while (index < text.length) {
      let nextLineBreak = find("\n");
      let nextHttp = find("http");

      let next = Math.min(
        find("\n"),
        find("http")
      );
    }

    return atoms;
  }

  render() {
    let text = this.props.text;
    let className = `message message-${this.props.isSelf ? "self" : "other"}`;

    if (this.props.isEcho) {
      className += " message-echo";
    } else if (text.startsWith("/me ")) {
      text = text.substr(4);
      className += " message-action";
    }

    let html = marked(text, {
      renderer,
      breaks: true,
      sanitize: true
    });

    let timestamp = new Intl.DateTimeFormat(undefined, {
      hour: "numeric", minute: "numeric", second: "numeric"
    }).format(this.props.date);

    return React.DOM.div({className: className},
      React.DOM.div({className: "message-timestamp"}, timestamp),
      React.DOM.div({className: "message-sender"}, this.props.sender),
      React.DOM.div({
        className: "message-text",
        dangerouslySetInnerHTML: {__html: html}
      })
    );
  }
}

function addMessage(options) {
  options.date = options.date || new Date();

  let container = document.createElement("div");
  ReactDOM.render(React.createElement(Message, options), container);

  let shouldScroll =
    messages.scrollTop >= messages.scrollHeight - messages.clientHeight;

  messages.appendChild(container);

  if (shouldScroll) {
    messages.scrollTop = messages.scrollHeight;
  }

  if (!options.isSelf) {
    if (!document.hasFocus()) {
      flashFrame(true);

      let sound = new Audio("sounds/chime_bell_ding.wav");
      sound.play();

      ipc.send("notify", {
        title: `${userPersona.player_name} said`,
        body: options.text,
        image: userPersona.avatar_url_full,
        clickSend: ["friends:chat", userSteamID]
      });
    } else {
      let sound = new Audio("sounds/digi_plink.wav");
      sound.play();
    }
  }
}

window.addEventListener("focus", () => {
  flashFrame(false);
});

ipc.on("user", (event, steamID, persona) => {
  if (steamID !== userSteamID) {
    console.log("ignoring persona for", steamID);
    return;
  }

  userPersona = persona;

  document.title = `${persona.player_name} - Chat`;

  ReactDOM.render(
    React.createElement(UserBlock, {steamID, persona}),
    user_container);
});

let typingTimeout;

ipc.on("message", (event, steamID, text) => {
  addMessage({
    isSelf: steamID === null,
    isEcho: steamID === null,
    sender: steamID === null ? "wats my name" : userPersona.player_name,
    text
  });

  user_info.textContent = "";
  clearTimeout(typingTimeout);
});

ipc.on("typing", event => {
  clearTimeout(typingTimeout);
  setTimeout(() => user_info.textContent = "", 15000);
  user_info.textContent = "Friend is typing a message...";
});

function sendMessage(message) {
  ipc.send("chat:message", userSteamID, message);
  addMessage({
    isSelf: true,
    sender: "wats my name",
    text: message
  });
}

new_message.addEventListener("keydown", (event) => {
  if (event.keyCode == 13) {
    event.preventDefault();

    if (event.ctrlKey) {
      // TODO: add at cursor
      new_message.value += "\n";
    } else {
      if(new_message.value.trim().length > 0) {
        sendMessage(new_message.value);
        new_message.value = "";
      }
    }
  } else {
    ipc.send("chat:typing", userSteamID);
  }
});

user_dropdown.addEventListener("click", (event) => {
  event.preventDefault();
  ipc.send("request-trade", userSteamID);
});
<commit_msg>Typing indicator now displays friend's name.<commit_after>/*jshint node: true */
/*jslint browser:true */
/*jshint esnext: true */

"use strict";

const marked = require("marked");
const React = require("react");
const ReactDOM = require("react-dom");

const UserBlock = require("../src/client/UserBlock.js");

const electron = require("electron");
const ipc = electron.ipcRenderer;

const messages = document.getElementById("messages");
const user_container = document.getElementById("user_container");
const user_info = document.getElementById("user_info");
const user_dropdown = document.getElementById("user_dropdown");
const new_message = document.getElementById("new_message");

const userSteamID = document.location.hash.substr(1);
let userPersona;

const renderer = new marked.Renderer();

// renderer.paragraph = text => {
//   console.log(text);
//   return `<span>${text}</span>`;
// };

// markedRenderer.link = (href, title, text) => {
//   // maybe embed images here?
// };

function flashFrame(active) {
  try {
    electron.remote.getCurrentWindow().flashFrame(active);
  } catch (error) {
    console.log("it is a mystery");
    console.log(error);
  }
}

class Message extends React.Component {
  renderMessage(text) {
    let atoms = [];
    let index = 0;

    function find(what) {
      let where = text.indexOf(what, index);

      if (where == -1) {
        return index.length;
      }

      return where;
    }

    while (index < text.length) {
      let nextLineBreak = find("\n");
      let nextHttp = find("http");

      let next = Math.min(
        find("\n"),
        find("http")
      );
    }

    return atoms;
  }

  render() {
    let text = this.props.text;
    let className = `message message-${this.props.isSelf ? "self" : "other"}`;

    if (this.props.isEcho) {
      className += " message-echo";
    } else if (text.startsWith("/me ")) {
      text = text.substr(4);
      className += " message-action";
    }

    let html = marked(text, {
      renderer,
      breaks: true,
      sanitize: true
    });

    let timestamp = new Intl.DateTimeFormat(undefined, {
      hour: "numeric", minute: "numeric", second: "numeric"
    }).format(this.props.date);

    return React.DOM.div({className: className},
      React.DOM.div({className: "message-timestamp"}, timestamp),
      React.DOM.div({className: "message-sender"}, this.props.sender),
      React.DOM.div({
        className: "message-text",
        dangerouslySetInnerHTML: {__html: html}
      })
    );
  }
}

function addMessage(options) {
  options.date = options.date || new Date();

  let container = document.createElement("div");
  ReactDOM.render(React.createElement(Message, options), container);

  let shouldScroll =
    messages.scrollTop >= messages.scrollHeight - messages.clientHeight;

  messages.appendChild(container);

  if (shouldScroll) {
    messages.scrollTop = messages.scrollHeight;
  }

  if (!options.isSelf) {
    if (!document.hasFocus()) {
      flashFrame(true);

      let sound = new Audio("sounds/chime_bell_ding.wav");
      sound.play();

      ipc.send("notify", {
        title: `${userPersona.player_name} said`,
        body: options.text,
        image: userPersona.avatar_url_full,
        clickSend: ["friends:chat", userSteamID]
      });
    } else {
      let sound = new Audio("sounds/digi_plink.wav");
      sound.play();
    }
  }
}

window.addEventListener("focus", () => {
  flashFrame(false);
});

ipc.on("user", (event, steamID, persona) => {
  if (steamID !== userSteamID) {
    console.log("ignoring persona for", steamID);
    return;
  }

  userPersona = persona;

  document.title = `${persona.player_name} - Chat`;

  ReactDOM.render(
    React.createElement(UserBlock, {steamID, persona}),
    user_container);
});

let typingTimeout;

ipc.on("message", (event, steamID, text) => {
  addMessage({
    isSelf: steamID === null,
    isEcho: steamID === null,
    sender: steamID === null ? "wats my name" : userPersona.player_name,
    text
  });

  user_info.textContent = "";
  clearTimeout(typingTimeout);
});

ipc.on("typing", event => {
  clearTimeout(typingTimeout);
  setTimeout(() => user_info.textContent = "", 15000);
  user_info.textContent = `${userPersona.player_name} is typing a message...`;
});

function sendMessage(message) {
  ipc.send("chat:message", userSteamID, message);
  addMessage({
    isSelf: true,
    sender: "wats my name",
    text: message
  });
}

new_message.addEventListener("keydown", (event) => {
  if (event.keyCode == 13) {
    event.preventDefault();

    if (event.ctrlKey) {
      // TODO: add at cursor
      new_message.value += "\n";
    } else {
      if(new_message.value.trim().length > 0) {
        sendMessage(new_message.value);
        new_message.value = "";
      }
    }
  } else {
    ipc.send("chat:typing", userSteamID);
  }
});

user_dropdown.addEventListener("click", (event) => {
  event.preventDefault();
  ipc.send("request-trade", userSteamID);
});
<|endoftext|>


<commit_before>// Array.filter polyfill (MDN)
[].filter||(Array.prototype.filter=function(c,f){if(null==this)throw new TypeError;var b=Object(this),g=b.length>>>0;if("function"!=typeof c)throw new TypeError;for(var d=[],a=0;a<g;a++)if(a in b){var e=b[a];c.call(f,e,a,b)&&d.push(e)}return d});

// Array.map polyfill (MDN)
[].map||(Array.prototype.map=function(d,f){var g,e,a;if(null==this)throw new TypeError(" this is null or not defined");var b=Object(this),h=b.length>>>0;if("function"!==typeof d)throw new TypeError(d+" is not a function");f&&(g=f);e=Array(h);for(a=0;a<h;){var c;a in b&&(c=b[a],c=d.call(g,c,a,b),e[a]=c);a++}return e});

// Array.reduce polyfill (MDN)
"function"!==typeof Array.prototype.reduce&&(Array.prototype.reduce=function(d,e){if(null===this||"undefined"===typeof this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!==typeof d)throw new TypeError(d+" is not a function");var a=0,f=this.length>>>0,b,c=!1;1<arguments.length&&(b=e,c=!0);for(;f>a;++a)this.hasOwnProperty(a)&&(c?b=d(b,this[a],a,this):(b=this[a],c=!0));if(!c)throw new TypeError("Reduce of empty array with no initial value");return b});

// Function.bind polyfill (MDN)
Function.prototype.bind||(Function.prototype.bind=function(b){if("function"!==typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var d=Array.prototype.slice.call(arguments,1),e=this,a=function(){},c=function(){return e.apply(this instanceof a&&b?this:b,d.concat(Array.prototype.slice.call(arguments)))};a.prototype=this.prototype;c.prototype=new a;return c});

/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true,
undef:true, unused:true, curly:true, browser:true, indent:3, maxerr:50, laxcomma:true,
forin:false, curly:false */

// syringe.js v0.1.8
(function () {

   "use strict";

   var root = this;

   root.syringe = function (props) {

      // Pointers and containers
      var syringe    = {}
         , deps      = {}
         , hasProp   = {}.hasOwnProperty
         , slice     = [].slice
         , toString  = Object.prototype.toString;

      // Patterns
      var PARAMS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m
         , PARAM = /^\s*(_?)(\S+?)\1\s*$/
         , CLEAN = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

      // Asynch script-loader/-runner...
      var addScript = function (src, callback) {

         var doc     = document
            , head   = doc.getElementsByTagName("head")[0] || doc.documentElement
            , script = doc.createElement("script")
            , done   = false;

         script.src = src;

         script.onload = script.onreadystatechange = function () {

            if (!done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {

               done = true;
               script.onload = script.onreadystatechange = null;

               if (head && script.parentNode) head.removeChild(script);
               if (getType(callback, true) === 'function') callback();

            }
         };

         head.insertBefore(script, head.firstChild);
      };

      // Get the number of "own" properties in an object
      var getPropSize = function (obj) {

         var size = 0, key;
         for (key in obj) {
            if (hasProp.call(obj, key)) size++;
         }
         return size;
      };

      // Get the name of an object type as a string
      var getType = function (obj, lc) {
         var str = toString.call(obj).slice(8, -1);
         return (lc && str.toLowerCase()) || str;
      };

      // Get an object from an (optional) context using delimited string notation
      var getObj = function (str, ctx, sep) {

         ctx = ctx || this;
         str = str.split(sep || '.');

         var arr = str.filter(function (num) {
            return num.length;
         }),
            obj = null;

         obj = arr.reduce(function (prev, curr, index, list) {
            if (prev) {
               return prev[list[index]];
            }
         }, ctx);
         return obj;
      };

      // Create an object within an (optional) context using delimited string notation
      var setObj = function (str, ctx, sep) {

         ctx = ctx || this;
         str = str.split(sep || '.');

         var obj, _i, _len;
         for (_i = 0, _len = str.length; _i < _len; _i++) {
            obj = str[_i];
            ctx = (ctx[obj] = ctx[obj] || {});
         }
         return ctx;
      };

      // Return a map of any items in the passed array that match
      // items in the dependency object
      var getDeps = function (arr) {

         var fn = function (item) {
            return deps[item];
         };
         return arr.map(fn, this);
      };

      // Execute a passed function by first reconciling its arguments
      // against the dependency object and then applying any matches
      // directly
      var run = function ( /* fn, args1, args2... */ ) {

         var args    = slice.call(arguments)
            , fn     = args.shift()
            , argStr = fn.toString().replace(CLEAN, '').match(PARAMS);

         var depArr = getDeps(argStr[1].split(',').map(function (val) {
            return val.replace(PARAM, function (match, p1, p2) {
               return p2;
            });
         }));

         depArr.length = depArr.length - args.length;
         return fn.apply(this, depArr.concat(args));
      };

      deps = (getType(props, true) === 'object') ? props : deps;

      // --------------------------- Start Public API ---------------------------

      syringe.register = syringe.add = function (name, dep, bind) {

         if (getType(name, true) === 'object') {
            for (var key in name) {
               if (!hasProp.call(name, key)) continue;
               this.add.apply(this, [key, name[key]]);
            }
            return this;
         }

         if (getType(dep, true) === 'function' && bind) {
            dep = this.on(dep);
         }

         deps[name] = dep;
         return this;
      };

      syringe.unregister = syringe.remove = function (name) {

         var newdeps = {};

         for (var key in deps) {
            if (!hasProp.call(deps, key) || (hasProp.call(deps, name) && key === name)) continue;
            newdeps[key] = deps[key];
         }

         deps = newdeps;
         return this;
      };

      syringe.on = syringe.bind = function (name, fn, ctx) {

         if (getType(name, true) === 'object') {
            for (var key in name) {
               if (!hasProp.call(name, key)) continue;
               this.on.apply(this, [key, name[key]]);
            }
            return this;
         }

         // Is this binding going to be assigned to a name in an optional context?
         if (getType(name, true) === 'string' && getType(fn, true) === 'function') {

            ctx = ctx || root;

            var strArr  = name.split('.')
            , objStr    = (strArr.length > 1) ? strArr.pop() : false;

            fn = run.bind(ctx, fn);

            if (objStr) setObj(strArr.join('.'), ctx)[objStr] = fn;
            else ctx[strArr.join('.')] = fn;
            return this;

         // Is this binding simply going to be returned as an anonymous function?
         } else if (getType(name, true) === 'function') {
            ctx = getType(fn, true) === 'object' ? fn : root;
            return run.bind(ctx, name);
         }
      };

      syringe.get = function (str) {

         if (getType(str, true) === 'string') {
            return (getObj(str, deps) || false);
         }

         return deps;
      };

      syringe.set = function (str, value) {

         var strArr  = str.split('.')
         , objStr    = (strArr.length > 1) ? strArr.pop() : false;

         if (objStr) setObj(strArr.join('.'), deps)[objStr] = value;
         else deps[strArr.toString()] = value;
         return this;
      };

      syringe.fetch = function (map, callback) {

         var self = this,
            count = 0;

         // Keeps a count of the script load events and reconciles it
         // against the length of the script list...
         var stack = function () {

            if (++count === getPropSize(map)) {
               for (var key in map) {
                  if (!hasProp.call(map, key)) continue;
                  self.add(key, getObj(map[key].bind, root));
               }
               callback.call(self);
            }
         };

         // Loop that adds new script element for each list item...
         for (var key in map) {
            if (!hasProp.call(map, key)) continue;
            addScript(map[key].path, stack);
         }
      };

      syringe.$ = root.jQuery || root.Zepto || root.ender || root.$;

      syringe.VERSION = '0.1.8';

      return syringe;

   };
}.call(this));<commit_msg>.create() method added<commit_after>// Array.filter polyfill (MDN)
[].filter||(Array.prototype.filter=function(c,f){if(null==this)throw new TypeError;var b=Object(this),g=b.length>>>0;if("function"!=typeof c)throw new TypeError;for(var d=[],a=0;a<g;a++)if(a in b){var e=b[a];c.call(f,e,a,b)&&d.push(e)}return d});

// Array.map polyfill (MDN)
[].map||(Array.prototype.map=function(d,f){var g,e,a;if(null==this)throw new TypeError(" this is null or not defined");var b=Object(this),h=b.length>>>0;if("function"!==typeof d)throw new TypeError(d+" is not a function");f&&(g=f);e=Array(h);for(a=0;a<h;){var c;a in b&&(c=b[a],c=d.call(g,c,a,b),e[a]=c);a++}return e});

// Array.reduce polyfill (MDN)
"function"!==typeof Array.prototype.reduce&&(Array.prototype.reduce=function(d,e){if(null===this||"undefined"===typeof this)throw new TypeError("Array.prototype.reduce called on null or undefined");if("function"!==typeof d)throw new TypeError(d+" is not a function");var a=0,f=this.length>>>0,b,c=!1;1<arguments.length&&(b=e,c=!0);for(;f>a;++a)this.hasOwnProperty(a)&&(c?b=d(b,this[a],a,this):(b=this[a],c=!0));if(!c)throw new TypeError("Reduce of empty array with no initial value");return b});

// Function.bind polyfill (MDN)
Function.prototype.bind||(Function.prototype.bind=function(b){if("function"!==typeof this)throw new TypeError("Function.prototype.bind - what is trying to be bound is not callable");var d=Array.prototype.slice.call(arguments,1),e=this,a=function(){},c=function(){return e.apply(this instanceof a&&b?this:b,d.concat(Array.prototype.slice.call(arguments)))};a.prototype=this.prototype;c.prototype=new a;return c});

/* jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true,
undef:true, unused:true, curly:true, browser:true, indent:3, maxerr:50, laxcomma:true,
forin:false, curly:false */

// syringe.js v0.1.9
(function () {

   "use strict";

   var root = this, syringe;

   syringe = function (props) {

      // Pointers and containers
      var syringe    = {}
         , deps      = {}
         , hasProp   = {}.hasOwnProperty
         , slice     = [].slice
         , toString  = Object.prototype.toString;

      // Patterns
      var PARAMS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m
         , PARAM = /^\s*(_?)(\S+?)\1\s*$/
         , CLEAN = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;

      // Asynch script-loader/-runner...
      var addScript = function (src, callback) {

         var doc     = document
            , head   = doc.getElementsByTagName("head")[0] || doc.documentElement
            , script = doc.createElement("script")
            , done   = false;

         script.src = src;

         script.onload = script.onreadystatechange = function () {

            if (!done && (!this.readyState || this.readyState === "loaded" || this.readyState === "complete")) {

               done = true;
               script.onload = script.onreadystatechange = null;

               if (head && script.parentNode) head.removeChild(script);
               if (getType(callback, true) === 'function') callback();

            }
         };

         head.insertBefore(script, head.firstChild);
      };

      // Get the number of "own" properties in an object
      var getPropSize = function (obj) {

         var size = 0, key;
         for (key in obj) {
            if (hasProp.call(obj, key)) size++;
         }
         return size;
      };

      // Get the name of an object type as a string
      var getType = function (obj, lc) {
         var str = toString.call(obj).slice(8, -1);
         return (lc && str.toLowerCase()) || str;
      };

      // Get an object from an (optional) context using delimited string notation
      var getObj = function (str, ctx, sep) {

         ctx = ctx || this;
         str = str.split(sep || '.');

         var arr = str.filter(function (num) {
            return num.length;
         }),
            obj = null;

         obj = arr.reduce(function (prev, curr, index, list) {
            if (prev) {
               return prev[list[index]];
            }
         }, ctx);
         return obj;
      };

      // Create an object within an (optional) context using delimited string notation
      var setObj = function (str, ctx, sep) {

         ctx = ctx || this;
         str = str.split(sep || '.');

         var obj, _i, _len;
         for (_i = 0, _len = str.length; _i < _len; _i++) {
            obj = str[_i];
            ctx = (ctx[obj] = ctx[obj] || {});
         }
         return ctx;
      };

      // Return a map of any items in the passed array that match
      // items in the dependency object
      var getDeps = function (arr) {

         var fn = function (item) {
            return deps[item];
         };
         return arr.map(fn, this);
      };

      // Execute a passed function by first reconciling its arguments
      // against the dependency object and then applying any matches
      // directly
      var run = function ( /* fn, args1, args2... */ ) {

         var args    = slice.call(arguments)
            , fn     = args.shift()
            , argStr = fn.toString().replace(CLEAN, '').match(PARAMS);

         var depArr = getDeps(argStr[1].split(',').map(function (val) {
            return val.replace(PARAM, function (match, p1, p2) {
               return p2;
            });
         }));

         depArr.length = depArr.length - args.length;
         return fn.apply(this, depArr.concat(args));
      };

      deps = (getType(props, true) === 'object') ? props : deps;

      // --------------------------- Start Public API ---------------------------

      syringe.register = syringe.add = function (name, dep, bind) {

         if (getType(name, true) === 'object') {
            for (var key in name) {
               if (!hasProp.call(name, key)) continue;
               this.add.apply(this, [key, name[key]]);
            }
            return this;
         }

         if (getType(dep, true) === 'function' && bind) {
            dep = this.on(dep);
         }

         deps[name] = dep;
         return this;
      };

      syringe.unregister = syringe.remove = function (name) {

         var newdeps = {};

         for (var key in deps) {
            if (!hasProp.call(deps, key) || (hasProp.call(deps, name) && key === name)) continue;
            newdeps[key] = deps[key];
         }

         deps = newdeps;
         return this;
      };

      syringe.on = syringe.bind = function (name, fn, ctx) {

         if (getType(name, true) === 'object') {
            for (var key in name) {
               if (!hasProp.call(name, key)) continue;
               this.on.apply(this, [key, name[key]]);
            }
            return this;
         }

         // Is this binding going to be assigned to a name in an optional context?
         if (getType(name, true) === 'string' && getType(fn, true) === 'function') {

            ctx = ctx || root;

            var strArr  = name.split('.')
            , objStr    = (strArr.length > 1) ? strArr.pop() : false;

            fn = run.bind(ctx, fn);

            if (objStr) setObj(strArr.join('.'), ctx)[objStr] = fn;
            else ctx[strArr.join('.')] = fn;
            return this;

         // Is this binding simply going to be returned as an anonymous function?
         } else if (getType(name, true) === 'function') {
            ctx = getType(fn, true) === 'object' ? fn : root;
            return run.bind(ctx, name);
         }
      };

      syringe.get = function (str) {

         if (getType(str, true) === 'string') {
            return (getObj(str, deps) || false);
         }

         return deps;
      };

      syringe.set = function (str, value) {

         var strArr  = str.split('.')
         , objStr    = (strArr.length > 1) ? strArr.pop() : false;

         if (objStr) setObj(strArr.join('.'), deps)[objStr] = value;
         else deps[strArr.toString()] = value;
         return this;
      };

      syringe.fetch = function (map, callback) {

         var self = this,
            count = 0;

         // Keeps a count of the script load events and reconciles it
         // against the length of the script list...
         var stack = function () {

            if (++count === getPropSize(map)) {
               for (var key in map) {
                  if (!hasProp.call(map, key)) continue;
                  self.add(key, getObj(map[key].bind, root));
               }
               callback.call(self);
            }
         };

         // Loop that adds new script element for each list item...
         for (var key in map) {
            if (!hasProp.call(map, key)) continue;
            addScript(map[key].path, stack);
         }
      };

      syringe.$ = root.jQuery || root.Zepto || root.ender || root.$;

      syringe.VERSION = '0.1.9';

      return syringe;

   };

   root.syringe         = syringe();
   root.syringe.create  = syringe;

}.call(this));<|endoftext|>


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

var media = Echo.App.manifest("Echo.StreamServer.Controls.MediaContainer");

if (Echo.App.isDefined(media)) return;

media.dependencies = [{
	"url": "{%= baseURLs.prod %}/sdk-derived/controls/nested-card.js",
	"loaded": function() { return !!Echo.StreamServer.Controls.NestedCard; }
}];

media.labels = {
	/**
	 * @echo_label
	 */
	"plus": "+",
	/**
	 * @echo_label
	 */
	"initialTooltip": "Drag file here or click to upload an image",
	/**
	 * @echo_label
	 */
	"loading": "Loading"
};

media.templates.attachmentsPanel =
	'
' +
		'
' +
			'
' +
				'
{label:plus}
' +
				'{label:initialTooltip}' +
			'
' +
		'
' +
	'
';

media.templates.main = function() {
	return this.config.get("attachmentsPanelRequired")
		? '
' + this.templates.attachmentsPanel + '
'
		: '

';
};

media.config = {
	"dragAndDropOptions": {
		"filepickerOptions": {},
		"onStart": function() {},
		"onSuccess": function() {},
		"onError": function() {}
	},
	"clickOptions": {
		"filepickerOptions": {},
		"beforeCallback": function() {},
		"onSuccess": function() {},
		"onError": function() {}
	},
	"filepickerAPIKey": undefined,
	"allowMultiple": false
};

media.vars = {
	"cards": []
};

media.init = function() {
	this.render();
	this.ready();
};

media.renderers.container = function(element) {
	var media = this.config.get("data", []);
	if (media.length) {
		if (!this.cardParentConfig) {
			this.cardParentConfig = this.config.getAsHash();
		}

		var config = $.extend({
			"target": document.createDocumentFragment(),
			"context": this.config.get("context"),
			"ready": function() {
				element.append(this.config.get("target"));
			}
		}, this.cardParentConfig.card);

		config.parent = this.itemParentConfig;

		this.cards = $.map(media, function(item) {
			return new Echo.StreamServer.Controls.NestedCard($.extend({
				"data": item
			}, config));
		});
	}
	this.changeContainerCapacity(media.length);
	return element;
};

media.renderers.attachmentsPanel = function(element) {
	var self = this;
	var mediaLength = this.config.get("data", []).length;

	Echo.Loader.download([{
		"url": "//api.filepicker.io/v1/filepicker.js",
		"loaded": function() {
			return !!(window.filepicker && window.filepicker.pick);
		}
	}], function() {
		self._initFilePickerPanel();
	});

	if (mediaLength > 0 && !this.config.get("allowMultiple")) {
		this._hideAttachmentsPanel();
	} else {
		this._showAttachmentsPanel();
	}
	return element;
};

media.methods.changeContainerCapacity = function(mediaLength) {
	var capacity = (mediaLength > 1 || (mediaLength === 1 && this.config.get("allowMultiple"))) ? "multiple" : "single";
	var prefix = this.cssPrefix;
	if (capacity === "single") {
		this.view.get("container")
			.removeClass(prefix + "multiple")
			.addClass(prefix + "single");
	} else {
		this.view.get("container")
			.removeClass(prefix + "single")
			.addClass(prefix + "multiple");
	}
};

media.methods._showAttachmentsPanel = function() {
	var panel = this.view.get("attachmentsPanel");
	if (!panel) return;
	this._changePanelLayoutState("normal");
	// this append call also helps to keep attachmentsPanel
	// as the last of mediaContainer DOM element childrens
	this.view.get("container").append(panel);
	// we use css() instead of show to avoid adding of "display: block"
	// which we don`t need here.
	// TODO: it should be replaced by animation or proper show function.
	panel.css("display", "");
};

media.methods._hideAttachmentsPanel = function() {
	var panel = this.view.get("attachmentsPanel");
	panel && panel.hide();
};

media.methods._changePanelLayoutState = function(state) {
	state = state || "normal";
	if (state === "loading") {
		this.view.get("attachmentsPanel-plus").addClass(this.cssPrefix + "loading-animation").empty();
		this.view.get("attachmentsPanel-tooltip").text(this.labels.get("loading"));
	} else {
		// filepicker widget adds "disabled" attribute to panel DOMElement on loading start, but doesn`t remove it.
		this.view.get("attachmentsPanel").removeAttr("disabled");
		this.view.get("attachmentsPanel-tooltip").text(this.labels.get("initialTooltip"));
		this.view.get("attachmentsPanel-plus").text(this.labels.get("plus")).removeClass(this.cssPrefix + "loading-animation");
	}
};

media.methods._initFilePickerPanel = function() {
	var self = this;
	var panel = this.view.get("attachmentsPanel");

	var filepickerAPIKey = this.config.get("filepickerAPIKey");
	if (!filepickerAPIKey) return;

	var panelOptions = this.config.get("dragAndDropOptions");
	if (!panelOptions) return;

	window.filepicker.setKey(filepickerAPIKey);
	window.filepicker.makeDropPane(panel[0], $.extend({}, panelOptions.filepickerOptions, {
		"onStart": function(files) {
			self._changePanelLayoutState("loading");
			panelOptions.onStart.apply(this, arguments);
		},
		"onSuccess": function(InkBlobs) {
			panelOptions.onSuccess.apply(this, arguments);
		},
		"onError": function(type, message) {
			panelOptions.onError.apply(this, arguments);
		}
	}));

	var clickOptions = this.config.get("clickOptions");
	if (!clickOptions) return;

	panel.click(function(event) {
		self._changePanelLayoutState("loading");
		clickOptions.beforeCallback.apply(this, arguments);
		window.filepicker.pick(clickOptions.filepickerOptions,
			function(InkBlob) {
				clickOptions.onSuccess.apply(this, arguments);
			},
			function(FPError) {
				self._changePanelLayoutState("normal");
				clickOptions.onError.apply(this, arguments);
			}
		);
	});
};

media.destroy = function() {
	$.each(this.cards, function(i, card) {
		card.destroy();
	});
};

var gradientStyle = function(value) {
	return $.map(["", "-webkit-", "-moz-", "-ms-", "-o-"], function(prefix) {
		return "background-image: " + prefix + "linear-gradient(" + value + ");";
	}).join("");
};

media.css =
	'.{class:container} { line-height: 1px; word-wrap: normal; white-space: nowrap; overflow-x: auto; overflow-y: hidden; }' +
	'.{class:container} > div { display: inline-block; max-width: 90%; vertical-align: top; }' +
	'.{class:container} > div > div { margin-right: 8px; }' +

	// single cards
	'.{class:container}.{class:single} { padding: 0px; border: 0px; }' +
	'.{class:container}.{class:single} > div { max-width: 100%; display: block; }' +
	'.{class:container}.{class:single} > div > div { margin-right: 0; }' +

	// multiple cards
	'.{class:container}.{class:multiple} { padding: 8px 0px 8px 8px; }' +

	// scrollbar
	'.{class:container}::-webkit-scrollbar { height: 10px; }' +
	'.{class:container}::-webkit-scrollbar-track { box-shadow: inset 0 0 6px rgba(0,0,0,0.3); }' +
	'.{class:container}::-webkit-scrollbar-thumb { background: #D2D2D2; box-shadow: inset 0 0 6px rgba(0,0,0,0.5); }' +

	// single > attachments panel
	'.{class:single} > .{class:attachmentsPanel} { border: 1px solid #D8D8D8; border-bottom: 0; }' +

	// multiple > attachments panel
	'.{class:container}.{class:multiple} > .{class:attachmentsPanel} { border: 1px solid #C2C2C2; width: 100%; margin-right: 8px; }' +

	// attachments panel
	'.{class:container} > div.{class:attachmentsPanel} { height: 100%; max-height: 388px; cursor: pointer; background-color: #eee; text-align: center; font-size: 16px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #9f9f9f; font-weight: normal; padding: 1px 0 0 0; max-width: 100%; }' +
	'.{class:attachmentsPanel-plus} { font-size: 128px; line-height: 128px; font-weight: bold; width: 128px; height: 128px; margin: 0 auto; }' +
	'.{class:strippedBackground} { ' +
		gradientStyle("left top, #e3e3e3 0%, #e3e3e3 25%, #eee 25%, #eee 50%, #e3e3e3 50%, #e3e3e3 75%, #eee 75%") + '; ' +
		'filter: progid:DXImageTransform.Microsoft.gradient( startColorstr="#e3e3e3", endColorstr="#eeeeee",GradientType=0 ); ' +
		'background-size: 70px 70px;' +
	'}' +
	'.{class:container} > .{class:attachmentsPanel} > .{class:attachmentsPanel-wrapper} { border: 1px solid #C4C4C4; margin: 5px; border-radius: 2px; }' +
	'.{class:attachmentsPanel-container} { padding: 10px 0 40px 0; margin: 5px; }' +
	'.{class:attachmentsPanel-tooltip} { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px;}' +
	'.{class:loading-animation} {background-image: url("{%= baseURLs.prod %}/images/loading.gif"); background-size: 80px; background-repeat: no-repeat; background-position: 24px 39px; }';

Echo.App.create(media);

})(Echo.jQuery);
<commit_msg>attachmentsPanel width is reduced; panel border changed;<commit_after>(function($) {
"use strict";

var media = Echo.App.manifest("Echo.StreamServer.Controls.MediaContainer");

if (Echo.App.isDefined(media)) return;

media.dependencies = [{
	"url": "{%= baseURLs.prod %}/sdk-derived/controls/nested-card.js",
	"loaded": function() { return !!Echo.StreamServer.Controls.NestedCard; }
}];

media.labels = {
	/**
	 * @echo_label
	 */
	"plus": "+",
	/**
	 * @echo_label
	 */
	"initialTooltip": "Drag file here or click to upload an image",
	/**
	 * @echo_label
	 */
	"loading": "Loading"
};

media.templates.attachmentsPanel =
	'
' +
		'
' +
			'
' +
				'
{label:plus}
' +
				'{label:initialTooltip}' +
			'
' +
		'
' +
	'
';

media.templates.main = function() {
	return this.config.get("attachmentsPanelRequired")
		? '
' + this.templates.attachmentsPanel + '
'
		: '

';
};

media.config = {
	"dragAndDropOptions": {
		"filepickerOptions": {},
		"onStart": function() {},
		"onSuccess": function() {},
		"onError": function() {}
	},
	"clickOptions": {
		"filepickerOptions": {},
		"beforeCallback": function() {},
		"onSuccess": function() {},
		"onError": function() {}
	},
	"filepickerAPIKey": undefined,
	"allowMultiple": false
};

media.vars = {
	"cards": []
};

media.init = function() {
	this.render();
	this.ready();
};

media.renderers.container = function(element) {
	var media = this.config.get("data", []);
	if (media.length) {
		if (!this.cardParentConfig) {
			this.cardParentConfig = this.config.getAsHash();
		}

		var config = $.extend({
			"target": document.createDocumentFragment(),
			"context": this.config.get("context"),
			"ready": function() {
				element.append(this.config.get("target"));
			}
		}, this.cardParentConfig.card);

		config.parent = this.itemParentConfig;

		this.cards = $.map(media, function(item) {
			return new Echo.StreamServer.Controls.NestedCard($.extend({
				"data": item
			}, config));
		});
	}
	this.changeContainerCapacity(media.length);
	return element;
};

media.renderers.attachmentsPanel = function(element) {
	var self = this;
	var mediaLength = this.config.get("data", []).length;

	Echo.Loader.download([{
		"url": "//api.filepicker.io/v1/filepicker.js",
		"loaded": function() {
			return !!(window.filepicker && window.filepicker.pick);
		}
	}], function() {
		self._initFilePickerPanel();
	});

	if (mediaLength > 0 && !this.config.get("allowMultiple")) {
		this._hideAttachmentsPanel();
	} else {
		this._showAttachmentsPanel();
	}
	return element;
};

media.methods.changeContainerCapacity = function(mediaLength) {
	var capacity = (mediaLength > 1 || (mediaLength === 1 && this.config.get("allowMultiple"))) ? "multiple" : "single";
	var prefix = this.cssPrefix;
	if (capacity === "single") {
		this.view.get("container")
			.removeClass(prefix + "multiple")
			.addClass(prefix + "single");
	} else {
		this.view.get("container")
			.removeClass(prefix + "single")
			.addClass(prefix + "multiple");
	}
};

media.methods._showAttachmentsPanel = function() {
	var panel = this.view.get("attachmentsPanel");
	if (!panel) return;
	this._changePanelLayoutState("normal");
	// this append call also helps to keep attachmentsPanel
	// as the last of mediaContainer DOM element childrens
	this.view.get("container").append(panel);
	// we use css() instead of show to avoid adding of "display: block"
	// which we don`t need here.
	// TODO: it should be replaced by animation or proper show function.
	panel.css("display", "");
};

media.methods._hideAttachmentsPanel = function() {
	var panel = this.view.get("attachmentsPanel");
	panel && panel.hide();
};

media.methods._changePanelLayoutState = function(state) {
	state = state || "normal";
	if (state === "loading") {
		this.view.get("attachmentsPanel-plus").addClass(this.cssPrefix + "loading-animation").empty();
		this.view.get("attachmentsPanel-tooltip").text(this.labels.get("loading"));
	} else {
		// filepicker widget adds "disabled" attribute to panel DOMElement on loading start, but doesn`t remove it.
		this.view.get("attachmentsPanel").removeAttr("disabled");
		this.view.get("attachmentsPanel-tooltip").text(this.labels.get("initialTooltip"));
		this.view.get("attachmentsPanel-plus").text(this.labels.get("plus")).removeClass(this.cssPrefix + "loading-animation");
	}
};

media.methods._initFilePickerPanel = function() {
	var self = this;
	var panel = this.view.get("attachmentsPanel");

	var filepickerAPIKey = this.config.get("filepickerAPIKey");
	if (!filepickerAPIKey) return;

	var panelOptions = this.config.get("dragAndDropOptions");
	if (!panelOptions) return;

	window.filepicker.setKey(filepickerAPIKey);
	window.filepicker.makeDropPane(panel[0], $.extend({}, panelOptions.filepickerOptions, {
		"onStart": function(files) {
			self._changePanelLayoutState("loading");
			panelOptions.onStart.apply(this, arguments);
		},
		"onSuccess": function(InkBlobs) {
			panelOptions.onSuccess.apply(this, arguments);
		},
		"onError": function(type, message) {
			panelOptions.onError.apply(this, arguments);
		}
	}));

	var clickOptions = this.config.get("clickOptions");
	if (!clickOptions) return;

	panel.click(function(event) {
		self._changePanelLayoutState("loading");
		clickOptions.beforeCallback.apply(this, arguments);
		window.filepicker.pick(clickOptions.filepickerOptions,
			function(InkBlob) {
				clickOptions.onSuccess.apply(this, arguments);
			},
			function(FPError) {
				self._changePanelLayoutState("normal");
				clickOptions.onError.apply(this, arguments);
			}
		);
	});
};

media.destroy = function() {
	$.each(this.cards, function(i, card) {
		card.destroy();
	});
};

var gradientStyle = function(value) {
	return $.map(["", "-webkit-", "-moz-", "-ms-", "-o-"], function(prefix) {
		return "background-image: " + prefix + "linear-gradient(" + value + ");";
	}).join("");
};

media.css =
	'.{class:container} { line-height: 1px; word-wrap: normal; white-space: nowrap; overflow-x: auto; overflow-y: hidden; }' +
	'.{class:container} > div { display: inline-block; max-width: 90%; vertical-align: top; }' +
	'.{class:container} > div > div { margin-right: 8px; }' +

	// single cards
	'.{class:container}.{class:single} { padding: 0px; border: 0px; }' +
	'.{class:container}.{class:single} > div { max-width: 100%; display: block; }' +
	'.{class:container}.{class:single} > div > div { margin-right: 0; }' +

	// multiple cards
	'.{class:container}.{class:multiple} { padding: 8px 0px 8px 8px; }' +

	// scrollbar
	'.{class:container}::-webkit-scrollbar { height: 10px; }' +
	'.{class:container}::-webkit-scrollbar-track { box-shadow: inset 0 0 6px rgba(0,0,0,0.3); }' +
	'.{class:container}::-webkit-scrollbar-thumb { background: #D2D2D2; box-shadow: inset 0 0 6px rgba(0,0,0,0.5); }' +

	// single > attachments panel
	'.{class:single} > .{class:attachmentsPanel} { border: 1px solid #D8D8D8; border-bottom: 0; }' +

	// multiple > attachments panel
	'.{class:container}.{class:multiple} > .{class:attachmentsPanel} { width: 90%; }' +
	'.{class:container}.{class:multiple} > .{class:attachmentsPanel} > .{class:attachmentsPanel-wrapper} { margin: 0px 8px 0px 0px; }' +

	// attachments panel
	'.{class:container} > div.{class:attachmentsPanel} { height: 100%; max-height: 388px; cursor: pointer; background-color: #f1f1f1; text-align: center; font-size: 16px; font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; color: #9f9f9f; font-weight: normal; padding: 1px 0 0 0; max-width: 100%; }' +
	'.{class:attachmentsPanel-plus} { font-size: 128px; line-height: 128px; font-weight: bold; width: 128px; height: 128px; margin: 0 auto; }' +
	'.{class:strippedBackground} { ' +
		gradientStyle("left top, #e3e3e3 0%, #e3e3e3 25%, #eee 25%, #eee 50%, #e3e3e3 50%, #e3e3e3 75%, #eee 75%") + '; ' +
		'filter: progid:DXImageTransform.Microsoft.gradient( startColorstr="#e3e3e3", endColorstr="#eeeeee",GradientType=0 ); ' +
		'background-size: 70px 70px;' +
	'}' +
	'.{class:container} > .{class:attachmentsPanel} > .{class:attachmentsPanel-wrapper} { border: 1px solid #C4C4C4; margin: 5px; border-radius: 2px; }' +
	'.{class:attachmentsPanel-container} { padding: 10px 0 40px 0; margin: 5px; }' +
	'.{class:attachmentsPanel-tooltip} { font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 14px;}' +
	'.{class:loading-animation} {background-image: url("{%= baseURLs.prod %}/images/loading.gif"); background-size: 80px; background-repeat: no-repeat; background-position: 24px 39px; }';

Echo.App.create(media);

})(Echo.jQuery);
<|endoftext|>


<commit_before>// The number of frames rendered per second.
const FPS = 60;

// The number of pixels per frame that the global velocity will increase by
// when positive sentiment text messages are received.
//
// Units: Pixels per frame per net positive sentiment.
// I.E. Acceleration for positive sentiment text messages.
const VELOCITY_INTERVAL = 0.5;

// The velocity at which things will move at "hyperspeed"
const HYPERSPEED_VELOCITY = 500;

const socket = io();
const randInt = (max) => {
    return Math.floor(Math.random() * max);
};

const canvas = document.getElementById('screen');
const ctx = canvas.getContext('2d');

let globalScreenWidth = $('#screen-container').width();
let globalScreenHeight = window.innerHeight;

// The number of pixels per frame that all non ship objects will move by.
let globalVelocity = 0;

let positiveMessages = 0;

// 0 -> Normal
// 1 -> White stars become lines
// 2 -> rainbow stars
let stage = 0;

class Element {

  constructor(x, y, vx, vy, width, height, color) {
    // Current x position
    this.x = x;

    // Current x position
    this.y = y;

    // Current horizontal velocity
    this.vx = vx || 0;

    // Current vertical velocity
    this.vy = vy || 0;

    if (width !== undefined) {
      this.width = width;
    }

    if (height !== undefined) {
      this.height = height;
    }

    if (color !== undefined) {
      this.color = color;
    }
  }

  draw() {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  move() {
    this.x += this.vx;
    this.y += this.vy;
  }

}

class Sprite extends Element {
  constructor(x, y, vx, vy, width, height, src) {
    super(x, y, vx, vy, width, height);

    this.image = new Image();
    this.image.src = src;
  }

  draw() {
    ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
  }
}

class Star extends Element {

  constructor(y) {
    const width = randInt(globalScreenWidth);
    super(width, y, 0, globalVelocity, 5, 5);
    this.colors = ['#FFFFFF', '#ff0000', '#0000ff', '#00ff00', '#ffff00', '#8a2be2'];

    if (stage > 0) {
      this.color = this.colors[randInt(this.colors.length)];
    } else {
      this.color = '#FFFFFF'
    }
  }

  move() {
    this.y += globalVelocity;
  }

  draw() {
    let rectStart = this.y;
    let rectHeight = this.height;

    if (stage === 1) {
      rectStart = this.hyperspeedStart;
      rectHeight = this.y - this.hyperspeedStart;

      if (allStarsAreAtTheBottom()) {
        this.color = this.colors[randInt(this.colors.length)];
      }
    }

    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, rectStart, this.width, rectHeight);
    ctx.fillStyle = '#000000';
  }
}

class Laser extends Element {
  constructor() {
    const x = randInt(globalScreenWidth);
    super(x, 0, 0, 5, 5, 50);
    this.color = '#ff0000';
  }
}

class Gauge extends Element {
  constructor(x, y, shipHeight) {
    // shipHeight because I am trying to line the gauge up with the ship vertically.
    super(x, y, 0, 0, 100, globalScreenHeight - shipHeight - y);
    this.color = '#ff0000';
  }

  draw() {
    if (stage === 0) {
      // How far up the rectangle will be drawn.
      let rectHeight = (globalVelocity/HYPERSPEED_VELOCITY) * this.height;

      // Draw the border.
      ctx.strokeStyle = '#FFFFFF';
      ctx.strokeRect(this.x, this.y, this.width, this.height);

      // Draw the gauge.
      ctx.fillStyle = this.color;
      ctx.fillRect(this.x, this.y + this.height, this.width, -rectHeight);
      ctx.fillStyle = '#000000';
    }
  }
}

let sky = new Element(0, 0, 0, 0, globalScreenWidth, globalScreenHeight - 100, '#3399ff');
let ground = new Element(0, sky.height, 0, 0, globalScreenWidth, globalScreenHeight - sky.height, '#CD853F');
let atmosphereStage1 = new Element(0, 0, 0, 0, globalScreenWidth, globalScreenHeight, ' #0000FF');
let ship = new Sprite(globalScreenWidth/2, sky.height - ground.height, 0, 0, 125, 125, 'http://i.imgur.com/TUZwV9i.png');
let gauge = new Gauge(7*globalScreenWidth/8, globalScreenHeight/6, ship.height);

let initializeStars = () => {
  // Generate randomly placed stars.
  let arr = [];

  for(let i = 0; i < 100; i += 1) {
    let randomHeight = randInt(globalScreenHeight);
    let star = new Star(randomHeight);

    arr.push(star);
  }

  return arr;
};

// All of the game elements on the screen.
let elements = initializeStars();

// Checks to see if all of the stars have reached the bottom of the screen
// during hyperspace. If so, we will reset the grid.
let allStarsAreAtTheBottom = () => {
  for (let element of elements) {
    if (element instanceof Star) {
      if (element.y < globalScreenHeight) {
        return false;
      }
    }
  }

  return true;
};

// Function to be executed for each frame.
let gameLoop = () => {
  canvas.height = globalScreenHeight;
  canvas.width = globalScreenWidth;

  globalScreenWidth = $('#screen-container').width();
  globalScreenHeight = window.innerHeight;

  if (stage === 0 && globalVelocity > HYPERSPEED_VELOCITY) {
    stage = 1;
    globalVelocity = 5;

    // Remove the gauge from the elements array.
    console.log('Removing gauge.');
    elements.splice(elements.length - 1, 1);

    // Set a marker for where each star was when hyperspeed was started.
    for (let element of elements) {
      if (element instanceof Star) {
        element.hyperspeedStart = element.y;
      }
    }
  }

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw all of the elements on the screen.
  for (let i = 0; i < elements.length; i++) {
    if (randInt(10000) === 10) {
      // console.log('Still drawing');
    }

    if (elements[i].y >= 0 && elements[i].y <= globalScreenHeight) {
      // elements[i] is currently on the screen so it can be moved.
      elements[i].move();
    } else if (stage === 1) {
      elements[i].move = function() {};

      if (allStarsAreAtTheBottom()) {
        elements = initializeStars();
        elements.push(ship);
        globalVelocity = 10;

        // Kick it into overdrive.
        stage = 2;
      }
    } else {

      // elements[i] is off the screen. Spawn a new star.
      let newPosition = 0;
      if (globalVelocity < 0) {
        newPosition = globalScreenHeight;
      }

      let newStar = new Star(newPosition);
      // if (isHyperspeed) {
      //   newStar.hyperspeedStart = 0;
      // }
      elements[i] = newStar;
    }

    elements[i].draw();
  }
};

// Listen for SMS events.
socket.on('sms', (sentiment) => {
  console.log(sentiment);
  if (stage > 0) {
    return;
  }

  // Positive messages increase the velocity. Negative messages fires a laser.
  if (sentiment === 'positive') {
    positiveMessages += 1;
    if (positiveMessages % 10 === 0) {
      globalVelocity += VELOCITY_INTERVAL;
    }
  } else if (sentiment === 'negative') {
    if (stage === 0) {
      elements.push(new Laser());
    }
  }
});

// Modify earth's move function to make it move like one of the stars.
// Why did I do this here? Because I needed it to work quickly.
// I didn't use () => {} because I don't want this to bind from the current scope.
sky.move = ground.move = function() {
  this.y += globalVelocity;
};

// Push the new elements onto the array.
elements.push(sky);
elements.push(ground);
elements.push(ship);
elements.push(gauge);

// Run the game loop forever. FOREVER.
setInterval(gameLoop, 1000 / FPS);
<commit_msg>Change the way lasers are fired and add new tweakable values<commit_after>// The number of frames rendered per second.
const FPS = 60;

// The number of pixels per frame that the global velocity will increase by
// when positive sentiment text messages are received.
//
// Units: Pixels per frame per net positive sentiment.
// I.E. Acceleration for positive sentiment text messages.
const VELOCITY_INTERVAL = 1;

// The velocity at which things will move at "hyperspeed"
const HYPERSPEED_VELOCITY = 25;

// The number of positive messages before velocity increases.
// If this is 20 then every 20 positive messages will increase the velocity.
const POSITIVE_MESSAGE_LIMIT = 25;

// The number of negative messages required to fire a laser.
const NEGATIVE_MESSAGE_LIMIT = 5;

const NUMBER_OF_STARS = 100;

const socket = io();
const randInt = (max) => {
    return Math.floor(Math.random() * max);
};

const canvas = document.getElementById('screen');
const ctx = canvas.getContext('2d');

let globalScreenWidth = $('#screen-container').width();
let globalScreenHeight = window.innerHeight;

// The number of pixels per frame that all non ship objects will move by.
let globalVelocity = 0;

// Number of positive messages since the last velocity increase.
let positiveMessages = 0;

// Number of negative messages since the last laser was fired.
let negativeMessages = 0;

// 0 -> Normal
// 1 -> White stars become lines
// 2 -> rainbow stars
let stage = 0;

class Element {

  constructor(x, y, vx, vy, width, height, color) {
    // Current x position
    this.x = x;

    // Current x position
    this.y = y;

    // Current horizontal velocity
    this.vx = vx || 0;

    // Current vertical velocity
    this.vy = vy || 0;

    if (width !== undefined) {
      this.width = width;
    }

    if (height !== undefined) {
      this.height = height;
    }

    if (color !== undefined) {
      this.color = color;
    }
  }

  draw() {
    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, this.y, this.width, this.height);
  }

  move() {
    this.x += this.vx;
    this.y += this.vy;
  }

}

class Sprite extends Element {
  constructor(x, y, vx, vy, width, height, src) {
    super(x, y, vx, vy, width, height);

    this.image = new Image();
    this.image.src = src;
  }

  draw() {
    ctx.drawImage(this.image, this.x, this.y, this.width, this.height);
  }
}

class Star extends Element {

  constructor(y) {
    const width = randInt(globalScreenWidth);
    super(width, y, 0, globalVelocity, 5, 5);
    this.colors = ['#FFFFFF', '#ff0000', '#0000ff', '#00ff00', '#ffff00', '#8a2be2'];

    if (stage > 0) {
      this.color = this.colors[randInt(this.colors.length)];
    } else {
      this.color = '#FFFFFF'
    }
  }

  move() {
    this.y += globalVelocity;
  }

  draw() {
    let rectStart = this.y;
    let rectHeight = this.height;

    if (stage === 1) {
      rectStart = this.hyperspeedStart;
      rectHeight = this.y - this.hyperspeedStart;

      if (allStarsAreAtTheBottom()) {
        this.color = this.colors[randInt(this.colors.length)];
      }
    }

    ctx.fillStyle = this.color;
    ctx.fillRect(this.x, rectStart, this.width, rectHeight);
    ctx.fillStyle = '#000000';
  }
}

class Laser extends Element {
  constructor() {
    const x = randInt(globalScreenWidth);
    super(x, 0, 0, 5, 5, 50);
    this.color = '#ff0000';
  }

  draw() {
    if (stage === 0) {
      ctx.fillStyle = this.color;
      ctx.fillRect(this.x, this.y, this.width, this.height);
    }
  }
}

class Gauge extends Element {
  constructor(x, y, shipHeight) {
    // shipHeight because I am trying to line the gauge up with the ship vertically.
    super(x, y, 0, 0, 100, globalScreenHeight - shipHeight - y);
    this.color = '#ff0000';
  }

  draw() {
    if (stage === 0) {
      // How far up the rectangle will be drawn.
      let rectHeight = (globalVelocity/HYPERSPEED_VELOCITY) * this.height;

      // Draw the border.
      ctx.strokeStyle = '#FFFFFF';
      ctx.strokeRect(this.x, this.y, this.width, this.height);

      // Draw the gauge.
      ctx.fillStyle = this.color;
      ctx.fillRect(this.x, this.y + this.height, this.width, -rectHeight);
      ctx.fillStyle = '#000000';
    }
  }
}

let sky = new Element(0, 0, 0, 0, globalScreenWidth, globalScreenHeight - 100, '#3399ff');
let ground = new Element(0, sky.height, 0, 0, globalScreenWidth, globalScreenHeight - sky.height, '#CD853F');
let atmosphereStage1 = new Element(0, 0, 0, 0, globalScreenWidth, globalScreenHeight, ' #0000FF');
let ship = new Sprite(globalScreenWidth/2, sky.height - ground.height, 0, 0, 125, 125, 'http://i.imgur.com/TUZwV9i.png');
let gauge = new Gauge(7*globalScreenWidth/8, globalScreenHeight/6, ship.height);

let initializeStars = () => {
  // Generate randomly placed stars.
  let arr = [];

  for(let i = 0; i < NUMBER_OF_STARS; i += 1) {
    let randomHeight = randInt(globalScreenHeight);
    let star = new Star(randomHeight);

    arr.push(star);
  }

  return arr;
};

// All of the game elements on the screen.
let elements = initializeStars();

// Checks to see if all of the stars have reached the bottom of the screen
// during hyperspace. If so, we will reset the grid.
let allStarsAreAtTheBottom = () => {
  for (let element of elements) {
    if (element instanceof Star) {
      if (element.y < globalScreenHeight) {
        return false;
      }
    }
  }

  return true;
};

// Function to be executed for each frame.
let gameLoop = () => {
  canvas.height = globalScreenHeight;
  canvas.width = globalScreenWidth;

  globalScreenWidth = $('#screen-container').width();
  globalScreenHeight = window.innerHeight;

  if (stage === 0 && globalVelocity > HYPERSPEED_VELOCITY) {
    stage = 1;
    globalVelocity = 5;

    // Remove the gauge from the elements array.
    console.log('Removing gauge.');
    elements.splice(elements.length - 1, 1);

    // Set a marker for where each star was when hyperspeed was started.
    for (let element of elements) {
      if (element instanceof Star) {
        element.hyperspeedStart = element.y;
      }
    }
  }

  ctx.clearRect(0, 0, canvas.width, canvas.height);

  // Draw all of the elements on the screen.
  for (let i = 0; i < elements.length; i++) {
    if (elements[i].y >= 0 && elements[i].y <= globalScreenHeight) {
      // elements[i] is currently on the screen so it can be moved.
      elements[i].move();
    } else if (stage === 1) {
      elements[i].move = function() {};

      if (allStarsAreAtTheBottom()) {
        elements = initializeStars();
        elements.push(ship);
        globalVelocity = 10;

        // Kick it into overdrive.
        stage = 2;
      }
    } else {

      // elements[i] is off the screen. Spawn a new star.
      let newPosition = 0;
      if (globalVelocity < 0) {
        newPosition = globalScreenHeight;
      }

      let newStar = new Star(newPosition);
      // if (isHyperspeed) {
      //   newStar.hyperspeedStart = 0;
      // }
      elements[i] = newStar;
    }

    elements[i].draw();
  }
};

// Listen for SMS events.
socket.on('sms', (sentiment) => {
  console.log(sentiment);
  if (stage > 0) {
    return;
  }

  // Positive messages increase the velocity. Negative messages fires a laser.
  if (sentiment === 'positive') {
    positiveMessages += 1;
    if (positiveMessages > POSITIVE_MESSAGE_LIMIT) {
      positiveMessages = 0;
      globalVelocity += VELOCITY_INTERVAL;
    }
  } else if (sentiment === 'negative') {
    negativeMessages += 1;
    if (negativeMessages > NEGATIVE_MESSAGE_LIMIT) {
      negativeMessages = 0;
      elements.push(new Laser());
    }
  }
});

// Modify earth's move function to make it move like one of the stars.
// Why did I do this here? Because I needed it to work quickly.
// I didn't use () => {} because I don't want this to bind from the current scope.
sky.move = ground.move = function() {
  this.y += globalVelocity;
};

// Push the new elements onto the array.
elements.push(sky);
elements.push(ground);
elements.push(ship);
elements.push(gauge);

// Run the game loop forever. FOREVER.
setInterval(gameLoop, 1000 / FPS);
<|endoftext|>


<commit_before>const got = require('got');
const url = require('url');
const stripIndents = require('common-tags').stripIndents;
exports.run = (bot, msg, args) => {
    let parsed = bot.utils.parseArgs(args, ['l:']);
    let lang = parsed.options.l || '';

    let code = parsed.leftover.join(' ');

    try {
        let evaled = eval(code);
        if (typeof evaled !== 'string') {
            evaled = require('util').inspect(evaled);
        }
        msg.delete();
        let output = clean(evaled).replace(bot.token, 'BOT_TOKEN' + String.fromCharCode(8203));
        got.post(url.resolve('https://hastebin.com', 'documents'), {
            body: output,
            json: true,
            headers: {
                'Content-Type': 'text/plain'
            }
        }).then(res => {
            if (!res.body || !res.body.key) {
                return 'Failed to upload, no key was returned!';
            }
            let output2 = res.body.key || res.body;
            msg.channel.send({
                embed: bot.utils.embed('', stripIndents`
                **Input:**\n\`\`\`js\n${code}\n\`\`\`
                **Output:**${evaled.split('').length < 1900 ? `\n\`\`\`${lang}\n${output}\n\`\`\`` : `\nhttps://hastebin.com/${output2}\n`}
            `)});
        });
    } catch (err) {
        msg.delete();
        msg.channel.send({
            embed: bot.utils.embed('', `**Input:**\n\`\`\`js\n${code}\n\`\`\`\n:x: **Error!**\n\`\`\`xl\n${clean(err)}\n\`\`\``, [], {
                color: '#ff0000'
            })}).then(m => m.delete(15000));
    }
};

function clean(text) {
    if (typeof (text) === 'string') {
        return text.replace(/`/g, '`' + String.fromCharCode(8203)).replace(/@/g, '@' + String.fromCharCode(8203));
    }
    else {
        return text;
    }
}

exports.info = {
    name: 'eval',
    usage: 'eval <code>',
    description: 'Evaluates arbitrary JavaScript',
    options: [
        {
            name: '-l',
            usage: '-l <lang>',
            description: 'Sets the output code-block syntax language'
        }
    ]
};
<commit_msg>Last minute change<commit_after>const got = require('got');
const url = require('url');
const stripIndents = require('common-tags').stripIndents;
exports.run = (bot, msg, args) => {
    let parsed = bot.utils.parseArgs(args, ['l:']);
    let lang = parsed.options.l || '';

    let code = parsed.leftover.join(' ');

    try {
        let evaled = eval(code);
        if (typeof evaled !== 'string') {
            evaled = require('util').inspect(evaled);
        }
        msg.delete();
        let output = clean(evaled).replace(bot.token, 'BOT_TOKEN' + String.fromCharCode(8203));
        got.post(url.resolve('https://hastebin.com', 'documents'), {
            body: output,
            json: true,
            headers: {
                'Content-Type': 'text/plain'
            }
        }).then(res => {
            if (!res.body || !res.body.key) {
                return 'Failed to upload, no key was returned!';
            }
            let output2 = res.body.key || res.body;
            msg.channel.send({
                embed: bot.utils.embed('', stripIndents`
                **Input:**\n\`\`\`js\n${code}\n\`\`\`
                **Output:**${evaled.split('').length < 1500 ? `\n\`\`\`${lang}\n${output}\n\`\`\`` : `\nhttps://hastebin.com/${output2}\n`}
            `)});
        });
    } catch (err) {
        msg.delete();
        msg.channel.send({
            embed: bot.utils.embed('', `**Input:**\n\`\`\`js\n${code}\n\`\`\`\n:x: **Error!**\n\`\`\`xl\n${clean(err)}\n\`\`\``, [], {
                color: '#ff0000'
            })}).then(m => m.delete(15000));
    }
};

function clean(text) {
    if (typeof (text) === 'string') {
        return text.replace(/`/g, '`' + String.fromCharCode(8203)).replace(/@/g, '@' + String.fromCharCode(8203));
    }
    else {
        return text;
    }
}

exports.info = {
    name: 'eval',
    usage: 'eval <code>',
    description: 'Evaluates arbitrary JavaScript',
    options: [
        {
            name: '-l',
            usage: '-l <lang>',
            description: 'Sets the output code-block syntax language'
        }
    ]
};
<|endoftext|>


<commit_before>/*global window: false, $: false, pageTracker: false */

/**
 * bootstrapped kanso code,
 * required before the commonjs environment is available.
 */

(function (exports) {

    exports.getBaseURL = function () {
        var re = new RegExp('(.*\\/_rewrite).*$');
        var match = re.exec(window.location.pathname);
        if (match) {
            return match[1];
        }
        return '';
    };

    exports.moduleCache = {};

    exports.normalizePath = function (p) {
        var path = [];
        var parts = p.split('/');
        for (var i = 0; i < parts.length; i += 1) {
            if (parts[i] === '..') {
                path.pop();
            }
            else if (parts[i] !== '.') {
                path.push(parts[i]);
            }
        }
        return path.join('/');
    };

    exports.getPropertyPath = function (obj, p) {
        // normalize to remove unessecary . and .. from paths
        var parts = exports.normalizePath(p).split('/');

        // if path is empty, return the root object
        if (!p) {
            return obj;
        }

        // loop through all parts of the path, throwing an exception
        // if a property doesn't exist
        var a = obj;
        for (var i = 0; i < parts.length; i += 1) {
            var x = parts[i];
            if (a[x] === undefined) {
                throw new Error('Invalid path: ' + p);
            }
            a = a[x];
        }
        return a;
    };

    exports.dirname = function (p) {
        if (p === '/') {
            return p;
        }
        var parts = p.split('/');
        parts.pop();
        if (parts.length === 1 && parts[0] === '') {
            return '/';
        }
        return parts.join('/');
    };

    exports.createRequire = function (current) {
        return function (target) {
            if (!exports.design_doc) {
                throw new Error('no design doc loaded');
            }
            var path;
            if (target.charAt(0) === '.') {
                var dir = exports.dirname(current);
                path = exports.normalizePath(dir + '/' + target);
            }
            else {
                path = exports.normalizePath(target);
            }
            if (!exports.moduleCache[path]) {
                var module = {exports: {}};
                var fn;
                eval('fn = (function (module, exports, require) {' +
                    exports.getPropertyPath(exports.design_doc, path) +
                '});');
                fn(module, module.exports, exports.createRequire(path));
                exports.moduleCache[path] = module.exports;
            }
            return exports.moduleCache[path];
        };
    };

    if (typeof require === 'undefined') {
        // make require available globally, unless already in a commonjs
        // environment
        this.require = exports.createRequire('');
    }

    exports.hashUnescape = function (hash) {
        return hash.replace(/%2F/g, '/');
    };

    exports.init = function () {

        if (!window.console) {
            // console.log is going to cause errors, just stub the functions
            // for now. TODO: add logging utility for IE?
            window.console = {
                log: function () {},
                error: function () {},
                info: function () {},
                warn: function () {}
            };
        }

        // fetch design_doc and handle current URL
        $.getJSON(exports.getBaseURL() + '/_designdoc', function (data) {
            exports.design_doc = data;
            exports.name = exports.design_doc.settings.name;

            // load the rest of the kanso module
            var kanso = require('kanso/core');
            for (var k in kanso) {
                if (kanso.hasOwnProperty(k)) {
                    exports[k] = kanso[k];
                }
            }

            // if using a URL with hash-state, but client supports replaceState,
            // then switch to replaceState instead.
            if (window.location.hash && window.history.replaceState) {
                window.history.replaceState(
                    {}, window.title, exports.getBaseURL() + exports.getURL()
                );
            }

            exports.handle(exports.getURL());

            $('a').live('click', function (ev) {
                var href = $(this).attr('href');

                if (exports.isAppURL(href)) {
                    var url = exports.appPath(href);
                    ev.preventDefault();

                    // changing the hash triggers onhashchange, which then
                    // fires exports.handle for us
                    if (window.onpopstate) {
                        exports.handle(url);
                        exports.setURL(url);
                    }
                    /*else if (window.onhashchange) {
                        window.location.hash = url;
                    }*/
                    else {
                        // TODO: make this an option?
                        var winpath = window.location.pathname;
                        if (winpath !== exports.getBaseURL() + '/') {
                            // redirect to root so hash-based urls look nicer
                            window.location = exports.getBaseURL() + '/#' +
                                exports.hashUnescape(encodeURIComponent(url));
                        }
                        else {
                            $.history.load(url);
                        }
                        //window.location.hash = url;
                        //exports.handle(url);
                    }
                }
            });

            var _handle = function (ev) {
                var url = exports.getURL();
                exports.handle(url);
                // for google analytics
                if (window.pageTracker) {
                    pageTracker._trackPageview(url);
                }
            };
            if ('onpopstate' in window) {
                window.onpopstate = _handle;
            }
            /*else if ('onhashchange' in window) {
                window.onhashchange = _handle;
            }*/
            else {
                $.history.init(_handle, {
                    unescape: exports.hashUnescape
                });
            }
        });
    };

    // run init function if in the browser
    if (typeof module === 'undefined') {
        exports.init();
    }

}((typeof exports === 'undefined') ? this.kanso = {}: module.exports));
<commit_msg>don't track initial page view with google analytics<commit_after>/*global window: false, $: false, pageTracker: false */

/**
 * bootstrapped kanso code,
 * required before the commonjs environment is available.
 */

(function (exports) {

    /**
     * This is because the first page hit also triggers kanso to handle the url
     * client-side. Knowing it is the first page being loaded means we can stop
     * the pageTracker code from submitting the URL twice. Exported because this
     * might be useful information to other modules, it should not be modified
     * by them.
     */
    exports.initial_hit = true;

    exports.getBaseURL = function () {
        var re = new RegExp('(.*\\/_rewrite).*$');
        var match = re.exec(window.location.pathname);
        if (match) {
            return match[1];
        }
        return '';
    };

    exports.moduleCache = {};

    exports.normalizePath = function (p) {
        var path = [];
        var parts = p.split('/');
        for (var i = 0; i < parts.length; i += 1) {
            if (parts[i] === '..') {
                path.pop();
            }
            else if (parts[i] !== '.') {
                path.push(parts[i]);
            }
        }
        return path.join('/');
    };

    exports.getPropertyPath = function (obj, p) {
        // normalize to remove unessecary . and .. from paths
        var parts = exports.normalizePath(p).split('/');

        // if path is empty, return the root object
        if (!p) {
            return obj;
        }

        // loop through all parts of the path, throwing an exception
        // if a property doesn't exist
        var a = obj;
        for (var i = 0; i < parts.length; i += 1) {
            var x = parts[i];
            if (a[x] === undefined) {
                throw new Error('Invalid path: ' + p);
            }
            a = a[x];
        }
        return a;
    };

    exports.dirname = function (p) {
        if (p === '/') {
            return p;
        }
        var parts = p.split('/');
        parts.pop();
        if (parts.length === 1 && parts[0] === '') {
            return '/';
        }
        return parts.join('/');
    };

    exports.createRequire = function (current) {
        return function (target) {
            if (!exports.design_doc) {
                throw new Error('no design doc loaded');
            }
            var path;
            if (target.charAt(0) === '.') {
                var dir = exports.dirname(current);
                path = exports.normalizePath(dir + '/' + target);
            }
            else {
                path = exports.normalizePath(target);
            }
            if (!exports.moduleCache[path]) {
                var module = {exports: {}};
                var fn;
                eval('fn = (function (module, exports, require) {' +
                    exports.getPropertyPath(exports.design_doc, path) +
                '});');
                fn(module, module.exports, exports.createRequire(path));
                exports.moduleCache[path] = module.exports;
            }
            return exports.moduleCache[path];
        };
    };

    if (typeof require === 'undefined') {
        // make require available globally, unless already in a commonjs
        // environment
        this.require = exports.createRequire('');
    }

    exports.hashUnescape = function (hash) {
        return hash.replace(/%2F/g, '/');
    };

    exports.init = function () {

        if (!window.console) {
            // console.log is going to cause errors, just stub the functions
            // for now. TODO: add logging utility for IE?
            window.console = {
                log: function () {},
                error: function () {},
                info: function () {},
                warn: function () {}
            };
        }

        // fetch design_doc and handle current URL
        $.getJSON(exports.getBaseURL() + '/_designdoc', function (data) {
            exports.design_doc = data;
            exports.name = exports.design_doc.settings.name;

            // load the rest of the kanso module
            var kanso = require('kanso/core');
            for (var k in kanso) {
                if (kanso.hasOwnProperty(k)) {
                    exports[k] = kanso[k];
                }
            }

            // if using a URL with hash-state, but client supports replaceState,
            // then switch to replaceState instead.
            if (window.location.hash && window.history.replaceState) {
                window.history.replaceState(
                    {}, window.title, exports.getBaseURL() + exports.getURL()
                );
            }

            exports.handle(exports.getURL());

            $('a').live('click', function (ev) {
                var href = $(this).attr('href');

                if (exports.isAppURL(href)) {
                    var url = exports.appPath(href);
                    ev.preventDefault();

                    // changing the hash triggers onhashchange, which then
                    // fires exports.handle for us
                    if (window.onpopstate) {
                        exports.handle(url);
                        exports.setURL(url);
                    }
                    /*else if (window.onhashchange) {
                        window.location.hash = url;
                    }*/
                    else {
                        // TODO: make this an option?
                        var winpath = window.location.pathname;
                        if (winpath !== exports.getBaseURL() + '/') {
                            // redirect to root so hash-based urls look nicer
                            window.location = exports.getBaseURL() + '/#' +
                                exports.hashUnescape(encodeURIComponent(url));
                        }
                        else {
                            $.history.load(url);
                        }
                        //window.location.hash = url;
                        //exports.handle(url);
                    }
                }
            });

            var _handle = function (ev) {
                var url = exports.getURL();
                exports.handle(url);
                /**
                 * if google analytics is included on the page, and this url
                 * has not been tracked (not the initial hit) then manually
                 * track a page view. This is done consistently for hash-based
                 * and pushState urls
                 */
                if (window.pageTracker && !exports.initial_hit) {
                    pageTracker._trackPageview(url);
                }
                exports.initial_hit = false;
            };
            if ('onpopstate' in window) {
                window.onpopstate = _handle;
            }
            /*else if ('onhashchange' in window) {
                window.onhashchange = _handle;
            }*/
            else {
                $.history.init(_handle, {
                    unescape: exports.hashUnescape
                });
            }
        });
    };

    // run init function if in the browser
    if (typeof module === 'undefined') {
        exports.init();
    }

}((typeof exports === 'undefined') ? this.kanso = {}: module.exports));
<|endoftext|>


<commit_before>const {stripIndents} = require('common-tags');

class TodoItem {
    constructor(description, completed = false) {
        this.description = description;
        this.completed = completed;
    }

    complete(completed = true) {
        this.completed = completed;
    }

    toString() {
        return `${this.completed ? ':ballot_box_with_check:' : ':black_square_button: '} ${this.description}`;
    }

    static fromJSON({description, completed}) {
        return new TodoItem(description, completed);
    }
}

class TodoList {
    constructor(name) {
        this.name = name;
        this.items = [];
    }

    _validateIndex(index) {
        if (Number.isNaN(index)) {
            // This will throw the error below
            index = 0;
        }
        index -= 1;
        if (index < 0 || index > this.items.length) {
            throw 'Please specify a valid number!';
        }

        return index;
    }

    add(description, completed = false) {
        this.items.push(new TodoItem(description, completed));
    }

    remove(index) {
        index = this._validateIndex(index);
        this.items.splice(index, 1);
    }

    markComplete(index, completed = true) {
        index = this._validateIndex(index);
        this.items[index].complete(completed);
    }

    toString() {
        return stripIndents`
        **List: ${this.name}**
        **Items:** ${this.items.length === 0 ? 'None' : ''}
        ${this.items.map((item, i) => `${i + 1}. ${item.toString()}`).join('\n')}
        `;
    }

    static fromJSON({name, items}) {
        const todoList = new TodoList(name);
        todoList.items = items.map(item => TodoItem.fromJSON(item));
        return todoList;
    }
}

class TodoLists {
    constructor(bot) {
        this._bot = bot;
        this.storage = bot.storage('todos');
        this.lists = {};

        const lists = this.storage.get(TodoLists.listsStorageKey);
        if (!lists) {
            this.lists = {
                [TodoLists.mainListName]: new TodoList(TodoLists.mainListName)
            };
        } else {
            Object.keys(lists)
                .forEach(listName => {
                    this.lists[listName] = TodoList.fromJSON(lists[listName]);
                });
        }

        let currentList = this.storage.get(TodoLists.currentListStorageKey);
        if (!this.getList(currentList)) {
            currentList = TodoLists.mainListName;
        }
        this.currentListName = currentList;
    }

    /**
     *
     * @returns {TodoList}
     */
    getCurrentList() {
        return this.getList(this.currentListName);
    }

    save() {
        this.storage.set(TodoLists.currentListStorageKey, this.currentListName);
        this.storage.set(TodoLists.listsStorageKey, this.lists);
        this.storage.save();
    }

    getList(name) {
        return this.lists[name];
    }

    getListNames() {
        return Object.keys(this.lists);
    }

    deleteList(name) {
        if (!this.getList(name)) {
            throw `List '${name}' does not exist!`;
        }
        if (name === TodoLists.mainListName) {
            throw `List '${name}' cannot be deleted!`;
        }
        delete this.lists[name];

        if (this.currentListName === name) {
            this.switchList(TodoLists.mainListName);
        }
    }

    createList(name) {
        if (this.getList(name)) {
            throw `List'${name}' already exists!`;
        }
        this.lists[name] = new TodoList(name);
        this.switchList(name);
    }

    switchList(name) {
        if (!this.getList(name)) {
            throw `List '${name}' does not exist!`;
        }
        this.currentListName = name;
    }
}

TodoLists.listsStorageKey = 'lists';
TodoLists.currentListStorageKey = 'currentListName';
TodoLists.mainListName = 'Main';

module.exports.run = (bot, msg, args) => {
    const send = (message, prefix = ':white_check_mark: ') => msg.channel.send({
        embed: bot.utils.embed('Todo List Utility', `${prefix}${message}`)
    });

    if (args.length < 1) {
        return send(Lists.getCurrentList(), '');
    }

    const {options, leftover} = bot.utils.parseArgs(args, ['d', 'c', 'i', 'l', 'n', 'r', 's']);
    let indexOption = parseInt(leftover[0], 10);

    const ensureProperNumberOfArgs = (max, message = 'Expected list item number only', min = 1) => {
        if (leftover.length > max || leftover.length < min) {
            throw message;
        }
    };

    if (options.c) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().markComplete(indexOption);
        send(`Item ${indexOption} marked complete!`);
    } else if (options.i) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().markComplete(indexOption, false);
        send(`Item ${indexOption} marked incomplete!`);
    } else if (options.d) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().remove(indexOption);
        send(`Item ${indexOption} deleted!`);
    } else if (options.l) {
        send(`Available lists: ${Lists.getListNames().join(', ')}`);
    } else if (options.n) {
        ensureProperNumberOfArgs(1, 'Expected name of new list to be a single word');
        Lists.createList(leftover[0]);
        send(`List '${leftover[0]}' created!`);
    } else if (options.r) {
        ensureProperNumberOfArgs(1, 'Expected name of list to delete to be a single word');
        Lists.deleteList(leftover[0]);
        send(`List '${leftover[0]}' deleted!`);
    } else if (options.s) {
        ensureProperNumberOfArgs(1, 'Expected name of chosen list to be a single word');
        Lists.switchList(leftover[0]);
        send(`Switched to list '${leftover[0]}'`);
    } else {
        Lists.getCurrentList().add(leftover.join(' '));
        send('Item added!');
    }

    Lists.save();
};

/**
 * @type {TodoLists}
 */
let Lists;
module.exports.init = bot => {
    Lists = new TodoLists(bot);
};

module.exports.info = {
    name: 'todo',
    usage: 'todo | todo [options...] <index> | todo <text of item to add>',
    description: 'Manage a todo list. The command by itself will show the items in the current list.',
    options: [
        {
            name: '-c',
            usage: '-c ',
            description: 'Mark an item complete'
        },
        {
            name: '-i',
            usage: '-i ',
            description: 'Mark an item incomplete'
        },
        {
            name: '-d',
            usage: '-d',
            description: 'Delete an item'
        },
        {
            name: '-l',
            usage: '-l',
            description: 'Show all lists'
        },
        {
            name: '-n',
            usage: '-n ',
            description: 'Make a new list and set it as the current list'
        },
        {
            name: '-r',
            usage: '-r ',
            description: 'Remove a list. If the list being removed is the list currently in use, automatically switches back to the Main list.'
        },
        {
            name: '-s',
            usage: '-s ',
            description: 'Switch lists'
        }
    ]
};
<commit_msg>Use edit instead of sending a new messages, and auto delete messages<commit_after>const {stripIndents} = require('common-tags');

class TodoItem {
    constructor(description, completed = false) {
        this.description = description;
        this.completed = completed;
    }

    complete(completed = true) {
        this.completed = completed;
    }

    toString() {
        return `${this.completed ? ':ballot_box_with_check:' : ':black_square_button: '} ${this.description}`;
    }

    static fromJSON({description, completed}) {
        return new TodoItem(description, completed);
    }
}

class TodoList {
    constructor(name) {
        this.name = name;
        this.items = [];
    }

    _validateIndex(index) {
        if (Number.isNaN(index)) {
            // This will throw the error below
            index = 0;
        }
        index -= 1;
        if (index < 0 || index > this.items.length) {
            throw 'Please specify a valid number!';
        }

        return index;
    }

    add(description, completed = false) {
        this.items.push(new TodoItem(description, completed));
    }

    remove(index) {
        index = this._validateIndex(index);
        this.items.splice(index, 1);
    }

    markComplete(index, completed = true) {
        index = this._validateIndex(index);
        this.items[index].complete(completed);
    }

    toString() {
        return stripIndents`
        **List: ${this.name}**
        **Items:** ${this.items.length === 0 ? 'None' : ''}
        ${this.items.map((item, i) => `${i + 1}. ${item.toString()}`).join('\n')}
        `;
    }

    static fromJSON({name, items}) {
        const todoList = new TodoList(name);
        todoList.items = items.map(item => TodoItem.fromJSON(item));
        return todoList;
    }
}

class TodoLists {
    constructor(bot) {
        this._bot = bot;
        this.storage = bot.storage('todos');
        this.lists = {};

        const lists = this.storage.get(TodoLists.listsStorageKey);
        if (!lists) {
            this.lists = {
                [TodoLists.mainListName]: new TodoList(TodoLists.mainListName)
            };
        } else {
            Object.keys(lists)
                .forEach(listName => {
                    this.lists[listName] = TodoList.fromJSON(lists[listName]);
                });
        }

        let currentList = this.storage.get(TodoLists.currentListStorageKey);
        if (!this.getList(currentList)) {
            currentList = TodoLists.mainListName;
        }
        this.currentListName = currentList;
    }

    /**
     *
     * @returns {TodoList}
     */
    getCurrentList() {
        return this.getList(this.currentListName);
    }

    save() {
        this.storage.set(TodoLists.currentListStorageKey, this.currentListName);
        this.storage.set(TodoLists.listsStorageKey, this.lists);
        this.storage.save();
    }

    getList(name) {
        return this.lists[name];
    }

    getListNames() {
        return Object.keys(this.lists);
    }

    deleteList(name) {
        if (!this.getList(name)) {
            throw `List '${name}' does not exist!`;
        }
        if (name === TodoLists.mainListName) {
            throw `List '${name}' cannot be deleted!`;
        }
        delete this.lists[name];

        if (this.currentListName === name) {
            this.switchList(TodoLists.mainListName);
        }
    }

    createList(name) {
        if (this.getList(name)) {
            throw `List'${name}' already exists!`;
        }
        this.lists[name] = new TodoList(name);
        this.switchList(name);
    }

    switchList(name) {
        if (!this.getList(name)) {
            throw `List '${name}' does not exist!`;
        }
        this.currentListName = name;
    }
}

TodoLists.listsStorageKey = 'lists';
TodoLists.currentListStorageKey = 'currentListName';
TodoLists.mainListName = 'Main';

module.exports.run = (bot, msg, args) => {
    const send = (message, deleteInSeconds = 5, prefix = ':white_check_mark: ') => msg.edit({
        embed: bot.utils.embed(`Todo List Utility (message will self-destruct in ${deleteInSeconds} seconds)`, `${prefix}${message}`)
    })
        .then(m => m.delete(deleteInSeconds * 1000));

    if (args.length < 1) {
        return send(Lists.getCurrentList(), 45, '');
    }

    const {options, leftover} = bot.utils.parseArgs(args, ['d', 'c', 'i', 'l', 'n', 'r', 's']);
    let indexOption = parseInt(leftover[0], 10);

    const ensureProperNumberOfArgs = (max, message = 'Expected list item number only', min = 1) => {
        if (leftover.length > max || leftover.length < min) {
            throw message;
        }
    };

    if (options.c) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().markComplete(indexOption);
        send(`Item ${indexOption} marked complete!`);
    } else if (options.i) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().markComplete(indexOption, false);
        send(`Item ${indexOption} marked incomplete!`);
    } else if (options.d) {
        ensureProperNumberOfArgs(1);
        Lists.getCurrentList().remove(indexOption);
        send(`Item ${indexOption} deleted!`);
    } else if (options.l) {
        send(`Available lists: ${Lists.getListNames().join(', ')}`, 30);
    } else if (options.n) {
        ensureProperNumberOfArgs(1, 'Expected name of new list to be a single word');
        Lists.createList(leftover[0]);
        send(`List '${leftover[0]}' created!`);
    } else if (options.r) {
        ensureProperNumberOfArgs(1, 'Expected name of list to delete to be a single word');
        Lists.deleteList(leftover[0]);
        send(`List '${leftover[0]}' deleted!`);
    } else if (options.s) {
        ensureProperNumberOfArgs(1, 'Expected name of chosen list to be a single word');
        Lists.switchList(leftover[0]);
        send(`Switched to list '${leftover[0]}'`);
    } else {
        Lists.getCurrentList().add(leftover.join(' '));
        send(`Item '${leftover.join(' ')}' added!`);
    }

    Lists.save();
};

/**
 * @type {TodoLists}
 */
let Lists;
module.exports.init = bot => {
    Lists = new TodoLists(bot);
};

module.exports.info = {
    name: 'todo',
    usage: 'todo | todo [options...] <index> | todo <text of item to add>',
    description: 'Manage a todo list. The command by itself will show the items in the current list.',
    options: [
        {
            name: '-c',
            usage: '-c ',
            description: 'Mark an item complete'
        },
        {
            name: '-i',
            usage: '-i ',
            description: 'Mark an item incomplete'
        },
        {
            name: '-d',
            usage: '-d',
            description: 'Delete an item'
        },
        {
            name: '-l',
            usage: '-l',
            description: 'Show all lists'
        },
        {
            name: '-n',
            usage: '-n ',
            description: 'Make a new list and set it as the current list'
        },
        {
            name: '-r',
            usage: '-r ',
            description: 'Remove a list. If the list being removed is the list currently in use, automatically switches back to the Main list.'
        },
        {
            name: '-s',
            usage: '-s ',
            description: 'Switch lists'
        }
    ]
};
<|endoftext|>