FEATURE/SAML: RocketChat now loads SP cert and key for SAML.

--- packages/meteor-accounts-saml/saml_rocketchat.coffee
+++ packages/meteor-accounts-saml/saml_rocketchat.coffee
@@ -1,3 +1,5 @@
+fs = Npm.require('fs')
+
 logger = new Logger 'steffo:meteor-accounts-saml',
 	methods:
 		updated:
@@ -5,18 +7,33 @@

 RocketChat.settings.addGroup 'SAML'
 Meteor.methods
+
+  # Define configuration settings for each provider (name) in the
+  # admin SAML form.
+
 	addSamlService: (name) ->
 		RocketChat.settings.add "SAML_Custom_#{name}"                   , false                                                         , { type: 'boolean', group: 'SAML', section: name, i18nLabel: 'Accounts_OAuth_Custom_Enable'}
 		RocketChat.settings.add "SAML_Custom_#{name}_provider"          , 'openidp'                                                     , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Provider'}
 		RocketChat.settings.add "SAML_Custom_#{name}_entry_point"       , 'https://openidp.feide.no/simplesaml/saml2/idp/SSOService.php', { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Entry_point'}
 		RocketChat.settings.add "SAML_Custom_#{name}_issuer"            , 'https://rocket.chat/'                                        , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Issuer'}
 		RocketChat.settings.add "SAML_Custom_#{name}_cert"              , ''                                                            , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Cert'}
+		RocketChat.settings.add "SAML_Custom_#{name}_public_cert_file_path"  , 'filename.crt'                                           , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Public_Cert_File_Path'}
+		RocketChat.settings.add "SAML_Custom_#{name}_private_key_file_path"  , 'filename.key'                                           , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Private_Key_File_Path'}
 		RocketChat.settings.add "SAML_Custom_#{name}_button_label_text" , ''                                                            , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Text'}
 		RocketChat.settings.add "SAML_Custom_#{name}_button_label_color", '#FFFFFF'                                                     , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'Accounts_OAuth_Custom_Button_Label_Color'}
 		RocketChat.settings.add "SAML_Custom_#{name}_button_color"      , '#13679A'                                                     , { type: 'string' , group: 'SAML', section: name, i18nLabel: 'Accounts_OAuth_Custom_Button_Color'}
 		RocketChat.settings.add "SAML_Custom_#{name}_generate_username" , false                                                         , { type: 'boolean', group: 'SAML', section: name, i18nLabel: 'SAML_Custom_Generate_Username'}

 timer = undefined
+
+# Find existing SAML providers (idp's) in in RocketChat settings
+# (instances of RocketChat.models.Settings) and update the system.
+#
+# Updating the system includes:
+# 1) Appending enabled providers to Accounts.saml.settings.providers.
+# 2) Appending enabled providers to ServiceConfiguration.configurations.
+# 3) Removing disabled providers from ServiceConfiguration.configurations.
+
 updateServices = ->
 	Meteor.clearTimeout timer if timer?

@@ -38,13 +55,26 @@
 					clientConfig:
 						provider: RocketChat.settings.get("#{service.key}_provider")

-				Accounts.saml.settings.generateUsername = RocketChat.settings.get("#{service.key}_generate_username")
+				Accounts.saml.settings.generateUsername = RocketChat.settings.get("#{service._id}_generate_username")
+				privateKeyFilePath = RocketChat.settings.get("#{service._id}_private_key_file_path")
+				publicCertFilePath = RocketChat.settings.get("#{service._id}_public_cert_file_path")
+
+				try
+					privateKey = fs.readFileSync(privateKeyFilePath).toString()
+					privateCert = fs.readFileSync(publicCertFilePath).toString()
+				catch err
+					console.warn "Can't configure key or cert for SAML, signing and encryption will not work"
+					console.warn "Error was: #{err.message}"
+					privateKey = false
+					privateCert = false

 				Accounts.saml.settings.providers.push
 					provider: data.clientConfig.provider
 					entryPoint: RocketChat.settings.get("#{service.key}_entry_point")
 					issuer: RocketChat.settings.get("#{service.key}_issuer")
 					cert: RocketChat.settings.get("#{service.key}_cert")
+					privateCert: privateCert
+					privateKey: privateKey

 				ServiceConfiguration.configurations.upsert {service: serviceName.toLowerCase()}, $set: data
 			else
@@ -55,5 +85,6 @@
 	updateServices()

 Meteor.startup ->
+	# Create a Default provider if no providers are found.
 	if RocketChat.settings.get(/^(SAML_Custom)_[a-z]+$/i)?.length is 0
 		Meteor.call 'addSamlService', 'Default'


Make Marker event subscription methods public

Nothing else has the extended/essential distinction currently and this
isn’t the purpose of this documentation pass.

--- src/marker.coffee
+++ src/marker.coffee
@@ -93,7 +93,7 @@
     state.range = Range.deserialize(state.range)
     state

-  # Essential: Invoke the given callback when the marker is destroyed.
+  # Public: Invoke the given callback when the marker is destroyed.
   #
   # * `callback` {Function} to be called when the marker is destroyed.
   #
@@ -101,7 +101,7 @@
   onDidDestroy: (callback) ->
     @emitter.on 'did-destroy', callback

-  # Essential: Invoke the given callback when the state of the marker changes.
+  # Public: Invoke the given callback when the state of the marker changes.
   #
   # * `callback` {Function} to be called when the marker changes.
   #   * `event` {Object} with the following keys:


make the lazy loading of threejs asynchronous again

also @opts.element -> @element

--- src/math3d.coffee
+++ src/math3d.coffee
@@ -96,49 +96,54 @@
 math3d.threejs_src = "http://cdnjs.cloudflare.com/ajax/libs/three.js/r68/three.min.js"
 math3d.orbitcontrols_src = "OrbitControls.js"

-VERSION = '68'
-#$.ajaxSetup cache: true # when using getScript, cache result.
-
-loadScript = (script_src, cb) ->
+remove_element = (element) ->
+    if element is null
+        return
+    if (parent = element.parentElement) is null
+        return
+    parent.removeChild element
+
+loadScript = (script_src, callback) ->
+    run_callback = true
+
     script = document.createElement 'script'
     script.type = 'text/javascript'
     script.async = true
     script.src = script_src

-    heads = document.getElementsByTagName 'head'
-    if heads.length > 0
-        heads[0].appendChild script
-    else
-        document.documentElement.appendChild script
-
-    cb()
-
-load_threejs = (cb) ->
-    if THREE?
-        return cb()
-
-    _loading_threejs_callbacks.push cb
+    script.onload = ->
+            remove_element script
+            if run_callback
+                run_callback = false
+                callback()
+    script.onerror = ->
+            remove_element script
+            if run_callback
+                run_callback = false
+                callback "error loading script #{script.src}"
+
+    document.head.appendChild script
+
+load_threejs = (callback) ->
+    if THREE?.Scene? and THREE?.OrbitControls?
+        return callback()
+
+    _loading_threejs_callbacks.push callback
     #console.log("load_threejs")
     if _loading_threejs_callbacks.length > 1
         #console.log("load_threejs: already loading...")
         return

-    orbitcontrols_checker = ->
-        if THREE.OrbitControls?
-            for cb in _loading_threejs_callbacks
-                cb()
+    run_callbacks = (error) ->
+        for callback in _loading_threejs_callbacks
+            callback error
+        _loading_threejs_callbacks = []
+
+    loadScript math3d.threejs_src, (error) ->
+        if (error)
+            run_callbacks error
         else
-            #console.log("load_threejs: waiting for OrbitControls...")
-            setTimeout orbitcontrols_checker, 100
-
-    threejs_checker = ->
-        if THREE?
-            loadScript math3d.orbitcontrols_src, orbitcontrols_checker
-        else
-            #console.log("load_threejs: waiting for threejs...")
-            setTimeout threejs_checker, 100
-
-    loadScript math3d.threejs_src, threejs_checker
+            loadScript math3d.orbitcontrols_src, run_callbacks

 math3d.load_threejs = load_threejs

@@ -176,13 +181,6 @@

     _renderer

-remove_element = (element) ->
-    if element is null
-        return
-    if (parent = element.parentElement) is null
-        return
-    parent.removeChild element
-
 class Math3dThreeJS
     constructor: (parent_element, opts) ->
         @opts = defaults opts,
@@ -196,27 +194,27 @@
             aspect_ratio    : undefined  # undefined does nothing or a triple [x,y,z] of length three, which scales the x,y,z coordinates of everything by the given values.
             stop_when_gone  : undefined  # if given, animation, etc., stops when this html element (not jquery!) is no longer in the DOM
             frame           : undefined  # if given call set_frame with opts.frame as input when init_done called
-            cb              : undefined  # opts.cb(undefined, this object)
-
-        load_threejs (err) =>
-            if err
-                msg = "Error loading THREE.js -- #{err}"
-                if not opts.cb?
+            callback        : undefined  # opts.callback(error, this object)
+
+        load_threejs (error) =>
+            if error
+                msg = "Error loading THREE.js -- #{error}"
+                if @opts.callback?
+                    @opts.callback msg
+                else
                     console.log msg
-                else
-                    opts.cb? msg
             else
                 @attach_to_dom parent_element
-                @opts.cb? undefined, @
+                @opts.callback? undefined, @

     attach_to_dom: (parent_element) ->
-        if @opts.element?
-            remove_element @opts.element
+        if @element?
+            remove_element @element
         else
-            @opts.element = document.createElement 'span'
-            @opts.element.className = 'math-3d-viewer'
-
-        parent_element.appendChild @opts.element
+            @element = document.createElement 'span'
+            @element.className = 'math-3d-viewer'
+
+        parent_element.appendChild @element

     # client code should call this when start adding objects to the scene
     init: ->
@@ -245,10 +243,10 @@
         @init_light()

         # set background color
-        @opts.element.style.background = @opts.background
+        @element.style.background = @opts.background

         if not @opts.foreground?
-            c = @opts.element.style.background
+            c = @element.style.background
             if not c? or -1 is c.indexOf ')'
                 @opts.foreground = "#000"  # e.g., on firefox - this is best we can do for now
             else
@@ -276,7 +274,7 @@

         # possibly show the canvas warning.
         if dynamic_renderer_type is 'canvas'
-            @opts.element.title = 'WARNING: using slow non-WebGL canvas renderer'
+            @element.title = 'WARNING: using slow non-WebGL canvas renderer'

     set_dynamic_renderer: ->
         # console.log "dynamic renderer"
@@ -288,10 +286,10 @@
         @renderer_type = 'dynamic'

         # remove the current renderer (if it exists)
-        remove_element @opts.element.lastChild
+        remove_element @element.lastChild

         # place renderer in correct place in the DOM
-        @opts.element.appendChild @renderer.domElement
+        @element.appendChild @renderer.domElement

         @renderer.setClearColor @opts.background, 1
         @renderer.setSize @opts.width, @opts.height
@@ -325,21 +323,21 @@
             @last_canvas_target = @controls.target

         # remove the current renderer (if it exists)
-        remove_element @opts.element.lastChild
+        remove_element @element.lastChild

         # place renderer in correct place in the DOM
-        @opts.element.appendChild @static_image
+        @element.appendChild @static_image

     # On mouseover, we switch the renderer out to use webgl, if available, and also enable spin animation.
     init_on_mouseover: ->

-        @opts.element.onmouseenter = =>
+        @element.onmouseenter = =>
             @set_dynamic_renderer()

-        @opts.element.onmouseleave = =>
+        @element.onmouseleave = =>
             @set_static_renderer()

-        @opts.element.onclick = =>
+        @element.onclick = =>
             @set_dynamic_renderer()

     # initialize functions to create new vectors, which take into account the scene's 3d frame aspect ratio.
@@ -810,19 +808,19 @@
         @_animate opts

     _animate: (opts) ->
-        #console.log("anim?", @opts.element.length, @opts.element.is(":visible"))
+        #console.log("anim?", @element.length, @opts.element.is(":visible"))

         if @renderer_type is 'static'
             # will try again when we switch to dynamic renderer
             @_animate_started = false
             return

-        #if not $(@opts.element).is ":visible"
-        if @opts.element.offsetWidth <= 0 and @opts.element.offsetWidth <= 0
+        #if not $(@element).is ":visible"
+        if @element.offsetWidth <= 0 and @element.offsetWidth <= 0
             if @opts.stop_when_gone? and not contains document, @opts.stop_when_gone
                 # console.log("stop_when_gone removed from document -- quit animation completely")
                 @_animate_started = false
-            else if not contains document, @opts.element
+            else if not contains document, @element
                 # console.log("element removed from document; wait 5 seconds")
                 setTimeout (=> @_animate opts), 5000
             else
@@ -941,7 +939,7 @@
                     cb()
             # create the 3d renderer
             opts.scene.opts ?= {}
-            opts.scene.opts.cb = init
+            opts.scene.opts.callback = init

             obj = new Math3dThreeJS opts.element, opts.scene.opts
     ], (err) ->


fix [e51d4fc4e9122463b56b65b18f86db3f40afabbd]

--- review_front/app/components/copy-to-clipboard.coffee
+++ review_front/app/components/copy-to-clipboard.coffee
@@ -1,5 +1,5 @@
-`import Ember from 'ember'`
-`import ZeroClipboard from 'ember-cli-zero-clipboard/components/zero-clipboard'`
+`import Ember from 'ember';`
+`import ZeroClipboard from 'ember-cli-zero-clipboard/components/zero-clipboard'; `

 CopyToClipboard = ZeroClipboard.extend
   tagName: 'button'
@@ -7,9 +7,13 @@
   justCopied: false

   justCopiedToggle: ->
-     @justCopiedToggle()
-     Ember.run.later @, (->
-       @justCopiedToggle()
-     ), 1500
+    @set 'justCopied', !@justCopied

-  `export default CopyToClipboard`
+  actions:
+    afterCopy: ->
+      @justCopiedToggle()
+      Ember.run.later (->
+        @justCopiedToggle()
+      ), 1500
+
+`export default CopyToClipboard;`


package.json docs cleanup

package.json docs cleanup

--- coffee/formats/package.coffee
+++ coffee/formats/package.coffee
@@ -15,7 +15,7 @@
   'default': '''

package.json

-
All npm packages contain a file, usually in the project root, called package.json - this file holds various metadata relevant to the project. This file is used to give information to npm that allows it to identify the project as well as handle the project's dependencies. It can also contain other metadata such as a project description, the version of the project in a particular distribution, license information, even configuration data - all of which can be vital to both npm and to the end users of the package. The package.json file is normally located at the root directory of a Node.js project.

+
All npm packages contain a file, usually in the project root, called package.json - this file holds various metadata relevant to the project. This file is used to give information to npm that allows it to identify the project as well as handle the project’s dependencies. It can also contain other metadata such as a project description, the version of the project in a particular distribution, license information, even configuration data - all of which can be vital to both npm and to the end users of the package. The package.json file is normally located at the root directory of a Node.js project.

{
       "name": "App",
@@ -36,17 +36,17 @@

Some rules:

The name must be shorter than 214 characters. This includes the scope for scoped packages.
-
The name can't start with a dot or an underscore.
+
The name can’t start with a dot or an underscore.

New packages must not have uppercase letters in the name.
-
The name ends up being part of a URL, an argument on the command line, and a folder name. Therefore, the name can't contain any non-URL-safe characters.
+
The name ends up being part of a URL, an argument on the command line, and a folder name. Therefore, the name can’t contain any non-URL-safe characters.

Some tips:

-
Don't use the same name as a core Node module.
-
Don't put "js" or "node" in the name.  It's assumed that it's js, since you're writing a package.json file, and you can specify the engine using the "engines" field.
+
Don’t use the same name as a core Node module.
+
Don’t put “js” or “node” in the name. It’s assumed that it’s js, since you’re writing a package.json file, and you can specify the engine using the “engines” field.

The name will probably be passed as an argument to require(), so it should be something short, but also reasonably descriptive.
-
You may want to check the npm registry to see if there's something by that name already, before you get too attached to it.
+
You may want to check the npm registry to see if there’s something by that name already, before you get too attached to it.

A name can be optionally prefixed by a scope, e.g. @myorg/mypackage.

@@ -55,13 +55,13 @@
   'version': '''

Version

-
The version must be a semver version identifier.  Changes to the package should come along with changes to the version.

+
The version must be a semver version identifier. Changes to the package should come along with changes to the version.

   '''

   'description': '''

Description

-
The description should be a string of human-readable information about the package.  It's listed on npmjs.com and in the output of npm search.

+
The description should be a string of human-readable information about the package. It’s listed on npmjs.com and in the output of npm search.

   '''

   'keywords(\.INDEX)?': '''
@@ -79,15 +79,15 @@
   'bugs(\.PROPERTY)?': '''

Bugs

-
The url to your project's issue tracker and / or the email address to which issues should be reported. These are helpful for people who encounter issues with your package.

+
The url to your project’s issue tracker and / or the email address to which issues should be reported. These are helpful for people who encounter issues with your package.

It should look like this:

-
{ "url" : "https://github.com/owner/project/issues",
-      "email" : "project@hostname.com"
-    }
-

-
-
You can specify either one or both values. If you want to provide only a url, you can specify the value for "bugs" as a simple string instead of an object.

+
{
+      "url": "https://github.com/owner/project/issues",
+      "email": "project@hostname.com"
+    }

+
+
You can specify either one or both values. If you want to provide only a url, you can specify the value for “bugs” as a simple string instead of an object.

If a url is provided, it will be used by the npm bugs command.

   '''
@@ -99,32 +99,33 @@

If you're using a common license such as BSD-2-Clause or MIT, add a current SPDX license identifier for the license you're using, like this:

-
{ "license" : "BSD-3-Clause" }

+
{ "license": "BSD-3-Clause" }

You can check the full list of SPDX license IDs.

If your package is licensed under multiple common licenses, use an SPDX license expression syntax version 2.0 string, like this:

-
{ "license" : "(ISC OR GPL-3.0)" }

+
{ "license": "(ISC OR GPL-3.0)" }

If you are using a license that hasn't been assigned an SPDX identifier, or if you are using a custom license, use the following valid SPDX expression:

-
{ "license" : "SEE LICENSE IN <filename>" }

+
{ "license": "SEE LICENSE IN <filename>" }

Then include a file named <filename> at the top level of the package.

If you do not wish to grant others the right to use a private or unpublished package under any terms:

-
{ "license": "UNLICENSED"}

+
{ "license": "UNLICENSED" }

   '''

   '(author|contributors)': '''

Author / Contributors

-
Who created and maintains this project?.  Provide a single person as the author field or an array of multiple people as the contributors field.

+
Who created and maintains this project? Provide a single person as the author field or an array of multiple people as the contributors field.

Each person may be described as with object:

-
{ "name" : "Barney Rubble",
-      "email" : "b@rubble.com",
-      "url" : "http://barnyrubble.tumblr.com/"
+
{
+      "name": "Barney Rubble",
+      "email": "b@rubble.com",
+      "url": "http://barnyrubble.tumblr.com/"
     }

Or a string:

@@ -139,9 +140,10 @@

Each contributor may be described as with object:

-
{ "name" : "Barney Rubble",
-      "email" : "b@rubble.com",
-      "url" : "http://barnyrubble.tumblr.com/"
+
{
+      "name": "Barney Rubble",
+      "email": "b@rubble.com",
+      "url": "http://barnyrubble.tumblr.com/"
     }

Or a string:

@@ -158,7 +160,7 @@

You may use glob patterns like *.js, and specify folders like src/.

-
You can also provide a ".npmignore" file in the root of your package, which will keep files from being included, even if they would be picked up by the files array.

+
You can also provide a .npmignore file in the root of your package, which will keep files from being included, even if they would be picked up by the files array.

   '''

   'files\.INDEX': '''
@@ -182,11 +184,11 @@

bin allows you to install one or more executable files onto the PATH of this machine so they can be executed from the terminal.

-
For example, if you'd like your cli.js file to be executed when the user runs myapp on the command line, you could specify:

-
-
{ "bin" : { "myapp" : "./cli.js" } }

-
-
If you only have a single executable, and you'd like the command line program to have the same name as your package, you can just specify the file to be executed as a string:

+
For example, if you’d like your cli.js file to be executed when the user runs myapp on the command line, you could specify:

+
+
{ "bin": { "myapp": "./cli.js" } }

+
+
If you only have a single executable, and you’d like the command line program to have the same name as your package, you can just specify the file to be executed as a string:

"bin": "./path/to/program"

   '''
@@ -194,7 +196,7 @@
   'man': '''

Man

-
The man program is used by many UNIX-y operating systems to help users access documentation.  If you have one or more man pages for your project, specify them with the man option.

+
The man program is used by many UNIX-y operating systems to help users access documentation. If you have one or more man pages for your project, specify them with the man option.

The option can either be specified as a single filepath, or as an array of files.

   '''
@@ -205,15 +207,15 @@

Specify the place where your code lives.

For example:

-
{
-        "type" : "git",
-        "url" : "https://github.com/npm/npm.git"
-      }
-

+
+
{
+      "type": "git",
+      "url": "https://github.com/npm/npm.git"
+    }

The URL should be a publicly available (perhaps read-only) url that can be handed
-    directly to a VCS program without any modification.  It should not be a url to an
-    html project page that you put in your browser.  It's for computers.

+    directly to a VCS program without any modification. It should not be a url to an
+    html project page that you put in your browser. It’s for computers.

For GitHub, GitHub gist, Bitbucket, or GitLab repositories you can use the same
     shortcut syntax you use for npm install:

@@ -259,9 +261,9 @@
   'config(\.PROPERTY)?': '''

Config

-
npm supports configuration variables which can be set by the user using npm config.  The config option can be used to set defaults for these options.

-
-
Config variables are available as environment variables.  For example, if you had the config:

+
npm supports configuration variables which can be set by the user using npm config. The config option can be used to set defaults for these options.

+
+
Config variables are available as environment variables. For example, if you had the config:

"config": {
       "port": 8080
@@ -269,8 +271,7 @@

It would be available as the npm_package_config_port environment variable in your scripts, and could be overrode by the user with:

-
npm config set foo:port 8001
-

+
npm config set foo:port 8001

   '''

   'dependencies': '''
@@ -284,21 +285,21 @@

Dev dependencies work exactly as dependencies, but they are not installed when a user installs your package without downloading the source (unless they provide the --dev option).

-
It often makes sense to include build and test tools as dev dependencies, so end users don't have to install them if their not required to use the package.

+
It often makes sense to include build and test tools as dev dependencies, so end users don’t have to install them if their not required to use the package.

   ''' + depInfo

   'peerDependencies': '''

Peer Dependencies

-
If you're building a plugin which interacts with another package, it's often valuable to express what versions of that package you support.

-
-
Peer dependencies allow you to express this compatibility.  Be as broad as you can, as npm will error if the user tries to use your plugin with an incompatible version.

+
If you’re building a plugin which interacts with another package, it’s often valuable to express what versions of that package you support.

+
+
Peer dependencies allow you to express this compatibility. Be as broad as you can, as npm will error if the user tries to use your plugin with an incompatible version.

   ''' + depInfo

   'optionalDependencies': '''

Optional Dependencies

-
If a package in dependencies fails to install, the installation of your package will be aborted.  If you do not wish this to happen for some dependencies (i.e. your package can run without them), include them as optional dependencies.

+
If a package in dependencies fails to install, the installation of your package will be aborted. If you do not wish this to happen for some dependencies (i.e. your package can run without them), include them as optional dependencies.

   ''' + depInfo

   'engines(\.PROPERTY)?': '''
@@ -308,30 +309,29 @@

For example:

-
{ "engines" : { "node" : ">=0.10.3 <0.12" } }

+
{ "engines": { "node": ">=0.10.3 <0.12" } }

   '''

   '(dependencies|optionalDependencies|peerDependencies|devDependencies)\.PROPERTY': '''

Dependency

-
A dependency specifies a package which this project utilizes.  It is usually a package which is published to the npm registry, but it can also be anything hosted on the internet or even a local folder.

+
A dependency specifies a package which this project utilizes. It is usually a package which is published to the npm registry, but it can also be anything hosted on the internet or even a local folder.

When using the npm registry, specify a package name and a version string:

"vex": "1.4.3"

-
The version string should use semver.  You can use < and > to specify ranges, for example:
+
The version string should use semver. You can use < and > to specify ranges, for example:

"vex": ">1.2.3 <2.0.0"

     The ~ and ^ shorthands are also available:

-

-    ~1.2.3    : is >=1.2.3 <1.3.0
-
-    ^1.2.3    : is >=1.2.3 <2.0.0
-    ^0.2.3    : is >=0.2.3 <0.3.0   (0.x.x is special)
-    ^0.0.1    : is  =0.0.1          (0.0.x is special)
+
~1.2.3   : is >=1.2.3 <1.3.0
+
+    ^1.2.3   : is >=1.2.3 <2.0.0
+    ^0.2.3   : is >=0.2.3 <0.3.0  (0.x.x is special)
+    ^0.0.1   : is  =0.0.1         (0.0.x is special)

In the version field you can also specify:

@@ -342,13 +342,13 @@

A GitHub repo (visionmedia/mocha#4727d357ea)

-
As in those examples, it's usually a good idea to specify a specific commit or tag when using git URLs (by appending the identifier after the "#" symbol).

+
As in those examples, it’s usually a good idea to specify a specific commit or tag when using git URLs (by appending the identifier after the “#” symbol).

   '''

   'bundledDependencies(\.INDEX)?': '''

Bundled Dependencies

-
Occasionally it's helpful to include a local copy of a dependency (it's folder in node_modules/) with the project when it's published.

+
Occasionally it’s helpful to include a local copy of a dependency (it’s folder in node_modules/) with the project when it’s published.

Bundled dependencies are specified as an array of package names, like:

@@ -362,11 +362,11 @@

os allows you to specify which operating systems your package will run on.

-
You can specify a list of OS' it works on:

+
You can specify a list of OS’ it works on:

["darwin", "linux"]

-
Or a list it doesn't work on:

+
Or a list it doesn’t work on:

["!win32"]

@@ -382,7 +382,7 @@

["x64", "ia32"]

-
Or a list it doesn't work on:

+
Or a list it doesn’t work on:

["!mips"]

@@ -406,7 +406,7 @@
   'publishConfig': '''

Publish Config

-
This is a set of config values that will be used at publish-time. It's especially handy if you want to set the tag or registry, so that you can ensure that a given package is not tagged with "latest" or published to the global public registry by default.

+
This is a set of config values that will be used at publish-time. It’s especially handy if you want to set the tag or registry, so that you can ensure that a given package is not tagged with “latest” or published to the global public registry by default.

See npm help config to see the list of config options that can be overridden.

   '''


Safari compatibility fix

Safari compatibility fix

--- components/BlobToEntry.coffee
+++ components/BlobToEntry.coffee
@@ -94,7 +94,7 @@
     out: ['graph', 'component', 'spec']
   , (data, groups, out) ->
     sha = data.sha
-    content = data.content
+    content = data.content.replace /\s/g, ''
     content = decodeURIComponent escape atob content if data.encoding is 'base64'

     unless c.params.operation.pull?.length


Server: dont check allowed domain for the users created via api

--- servers/lib/server/handlers/api/createuser.coffee
+++ servers/lib/server/handlers/api/createuser.coffee
@@ -61,7 +61,12 @@
         groupName : apiToken.group
       }

-      JUser.convert client, userData, (err, data) ->
+      # here we don't check if email is in allowed domains
+      # because the user who has the api token must be a group admin
+      # they should be able to use any email they want for their own team  ~ OK
+      options  = { ignoreAllowedDomainCheck : yes }
+
+      JUser.convert client, userData, options, (err, data) ->

         if err?.message is 'Email is already in use!'
           return sendApiError res, apiErrors.emailAlreadyExists


add specs

add specs

--- spec/pane-element-spec.coffee
+++ spec/pane-element-spec.coffee
@@ -113,6 +113,49 @@
         expect(paneElement.dataset.activeItemPath).toBeUndefined()
         expect(paneElement.dataset.activeItemName).toBeUndefined()

+      describe "when the path of the item changes", ->
+        [item1, item2] = []
+
+        beforeEach ->
+          item1 = document.createElement('div')
+          item1.path = '/foo/bar.txt'
+          item1.changePathCallbacks = []
+          item1.setPath = (path) ->
+            this.path = path
+            callback() for callback in changePathCallbacks
+          item1.getPath = -> this.path
+          item1.onDidChangePath = (callback) -> this.changePathCallbacks.push(callback)
+
+          item2 = document.createElement('div')
+
+          pane.addItem(item1)
+          pane.addItem(item2)
+
+        it "changes the file path and file name data attributes on the pane if the active item path is changed", ->
+
+          expect(paneElement.dataset.activeItemPath).toBe '/foo/bar.txt'
+          expect(paneElement.dataset.activeItemName).toBe 'bar.txt'
+
+          item1.setPath "/foo/bar1.txt"
+
+          expect(paneElement.dataset.activeItemPath).toBe '/foo/bar1.txt'
+          expect(paneElement.dataset.activeItemName).toBe 'bar1.txt'
+
+          pane.activateItem(item2)
+
+          expect(paneElement.dataset.activeItemPath).toBeUndefined()
+          expect(paneElement.dataset.activeItemName).toBeUndefined()
+
+          item1.setPath "/foo/bar2.txt"
+
+          expect(paneElement.dataset.activeItemPath).toBeUndefined()
+          expect(paneElement.dataset.activeItemName).toBeUndefined()
+
+          pane.activateItem(item1)
+
+          expect(paneElement.dataset.activeItemPath).toBe '/foo/bar2.txt'
+          expect(paneElement.dataset.activeItemName).toBe 'bar2.txt'
+
   describe "when an item is removed from the pane", ->
     describe "when the destroyed item is an element", ->
       it "removes the item from the itemViews div", ->


Don't use atom.workspaceView

Don't use atom.workspaceView

--- spec/symbols-view-spec.coffee
+++ spec/symbols-view-spec.coffee
@@ -1,5 +1,5 @@
 path = require 'path'
-{WorkspaceView} = require 'atom'
+{$} = require 'atom'
 fs = require 'fs-plus'
 temp = require 'temp'
 SymbolsView = require '../lib/symbols-view'
@@ -9,13 +9,11 @@
   [symbolsView, activationPromise, editor] = []

   beforeEach ->
-    atom.workspaceView = new WorkspaceView
-    atom.workspace = atom.workspaceView.model
     atom.project.setPath(temp.mkdirSync('atom-symbols-view-'))
     fs.copySync(path.join(__dirname, 'fixtures'), atom.project.getPath())

     activationPromise = atom.packages.activatePackage("symbols-view")
-    atom.workspaceView.attachToDom()
+    jasmine.attachToDOM(atom.views.getView(atom.workspace))

     waitsForPromise ->
       atom.packages.activatePackage('language-ruby')
@@ -26,13 +24,13 @@
         atom.workspace.open('sample.js')

       runs ->
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace.getActiveTextEditor()), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -40,7 +38,7 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect(atom.workspaceView.find('.symbols-view')).toExist()
+        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'quicksort'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'Line 1'
@@ -54,13 +52,13 @@

       runs ->
         editor = atom.workspace.getActiveEditor()
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -68,7 +66,7 @@
       runs ->
         symbolsView.cancel()
         spyOn(symbolsView, 'generateTags').andCallThrough()
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -79,7 +77,7 @@
         expect(symbolsView.generateTags).not.toHaveBeenCalled()
         editor.getBuffer().emit 'saved'
         symbolsView.cancel()
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -96,13 +94,13 @@
         atom.workspace.open('sample.js')

       runs ->
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()

       waitsFor ->
         symbolsView.list.children('li').length > 0
@@ -111,7 +109,7 @@
         symbolsView.filterEditorView.setText("nothing will match this")
         window.advanceClock(symbolsView.inputThrottle)

-        expect(atom.workspaceView.find('.symbols-view')).toExist()
+        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 0
         expect(symbolsView.error).toBeVisible()
         expect(symbolsView.error.text().length).toBeGreaterThan 0
@@ -129,13 +127,13 @@
         atom.workspace.open('sample.txt')

       runs ->
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()

       waitsFor ->
         symbolsView.error.isVisible()
@@ -153,14 +151,14 @@

     runs ->
       expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [0,0]
-      expect(atom.workspaceView.find('.symbols-view')).not.toExist()
-      atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
+      expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).not.toExist()
+      atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")

     waitsFor ->
-      atom.workspaceView.find('.symbols-view').find('li').length
+      $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li').length

     runs ->
-      atom.workspaceView.find('.symbols-view').find('li:eq(1)').mousedown().mouseup()
+      $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li:eq(1)').mousedown().mouseup()
       expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [1,2]

   describe "TagGenerator", ->
@@ -196,7 +194,7 @@
       runs ->
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([0,2])
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

       waitsForPromise ->
         activationPromise
@@ -212,7 +210,7 @@
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([6,24])
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -227,13 +225,13 @@
       runs ->
         editor = atom.workspace.getActiveEditor()
         editor.setCursorBufferPosition([8,14])
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
-
-      waitsFor ->
-        atom.workspaceView.find('.symbols-view').find('li').length > 0
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')
+
+      waitsFor ->
+        $(atom.views.getView(atom.workspace)).find('.symbols-view').find('li').length > 0
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
         expect(symbolsView.list.children('li').length).toBe 2
         expect(symbolsView).toBeVisible()
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
@@ -255,7 +253,7 @@
       runs ->
         spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
         atom.workspace.getActiveEditor().setCursorBufferPosition([13,4])
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -264,7 +262,7 @@
         expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [5,2]
         SymbolsView::moveToPosition.reset()
         atom.workspace.getActiveEditor().setCursorBufferPosition([14,2])
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -273,7 +271,7 @@
         expect(atom.workspace.getActiveEditor().getCursorBufferPosition()).toEqual [9,2]
         SymbolsView::moveToPosition.reset()
         atom.workspace.getActiveEditor().setCursorBufferPosition([15,5])
-        atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+        atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

       waitsFor ->
         SymbolsView::moveToPosition.callCount is 1
@@ -289,7 +287,7 @@
         runs ->
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([6,0])
-          atom.workspaceView.getActiveView().trigger 'symbols-view:return-from-declaration'
+          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:return-from-declaration')
           expect(editor.getCursorBufferPosition()).toEqual [6,0]

       it "returns to previous row and column", ->
@@ -300,14 +298,14 @@
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([6,24])
           spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-          atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 1

         runs ->
           expect(editor.getCursorBufferPosition()).toEqual [2,0]
-          atom.workspaceView.getActiveView().trigger 'symbols-view:return-from-declaration'
+          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:return-from-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 2
@@ -326,7 +324,7 @@
           editor = atom.workspace.getActiveEditor()
           editor.setCursorBufferPosition([8,14])
           spyOn(SymbolsView.prototype, "moveToPosition").andCallThrough()
-          atom.workspaceView.getActiveView().trigger 'symbols-view:go-to-declaration'
+          atom.commands.dispatch(atom.views.getView(atom.workspace), 'symbols-view:go-to-declaration')

         waitsFor ->
           SymbolsView::moveToPosition.callCount is 1
@@ -342,14 +340,14 @@
         atom.workspace.open("tagged.js")

       runs ->
-        expect(atom.workspaceView.find('.symbols-view')).not.toExist()
-        atom.workspaceView.trigger "symbols-view:toggle-project-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).not.toExist()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      runs ->
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -357,14 +355,14 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect(atom.workspaceView.find('.symbols-view')).toExist()
+        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li').length).toBe 4
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'callMeMaybe'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'tagged.js'
         expect(symbolsView.list.children('li:last').find('.primary-line')).toHaveText 'thisIsCrazy'
         expect(symbolsView.list.children('li:last').find('.secondary-line')).toHaveText 'tagged.js'
         expect(symbolsView.error).not.toBeVisible()
-        atom.workspaceView.trigger "symbols-view:toggle-project-symbols"
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")

         fs.removeSync(atom.project.resolve('tags'))

@@ -372,7 +370,7 @@
         symbolsView.reloadTags

       runs ->
-        atom.workspaceView.trigger "symbols-view:toggle-project-symbols"
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")

       waitsFor ->
         symbolsView.error.isVisible()
@@ -386,21 +384,21 @@
           fs.removeSync(atom.project.resolve("tagged.js"))

         it "doesn't open the editor", ->
-          atom.workspaceView.trigger "symbols-view:toggle-project-symbols"
+          atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-project-symbols")

           waitsForPromise ->
             activationPromise

           runs ->
-            symbolsView = atom.workspaceView.find('.symbols-view').view()
+            symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()

           waitsFor ->
             symbolsView.list.children('li').length > 0

           runs ->
-            spyOn(atom.workspaceView, 'open').andCallThrough()
+            spyOn(atom.workspace, 'open').andCallThrough()
             symbolsView.list.children('li:first').mousedown().mouseup()
-            expect(atom.workspaceView.open).not.toHaveBeenCalled()
+            expect(atom.workspace.open).not.toHaveBeenCalled()
             expect(symbolsView.error.text().length).toBeGreaterThan 0

   describe "when useEditorGrammarAsCtagsLanguage is set to true", ->
@@ -416,19 +414,19 @@
       runs ->
         atom.workspace.getActiveEditor().setText("var test = function() {}")
         atom.workspace.getActiveEditor().save()
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-
-      waitsForPromise ->
-        activationPromise
-
-      waitsFor ->
-        atom.workspaceView.find('.symbols-view').view().error.isVisible()
-
-      runs ->
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+
+      waitsForPromise ->
+        activationPromise
+
+      waitsFor ->
+        $(atom.views.getView(atom.workspace)).find('.symbols-view').view().error.isVisible()
+
+      runs ->
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
         atom.workspace.getActiveEditor().setGrammar(atom.syntax.grammarForScopeName('source.js'))
-        atom.workspaceView.getActiveView().trigger "symbols-view:toggle-file-symbols"
-        symbolsView = atom.workspaceView.find('.symbols-view').view()
+        atom.commands.dispatch(atom.views.getView(atom.workspace), "symbols-view:toggle-file-symbols")
+        symbolsView = $(atom.views.getView(atom.workspace)).find('.symbols-view').view()
         expect(symbolsView.loading).toBeVisible()

       waitsFor ->
@@ -436,6 +434,6 @@

       runs ->
         expect(symbolsView.loading).toBeEmpty()
-        expect(atom.workspaceView.find('.symbols-view')).toExist()
+        expect($(atom.views.getView(atom.workspace)).find('.symbols-view')).toExist()
         expect(symbolsView.list.children('li:first').find('.primary-line')).toHaveText 'test'
         expect(symbolsView.list.children('li:first').find('.secondary-line')).toHaveText 'Line 1'


add test for hash change

add test for hash change

--- test/components/scroll-to.spec.coffee
+++ test/components/scroll-to.spec.coffee
@@ -18,26 +18,46 @@
 describe 'ScrollTo Mixin', ->

   beforeEach ->
+    # TODO clean up location and window stubs to include in `helpers/component-testing`
+    windowListeners = {}
+
+    _location =
+      hash: ''
+    locationStub = {}
+    locationGetSet =
+      get: ->
+        _location.hash
+      set: (value) ->
+        _location.hash = value
+        windowListeners.hashchange?()
+    Object.defineProperty locationStub, 'hash', locationGetSet
+
     @props =
       windowImpl:
         scroll: sinon.spy()
-        location: { hash: '' }
+        location: locationStub
         pageYOffset: 0
-        addEventListener: sinon.spy()
-        removeEventListener: sinon.spy()
         requestAnimationFrame: sinon.spy()
-
-  it 'attaches event listeners when mounted', ->
-    Testing.renderComponent( TestComponent, props: @props ).then ({element}) =>
-      expect(@props.windowImpl.addEventListener).to.have.been.calledWith(
-        'hashchange', element._onHashChange, false
-      )
+        addEventListener: (name, callback) ->
+          windowListeners[name] = callback
+        removeEventListener: (name, callback) ->
+          delete windowListeners[name] if windowListeners[name] is callback

   it 'scrolls to target on page load if it exists', ->
     @props.windowImpl.location.hash = '#bar'
     Testing.renderComponent( TestComponent, props: @props ).then =>
       expect(@props.windowImpl.scroll).to.have.been.called

+  it 'scrolls to target on hash change', ->
+    @props.windowImpl.location.hash = '#bar'
+    Testing.renderComponent( TestComponent, props: @props ).then ({element, dom}) =>
+      element.scrollToElement = sinon.spy()
+      testHash = '#foo'
+      fooDOM = dom.querySelector(testHash)
+
+      @props.windowImpl.location.hash = testHash
+      expect(element.scrollToElement).to.have.been.calledWith(fooDOM)
+
   it 'scrolls to an element', ->
     Testing.renderComponent( TestComponent, props: @props ).then ({dom, element}) =>
       element.scrollToElement(dom.querySelector('#foo'))


update featured judges

update featured judges

--- config/featured_judges.coffee
+++ config/featured_judges.coffee
@@ -5,36 +5,36 @@
     "name": "Dion Almaer",
     "label": "Ajaxian",
     "twitter": "http://twitter.com/dalmaer",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/dalmaer",
     "avatar": "https://si0.twimg.com/profile_images/3380865881/f73b3687ff39b795db05fcaf35972270.jpeg"
   },
   {
     "name": "Isaac Schlueter",
     "label": "Node Maintainer",
     "twitter": "http://twitter.com/izs",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/pub/isaac-schlueter/38/1a8/35b",
     "avatar": "https://si0.twimg.com/profile_images/378800000078943747/76b6a0545d5118abddb965af3b9fe898.jpeg"
   },
   {
     "name": "John Resig",
     "label": "jQuery / Khan Academy",
     "twitter": "http://twitter.com/jeresig",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/jeresig",
     "avatar": "https://si0.twimg.com/profile_images/3710418056/50890b833c898cab45237a461e873807.jpeg"
   },
   {
     "name": "Guillermo Rausch",
     "label": "socket.io",
     "twitter": "http://twitter.com/rauchg",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/rauchg",
     "avatar": "https://si0.twimg.com/profile_images/3631916180/63b85c1405b17281e40548a919f5f7f7.jpeg"
   },
   {
     "name": "Daniel Shaw",
     "label": "Node Firm + \"\"The Voice\"\"",
     "twitter": "http://twitter.com/dshaw",
-    "linkedin": "",
-    "avatar": "https://si0.twimg.com/profile_images/1552591406/angry_unicorn.png"
+    "linkedin": "http://www.linkedin.com/in/dshaw",
+    "avatar": "http://m.c.lnkd.licdn.com/mpr/mpr/shrink_200_200/p/3/000/076/3fe/0b3a921.jpg"
   },
   {
     "name": "Divya Manian",
@@ -52,58 +52,58 @@
   },
   {
     "name": "Misko Hevery",
-    "label": "AngularJS founder",
+    "label": "Father of AngularJS",
     "twitter": "http://twitter.com/mhevery",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/pub/misko-hevery/0/3b1/388",
     "avatar": "https://si0.twimg.com/profile_images/2614358219/0ixx2n6n059uf5gdwaxl.jpeg"
   },
   {
     "name": "Adam Abrons",
-    "label": "Square, AngularJS founder",
+    "label": "Square, AngularJS",
     "twitter": "http://twitter.com/abrons",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/adamabrons",
     "avatar": "https://si0.twimg.com/profile_images/1414474674/abrons.jpg"
   },
   {
     "name": "Andrew Chen",
     "label": "growth hacker",
     "twitter": "http://twitter.com/andrewchen",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/andrewchen",
     "avatar": "https://si0.twimg.com/profile_images/2199553637/IMG_1584.JPG"
   },
   {
     "name": "Geoffrey Grosenbach",
     "label": "PeepCode",
     "twitter": "http://twitter.com/topfunky",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/topfunky",
     "avatar": "https://si0.twimg.com/profile_images/1416014432/mask_avatar_fin_v2-300x300.jpg"
   },
   {
-    "name": "Sacha Grief",
+    "name": "Sacha Greif",
     "label": "Designer",
-    "twitter": "http://twitter.com/SachaGrief",
+    "twitter": "http://twitter.com/SachaGreif",
     "linkedin": "",
-    "avatar": "https://si0.twimg.com/profile_images/2487116271/j8ehsrukq7v6bh6tswfc.png"
+    "avatar": "http://dun4nx4d6jyre.cloudfront.net/flat-pixels/images/sacha.jpg"
   },
   {
     "name": "Bill Scott",
     "label": "Paypal",
     "twitter": "http://twitter.com/billwscott",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/billwscott",
     "avatar": "https://si0.twimg.com/profile_images/378800000448354163/7e0dbc9115f75100d0e01e5f347f71a2.jpeg"
   },
   {
     "name": "Chris Matthieu",
     "label": "node dev",
     "twitter": "http://twitter.com/chrismatthieu",
-    "linkedin": "",
-    "avatar": "https://si0.twimg.com/profile_images/918916153/SuperChrisSmall.jpg"
+    "linkedin": "http://www.linkedin.com/in/chrismatthieu",
+    "avatar": "http://m.c.lnkd.licdn.com/mpr/mpr/shrink_200_200/p/1/000/2cb/322/05ca28a.jpg"
   },
   {
     "name": "Jason Hoffman",
     "label": "Joyent founder",
     "twitter": "http://twitter.com/jasonh",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/jasonhoffman",
     "avatar": "https://si0.twimg.com/profile_images/3021341335/e19f61309193fbc176421481be47e1f7.png"
   },
   {
@@ -124,8 +124,7 @@
     "name": "Mikeal Rogers",
     "label": "node core",
     "twitter": "http://twitter.com/mikeal",
-    "linkedin": "",
+    "linkedin": "http://www.linkedin.com/in/mikealrogers",
     "avatar": "https://si0.twimg.com/profile_images/3696914564/537a181733d33d4125eab616e896afe8.png"
   }
-]
-
+]


initialze session storage

initialze session storage

--- cf/spell.coffee
+++ cf/spell.coffee
@@ -16,6 +16,7 @@
   )

   startUp = (data) ->
+    initializeSessionStorage()
     $("#question, #answer").show()
     @current = ""
     @fail = 0


profile editing: html decode input values

--- client/Account/views/username.coffee
+++ client/Account/views/username.coffee
@@ -158,10 +158,10 @@
     {email} = @userInfo
     {nickname, firstName, lastName} = @account.profile

-    @emailForm.inputs.email.setDefaultValue email
-    @emailForm.inputs.username.setDefaultValue nickname
-    @emailForm.inputs.firstName.setDefaultValue firstName
-    @emailForm.inputs.lastName.setDefaultValue lastName
+    @emailForm.inputs.email.setDefaultValue Encoder.htmlDecode email
+    @emailForm.inputs.username.setDefaultValue Encoder.htmlDecode nickname
+    @emailForm.inputs.firstName.setDefaultValue Encoder.htmlDecode firstName
+    @emailForm.inputs.lastName.setDefaultValue Encoder.htmlDecode lastName

     {focus} = KD.utils.parseQuery()
     @emailForm.inputs[focus]?.setFocus()  if focus


Klient: try to connect on init

Klient: try to connect on init

--- client/Main/kite/kites/klient.coffee
+++ client/Main/kite/kites/klient.coffee
@@ -23,4 +23,6 @@
     webtermKillSession : 'webterm.killSession'
     webtermPing        : 'webterm.ping'

-  init: -> Promise.resolve()
+  init: ->
+    @connect()
+    Promise.resolve()


Fix folding of original message when editing draft, refs #79

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -548,9 +548,10 @@
                     header.on 'click', ->
                         jQuery('.rt-editor').toggleClass('folded')
                 catch e
-                    console.log e
-            jQuery('.rt-editor .originalToggle').on 'click', ->
-                jQuery('.rt-editor').toggleClass('folded')
+                    console.error e
+            else
+                jQuery('.rt-editor .originalToggle').on 'click', ->
+                    jQuery('.rt-editor').toggleClass('folded')

         else
             # Text message


Revert "Client: User join/leave messages gone."

This reverts commit 507dcfebe70d3c0f6e514bb674403bb3d46f4e25.

--- client/app/js/views/chat_view.coffee
+++ client/app/js/views/chat_view.coffee
@@ -16,6 +16,8 @@
     @chatEvent ||= "sendChat"
     @chatArgs ||= []
     @listenTo(@collection, 'add remove reset', @renderUserList)
+    @listenTo(@collection, 'add', @userJoin)
+    @listenTo(@collection, 'remove', @userLeave)
     @chatHistory = []
     @mostRecentNames = []
     @tabCompleteIndex = -1
@@ -132,6 +134,12 @@
     @mostRecentNames.push(username)  if username not in @mostRecentNames
     @mostRecentNames.shift()  if @mostRecentNames.length > MAX_USERNAME_HISTORY

+  userJoin: (user) =>
+    @updateChat("#{user.id} joined!")
+
+  userLeave: (user) =>
+    @updateChat("#{user.id} left!")
+
   updateChat: (message) =>
     wasAtBottom = @isAtBottom()
     @print("<p>#{message}</p>")


Check optimizely experiment bucket

Check optimizely experiment bucket

--- client/code/view/dataset/list.coffee
+++ client/code/view/dataset/list.coffee
@@ -26,10 +26,13 @@
         return
       if not app.datasets().length
         setTimeout ->
-          if window.user.effective.datasetDisplay == 'list'
-            guiderNewDataset('.new-dataset')
-          else
-            guiderNewDataset('.new-dataset-tile')
+          # This is the optimizely id for the "Create dataset guider" experiment.
+          # We only show the guider when the user is in the Variant #1 bucket for it.
+          if window['optimizely'].data.state.variationMap[1346607875] == 1
+            if window.user.effective.datasetDisplay == 'list'
+              guiderNewDataset('.new-dataset')
+            else
+              guiderNewDataset('.new-dataset-tile')
         , 1
     app.on 'route', () ->
       guiders.hideAll()


Better decide which notifications to show

--- client/components/CommentList.coffee
+++ client/components/CommentList.coffee
@@ -49,7 +49,7 @@

     componentDidUpdate: (prevProps) ->
         # do not show anything if we do not have comments or we were not logged in
-        if (not @props.data) || (not prevProps.myUser?._id)
+        if (not @props.data?.length) || (not prevProps.data?.length)
             return
         viewedComments = {}
         for comment in prevProps.data || []


dashboard/billing: handle group credit card load error

--- client/home/lib/billing/index.coffee
+++ client/home/lib/billing/index.coffee
@@ -36,7 +36,7 @@
       { actions } = PaymentFlux(reactor)

       actions.loadStripeClient().then =>
-        actions.loadGroupCreditCard()
+        actions.loadGroupCreditCard().catch kd.warn

         @wrapper.addSubView header 'Koding Subscription'
         @wrapper.addSubView section 'Koding Subscription'


cleaned up, and set variables to sensible defaults

--- js-library/app/js/views/fact.coffee
+++ js-library/app/js/views/fact.coffee
@@ -1,6 +1,8 @@
 highlight_time_on_load    = 1500
 highlight_time_on_in_view = 1500
-delay_between_highlight_and_show_button_open = 400
+delay_between_highlight_and_show_button_open = 350
+delay_before_mouseover_detected = 50
+delay_before_mouseout_detected = 300

 class Highlighter
   constructor: (@$elements) ->
@@ -13,14 +15,14 @@
     @button_attention = new Factlink.AttentionSpan
       lost_attention:   => @show_button.hide()
       gained_attention: => @show_button.show()
-      wait_for_attention: 700
-      wait_for_neglection: 300
+      wait_for_attention:  delay_before_mouseover_detected + delay_between_highlight_and_show_button_open
+      wait_for_neglection: delay_before_mouseout_detected

     @highlight_attention = new Factlink.AttentionSpan
       lost_attention:   => @highlighter.dehighlight()
       gained_attention: => @highlighter.highlight()
-      wait_for_attention: 300
-      wait_for_neglection: 300
+      wait_for_attention:  delay_before_mouseover_detected
+      wait_for_neglection: delay_before_mouseout_detected

     @highlighter = new Highlighter $(@elements)

@@ -32,39 +34,30 @@
     @highlight_temporary highlight_time_on_load

     $(@elements)
-      .on('mouseenter', (e)=> @onFocus(e))
+      .on('mouseenter', (e) =>
+        @onFocus()
+        @show_button.setCoordinates($(e.target).offset().top, e.pageX)
       .on('mouseleave', => @onBlur())
       .on('click', => @openFactlinkModal())
       .on 'inview', (event, isInView, visiblePart) =>
         @highlight_temporary(highlight_time_on_in_view) if ( isInView && visiblePart == 'both' )

+  attentions_do: (action) ->
+    for attention in [@button_attention, @highlight_attention]
+      attention[action]()
+
   highlight_temporary: (duration) ->
     @highlighter.highlight()
     setTimeout (=> highlight_attention.check_attention()), duration

-  onBlur: ->
-    @highlight_attention.neglect()
-    @button_attention.neglect()
-  onFocus: (e) =>
-    if e?
-      @show_button.setCoordinates($(e.target).offset().top, e.pageX)
-    @highlight_attention.attend()
-    @button_attention.attend()
+  onFocus: -> @attentions_do 'attend'
+  onBlur:  -> @attentions_do 'neglect'

-  shouldHaveEmphasis: => || @_loading
+  openFactlinkModal: ->
+    @show_button.startLoading()

-  openFactlinkModal: =>
-    @startLoading()
-    Factlink.showInfo @id, => @stopLoading()
-
-  startLoading: ->
-    @show_button.startLoading()
-    @highlight_attention.gain_attention()
-    @button_attention.gain_attention()
-
-  stopLoading: ->
-    @highlight_attention.loose_attention()
-    @button_attention.loose_attention()
+    @attentions_do 'gain_attention'
+    Factlink.showInfo @id, => @attentions_do 'loose_attention'

   destroy: ->
     for el in @elements


moved responsibilities around

moved responsibilities around

--- js-library/app/js/views/fact.coffee
+++ js-library/app/js/views/fact.coffee
@@ -12,7 +12,7 @@
   dehighlight: -> @$elements.removeClass(@class)

 class FactInteraction
-  constructor: (@fact) ->
+  constructor: (elements, @id, @options) ->
     @button_attention = new Factlink.AttentionSpan
       lost_attention:   => @show_button.hide()
       gained_attention: => @show_button.show()
@@ -25,31 +25,30 @@
       wait_for_attention:  delay_before_mouseover_detected
       wait_for_neglection: delay_before_mouseout_detected

-    @highlighter = new Highlighter $(@fact.elements), 'fl-active'
+    @highlighter = new Highlighter $(elements), 'fl-active'

     @show_button = new Factlink.ShowButton
       mouseenter: => @onFocus()
       mouseleave: => @onBlur()
-      click:      => @openFactlinkModal()
+      click:      => @onClick()

-    $(@fact.elements)
-      .on 'mouseenter', (e) =>
+    $(elements).on
+      mouseenter: (e) =>
         @onFocus()
         @show_button.setCoordinates($(e.target).offset().top, e.pageX)
-      .on 'mouseleave', =>
-        @onBlur()
-      .on 'click', =>
-        @openFactlinkModal()
+      mouseleave: => @onBlur()
+      click: => @onClick()

   attentions_do: (action) ->
     for attention in [@button_attention, @highlight_attention]
       attention[action]()

-  openFactlinkModal: ->
+  onClick: (options={}) =>
     @show_button.startLoading()

     @attentions_do 'gain_attention'
-    Factlink.showInfo @fact.id, => @attentions_do 'loose_attention'
+    @options.on_click success: =>
+      @attentions_do 'loose_attention'

   onFocus: -> @attentions_do 'attend'
   onBlur:  -> @attentions_do 'neglect'
@@ -58,21 +57,28 @@
     @attentions_do 'loose_attention'
     @show_button.destroy()

-class Factlink.Fact
-  constructor: (@id, @elements) ->
-    @fact_interaction = new FactInteraction(this)
-
-    @highlighter = new Highlighter $(elements), 'fl-highlight'
+class FactPromotion
+  constructor: (fact) ->
+    @highlighter = new Highlighter $(fact.elements), 'fl-highlight'

     @highlight_temporary highlight_time_on_load

-    $(@elements).on 'inview', (event, isInView, visiblePart) =>
+    $(fact.elements).on 'inview', (event, isInView, visiblePart) =>
       @highlight_temporary(highlight_time_on_in_view) if ( isInView && visiblePart == 'both' )
-

   highlight_temporary: (duration) ->
     @highlighter.highlight()
     setTimeout (=> @highlighter.dehighlight()), duration
+
+class Factlink.Fact
+  constructor: (@id, @elements) ->
+    @fact_interaction = new FactInteraction @elements, @id,
+      on_click: @openFactlinkModal
+    @fact_promotion = new FactPromotion(this)
+
+
+  openFactlinkModal: (options={})=>
+    Factlink.showInfo @id, => options.success?()

   destroy: ->
     for el in @elements


Fix cmd + enter submit

Fix cmd + enter submit

--- lineman/app/services/key_event_service.coffee
+++ lineman/app/services/key_event_service.coffee
@@ -29,5 +29,5 @@
     submitOnEnter: (scope) ->
       @registerKeyEvent scope, 'pressedEnter', scope.submit, (active, event) =>
         (event.ctrlKey or event.metaKey) and
-        angular.element(active).scope() == scope and
+        angular.element(active).scope().$$isolateBindings == scope.$$isolateBindings and
         _.contains(active.classList, 'lmo-primary-form-input')


Configure icon for SetLayerProperty component.

--- components/SetLayerProperties.coffee
+++ components/SetLayerProperties.coffee
@@ -7,7 +7,7 @@
 exports.getComponent = ->
   component = new noflo.Component
   component.description = 'Set layer properties.'
-  # component.icon = 'picture'
+  component.icon = 'cogs'

   # Add input ports
   component.inPorts.add 'in',


Another fix for agent

Another fix for agent

--- components/default-components.coffee
+++ components/default-components.coffee
@@ -906,6 +906,7 @@
             #     group: 'Advanced'
             #     label: 'Upload Photo'
             #     description: ''
+            #     uploaderType ''
             #     required: no
             #     readOnly: no
             #     template:


Pass in a destination option for artist and profile follows

--- components/follow_button/view.coffee
+++ components/follow_button/view.coffee
@@ -36,7 +36,11 @@
     @trigger 'click'

     unless @following
-      mediator.trigger 'open:auth', { mode: 'register', copy: "Sign up to follow #{@modelName}s" }
+      mediator.trigger 'open:auth',
+        mode: 'register'
+        copy: "Sign up to follow #{@modelName}s"
+        destination: "#{@model.href()}/follow"
+
       return false

     if @following.isFollowing @model.id


SplitView: Fixed split view's resizer reposition problem for animated horizontal split views when called resizePanel method.

--- components/split/splitresizer.coffee
+++ components/split/splitresizer.coffee
@@ -24,11 +24,12 @@
     if @isVertical then @getRelativeX() else @getRelativeY()

   _animateTo:(offset)->
-    offset -= @getWidth() / 2
     d = @parent.options.duration
     if @isVertical
+      offset -= @getWidth() / 2
       @$().animate left : offset,d
     else
+      offset -= @getHeight() / 2
       @$().animate top : offset,d

   dragFinished:(event, dragState)->


Add Raleway font

Add Raleway font

--- examples/Controls.cjsx
+++ examples/Controls.cjsx
@@ -66,6 +66,7 @@
   "Neuton"
   "Fanwood Text"
   "Petrona"
+  "Raleway"
 ]

 fonts = fonts.sort()


Classes for empty lines

Classes for empty lines

--- src/parser.coffee
+++ src/parser.coffee
@@ -324,6 +324,10 @@
         # If we have some text here that
         # is floating (not surrounded by a block),
         # wrap it in a generic block automatically.
+        #
+        # We will also send it through a pass through a comment parser here,
+        # for special handling of different forms of comments (or, e.g. in C mode, directives),
+        # and amalgamate multiline comments.
         placedSomething = false
         while line.length > 0
           if currentlyCommented
@@ -365,6 +369,7 @@
         if line.length is 0 and not placedSomething and stack[stack.length - 1]?.type in ['indent', 'document', undefined] and
             hasSomeTextAfter(lines, i)
           block = new model.Block 0, @opts.emptyLineColor, helper.BLOCK_ONLY
+          block.classes = ['__comment__', 'any-drop']

           head = helper.connect head, block.start
           head = helper.connect head, block.end


Update bindings and view parsing logic so that we're working with each individual section of a keypath (path + interface).

--- src/rivets.coffee
+++ src/rivets.coffee
@@ -23,7 +23,12 @@
   # All information about the binding is passed into the constructor; the
   # containing view, the DOM node, the type of binding, the model object and the
   # keypath at which to listen for changes.
-  constructor: (@view, @el, @type, @key, @keypath, @options = {}) ->
+  constructor: (@view, @el, @type, @keypath, @options = {}) ->
+    @formatters = @options.formatters || []
+    @setBinders()
+    @setModel()
+
+  setBinders: =>
     unless @binder = @view.binders[type]
       for identifier, value of @view.binders
         if identifier isnt '*' and identifier.indexOf('*') isnt -1
@@ -35,8 +40,17 @@

     @binder or= @view.binders['*']
     @binder = {routine: @binder} if @binder instanceof Function
-    @formatters = @options.formatters || []
-    @model = if @key then @view.models[@key] else @view.models
+
+  setModel: =>
+    interfaces = (k for k, v of @view.adapters)
+    tokens = Rivets.KeypathParser.parse @keypath, interfaces, '.'
+
+    @model = @view.models
+    @rootKey = tokens[0]
+    @key = tokens.pop()
+
+    for token, index in tokens
+      @model = @view.adapters[token.interface].read(@model, token.path)

   # Applies all the current formatters to the supplied value and returns the
   # formatted value.
@@ -74,7 +88,7 @@

   # Syncs up the view binding with the model.
   sync: =>
-    @set @view.adapters['.'].read @model, @keypath
+    @set @view.adapters[@key.interface].read @model, @key.path

   # Publishes the value currently set on the input element back to the model.
   publish: =>
@@ -87,14 +101,14 @@
       if @view.formatters[id]?.publish
         value = @view.formatters[id].publish value, args...

-    @view.adapters['.'].publish @model, @keypath, value
+    @view.adapters[@key.interface].publish @model, @key.path, value

   # Subscribes to the model for changes at the specified keypath. Bi-directional
   # routines will also listen for changes on the element to propagate them back
   # to the model.
   bind: =>
     @binder.bind?.call @, @el
-    @view.adapters['.'].subscribe @model, @keypath, @sync
+    @view.adapters[@key.interface].subscribe @model, @key.path, @sync
     @sync() if @view.config.preloadData

     if @options.dependencies?.length
@@ -112,7 +126,7 @@
   # Unsubscribes from the model and the element.
   unbind: =>
     @binder.unbind?.call @, @el
-    @view.adapters['.'].unsubscribe @model, @keypath, @sync
+    @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync

     if @options.dependencies?.length
       for dependency in @options.dependencies
@@ -129,13 +143,10 @@
   # Updates the binding's model from what is currently set on the view. Unbinds
   # the old model first and then re-binds with the new model.
   update: (models = {}) =>
-    if @key
-      if models[@key]
-        @view.adapters['.'].unsubscribe @model, @keypath, @sync
-        @model = models[@key]
-        @view.adapters['.'].subscribe @model, @keypath, @sync
-        @sync() if @view.config.preloadData
-    else
+    if models[@rootKey.path]
+      @view.adapters[@key.interface].unsubscribe @model, @key.path, @sync
+      @setModel()
+      @view.adapters[@key.interface].subscribe @model, @key.path, @sync
       @sync()

     @binder.update?.call @, models
@@ -199,9 +210,11 @@
 # differences while avoiding it being overwritten.
 class Rivets.TextBinding extends Rivets.Binding
   # Initializes a text binding for the specified view and text node.
-  constructor: (@view, @el, @type, @key, @keypath, @options = {}) ->
+  constructor: (@view, @el, @type, @keypath, @options = {}) ->
+    interfaces = (k for k, v of @view.adapters)
+    tokens = Rivets.KeypathParser.parse(@keypath, interfaces, '.')
     @formatters = @options.formatters || []
-    @model = if @key then @view.models[@key] else @view.models
+    @setModel()

   # A standard routine binder used for text node bindings.
   binder:
@@ -252,22 +265,14 @@

       pipes = (pipe.trim() for pipe in declaration.split '|')
       context = (ctx.trim() for ctx in pipes.shift().split '<')
-      path = context.shift()
-      splitPath = path.split '.'
+      keypath = context.shift()
+
       options.formatters = pipes
-
-      if splitPath[0]
-        key = splitPath.shift()
-      else
-        key = null
-        splitPath.shift()
-
-      keypath = splitPath.join '.'

       if dependencies = context.shift()
         options.dependencies = dependencies.split /\s+/

-      @bindings.push new Rivets[binding] @, node, type, key, keypath, options
+      @bindings.push new Rivets[binding] @, node, type, keypath, options

     parse = (node) =>
       unless node in skipNodes
@@ -553,6 +558,7 @@
           options =
             binders: @view.options.binders
             formatters: @view.options.formatters
+            adapters: @view.options.adapters
             config: @view.options.config

           (@nested = new Rivets.View(el, models, options)).bind()
@@ -632,6 +638,7 @@
           options =
             binders: @view.options.binders
             formatters: @view.options.formatters
+            adapters: @view.options.adapters
             config: {}

           options.config[k] = v for k, v of @view.options.config


Fix spinner.

Fix spinner.

--- src/router.coffee
+++ src/router.coffee
@@ -49,7 +49,7 @@

         indicateLoading: ->
             #$('#app-container').addClass 'invisible'
-            spinner.spin(document.body)
+            spinner.start(document.body)

         removeLoadingIndicator: ->
             $('#app-container').removeClass 'invisible'


Fixing dgram for changes in node v 0.10

Fixing dgram for changes in node v 0.10

--- lib/devices/Device.coffee
+++ lib/devices/Device.coffee
@@ -50,13 +50,13 @@
         @uuid = "uuid:#{res.uuid}"
         @address = res.address
         @httpPort = res.port
-        @broadcastSocket.bind @ssdp.port
-        @broadcastSocket.addMembership @ssdp.address
-        @broadcastSocket.setMulticastTTL @ssdp.ttl
-        @addServices()
-        @ssdpAnnounce()
-        console.log "Web server listening on http://#{@address}:#{@httpPort}"
-        @emit 'ready'
+        @broadcastSocket.bind @ssdp.port,'0.0.0.0', =>
+          @broadcastSocket.addMembership @ssdp.address
+          @broadcastSocket.setMulticastTTL @ssdp.ttl
+          @addServices()
+          @ssdpAnnounce()
+          console.log "Web server listening on http://#{@address}:#{@httpPort}"
+          @emit 'ready'

   # When HTTP and SSDP servers are started, we add and initialize services.


a better starting point for S-F

a better starting point for S-F

--- tree2.coffee
+++ tree2.coffee
@@ -170,6 +170,99 @@

     fn(this)

+class ShannonFanoNode extends Node
+  constructor: (@contains, @shape, @def_bbox, @label) ->
+    @update()
+
+  render: (ctx) ->
+
+    ctx.save()
+    setStyle(ctx, node_style)
+    ctx.translate(
+      @bbox.min.x + @bbox.len.x/2,
+      @bbox.min.y + @bbox.len.y/2)
+
+    ctx.save()
+    ctx.scale(@bbox.len.x,@bbox.len.y)
+    ctx.beginPath()
+    renderShape(ctx, @shape, .75)
+    # fix line width
+    ctx.scale(1/@bbox.len.x, 1/@bbox.len.y)
+    ctx.stroke()
+    ctx.restore()
+
+    setStyle(ctx, node_text_style)
+    ctx.textAlign = 'center'
+    ctx.textBaseline = 'top'
+
+    ctx.fillText(@label + ":" + @value,0,@bbox.len.y)
+
+    ctx.restore()
+
+    for n in @contains
+      n.render(ctx)
+
+
+  update: ->
+    @bbox = @def_bbox
+    @value = 0
+#      min: x: @bbox0.min.x + @bbox0.len.x*2, y:
+#                @bbox0.min.y
+#        len: x: default_node_radius * 2, y: default_node_radius * 2
+
+    if @contains.length > 0
+      minx = maxx = @contains[0].x
+      miny = maxy = @contains[0].y
+      @value = @contains[0].value
+
+      for n in @contains[1..]
+        @value += n.value
+
+        minx = Math.min(minx, n.x)
+        maxx = Math.max(maxx, n.x)
+        miny = Math.min(miny, n.y)
+        maxy = Math.max(maxy, n.y)
+
+      @bbox =
+        min: x: minx-default_node_radius, y: miny-default_node_radius
+        len: x: maxx-minx+default_node_radius*2, y:
+                maxy-miny+default_node_radius*3
+
+    @x = @bbox.min.x + @bbox.len.x/2
+    @y = @bbox.min.y + @bbox.len.y/2
+
+  addNode: (node) ->
+    @contains.push(node)
+    @update()
+
+  removeNode: (remove_node) ->
+    @contains = (n for n in @contains when n isnt remove_node)
+    @update()
+
+  isHit: (pos) ->
+    (pos.x >= @bbox.min.x && pos.x - @bbox.min.x < @bbox.len.x) and
+    (pos.y >= @bbox.min.y && pos.y - @bbox.min.y < @bbox.len.y)
+
+  forAll: (fn) ->
+    for n in @contains
+      n.forAll(fn)
+    fn(this)
+
+  tryAll: (fn) ->
+    for n in @contains
+      result = n.tryAll(fn)
+      if result?
+        return result
+
+    fn(this)
+
+  move: (newx, newy) ->
+    super(newx, newy)
+    @update()
+
+  clickend: (pos, t) ->
+    console.log("SF clicked")
+
 collection_dropdown_menu = [
   {
     name: 'sort by weight',
@@ -521,74 +614,13 @@
   constructor: (shape) ->
     super shape

-    @updateNodes([], [])
+  default_bbox: (min: (x: 100, y: 100), len: (x: 100, y: 100))

   copyNodesFrom: (nc) ->
-    super nc
-
-    @updateNodes(@nodes.slice(0), [])
-
-  updateNodes: (@nodes0, @nodes1) ->
-
-    @computeBBoxes()
-
-  nodesBBox: (nodes) ->
-    if nodes.length == 0
-      return min: (x:0, y:0), len: (x: 0, y: 0)
-
-    minx = maxx = nodes[0].x
-    miny = maxy = nodes[0].y
-    for n in nodes[1..]
-      minx = Math.min(minx, n.x)
-      maxx = Math.max(maxx, n.x)
-      miny = Math.min(miny, n.y)
-      maxy = Math.max(maxy, n.y)
-
-    min: x: minx-default_node_radius, y: miny-default_node_radius
-    len: x: maxx-minx+default_node_radius*2, y:
-            maxy-miny+default_node_radius*3
-
-  computeBBoxes: ->
-    @bbox0 = null
-    @bbox1 = null
-
-    if @nodes0.length > 0
-      @bbox0 = @nodesBBox(@nodes0)
-
-    if @nodes1.length > 0
-      @bbox1 = @nodesBBox(@nodes1)
-
-    if (not @bbox1?) and @bbox0
-      @bbox1 =
-        min: x: @bbox0.min.x + @bbox0.len.x*2, y:
-                @bbox0.min.y
-        len: x: default_node_radius * 2, y: default_node_radius * 2
+    @nodes = [ new ShannonFanoNode(nc.nodes, @shape, @default_bbox, "") ]

   render: (ctx, idx, t) ->
     super ctx, idx, t
-
-    for bbox, idx in [@bbox0, @bbox1]
-      ctx.save()
-      setStyle(ctx, node_style)
-      ctx.translate(
-        bbox.min.x + bbox.len.x/2,
-        bbox.min.y + bbox.len.y/2)
-
-      ctx.save()
-      ctx.scale(bbox.len.x,bbox.len.y)
-      ctx.beginPath()
-      renderShape(ctx, @shape, .75)
-      ctx.scale(1/bbox.len.x, 1/bbox.len.y)
-      ctx.stroke()
-      ctx.restore()
-
-      setStyle(ctx, node_text_style)
-      ctx.textAlign = 'center'
-      ctx.textBaseline = 'top'
-
-      ctx.fillText(''+idx,0,bbox.len.y)
-
-      ctx.restore()

   collection_dropdown_menu: shannon_fano_dropdown_menu


Add prefix to all messages.

Add prefix to all messages.

--- src/coffee/stockimport.coffee
+++ src/coffee/stockimport.coffee
@@ -10,13 +10,14 @@
 CHANNEL_KEY_FOR_XML_MAPPING = 'expectedStock'
 CHANNEL_REF_NAME = 'supplyChannel'
 CHANNEL_ROLES = ['InventorySupply', 'OrderExport', 'OrderImport']
+LOG_PREFIX = "[SphereStockImport] "

 class StockImport

   _log: (msg) ->
     if @client?
       client._logger.info msg
-    console.log "[SphereStockImport] #{msg}"
+    console.log "#{LOG_PREFIX}#{msg}"

   constructor: (options) ->
     if options?
@@ -60,7 +61,7 @@
         ElasticIo.returnFailure err, next
       .done()
     else
-      ElasticIo.returnFailure 'No data found in elastic.io msg.', next
+      ElasticIo.returnFailure "#{LOG_PREFIX}No data found in elastic.io msg.", next

   ensureChannelByKey: (rest, channelKey, channelRolesForCreation) ->
     deferred = Q.defer()
@@ -70,7 +71,7 @@
         deferred.reject "Error on getting channel: #{error}"
       else if response.statusCode isnt 200
         humanReadable = JSON.stringify body, null, 2
-        deferred.reject "Problem on getting channel: #{humanReadable}"
+        deferred.reject "#{LOG_PREFIX}Problem on getting channel: #{humanReadable}"
       else
         channels = body.results
         if _.size(channels) is 1
@@ -81,12 +82,12 @@
             roles: channelRolesForCreation
           rest.POST '/channels', channel, (error, response, body) ->
             if error?
-              deferred.reject "Error on creating channel: #{error}"
+              deferred.reject "#{LOG_PREFIX}Error on creating channel: #{error}"
             else if response.statusCode is 201
               deferred.resolve body
             else
               humanReadable = JSON.stringify body, null, 2
-              deferred.reject "Problem on creating channel: #{humanReadable}"
+              deferred.reject "#{LOG_PREFIX}Problem on creating channel: #{humanReadable}"

     deferred.promise

@@ -96,7 +97,7 @@
     else if mode is 'CSV'
       @performCSV fileContent, next
     else
-      Q.reject "Unknown import mode '#{mode}'!"
+      Q.reject "#{LOG_PREFIX}Unknown import mode '#{mode}'!"

   sumResult: (result) ->
     if _.isArray result
@@ -133,7 +134,7 @@

     # TODO: register this before!
     .on 'error', (error) ->
-      deferred.reject "Problem in parsing CSV: #{error}"
+      deferred.reject "#{LOG_PREFIX}Problem in parsing CSV: #{error}"

     deferred.promise

@@ -147,7 +148,7 @@
     deferred = Q.defer()
     xmlHelpers.xmlTransform xmlHelpers.xmlFix(fileContent), (err, xml) =>
       if err?
-        deferred.reject "Error on parsing XML: #{err}"
+        deferred.reject "#{LOG_PREFIX}Error on parsing XML: #{err}"
       else
         @ensureChannelByKey(@client._rest, CHANNEL_KEY_FOR_XML_MAPPING, CHANNEL_ROLES)
         .then (result) =>
@@ -202,7 +203,7 @@
         if entry[CHANNEL_REF_NAME]?
           msg.body.CHANNEL_ID = entry[CHANNEL_REF_NAME].id
         ElasticIo.returnSuccess msg, next
-      Q 'elastic.io messages sent.'
+      Q "#{LOG_PREFIX}elastic.io messages sent."
     else
       @_initMatcher().then =>
         @_createOrUpdate stocks
@@ -217,7 +218,7 @@
     .then (result) =>
       @existingInventoryEntries = result.body.results
       @_log "Existing entries: #{_.size @existingInventoryEntries}"
-      Q 'initialized'
+      Q '#{LOG_PREFIX}matcher initialized'

   _match: (entry) ->
     _.find @existingInventoryEntries, (existingEntry) ->


make sure app can reload if file is changes

--- packages/tower-application/server/watcher.coffee
+++ packages/tower-application/server/watcher.coffee
@@ -105,6 +105,9 @@
     delete require.cache[require.resolve(path)]

   fileUpdated: (path) ->
+    app = Tower.Application.instance()
+    app._loadApp() unless app.isInitialized
+
     pattern = (string) ->
       new RegExp(_.regexpEscape(string))

@@ -134,9 +137,16 @@
       # the klass may not exist if you just created a blank file
       # and then defined a model (which means the file was "updated")
       unless klass
+        # in case the app hasn't fully loaded but you changed something
         require(path)
       else
-        subclasses = klass.subclasses.getEach('__name__')
+        app = Tower.Application.instance()
+        subclasses = []
+
+        for key in _.keys(app)
+          value = app[key]
+          if klass != value && klass.detect(value)
+            subclasses.push(key)

         for name, index in subclasses
           subclasses[index] = _path.join(directory, _.camelize(name, true))


ENHANCE: Add web language extensions

ENHANCE: Add web language extensions

--- src/app/common/file-uploader/file-uploader.coffee
+++ src/app/common/file-uploader/file-uploader.coffee
@@ -47,7 +47,7 @@
         icon:       'fa-file-excel-o'
         name:       'CSV'
       code:
-        extensions: ['pas', 'cpp', 'c', 'cs', 'h', 'java', 'py']
+        extensions: ['pas', 'cpp', 'c', 'cs', 'h', 'java', 'py', 'js', 'html', 'coffee', 'rb', 'css']
         icon:       'fa-file-code-o'
         name:       'code'
       image:


Start of Document Server authorization

Start of Document Server authorization

--- src/code/providers/document-store-provider.coffee
+++ src/code/providers/document-store-provider.coffee
@@ -1,4 +1,7 @@
-{div} = React.DOM
+{div, button} = React.DOM
+
+authorizeUrl  = "http://document-store.herokuapp.com/user/authenticate"
+checkLoginUrl = "http://document-store.herokuapp.com/user/info"

 tr = require '../utils/translate'
 isString = require '../utils/is-string'
@@ -8,8 +11,14 @@

 DocumentStoreAuthorizationDialog = React.createFactory React.createClass
   displayName: 'DocumentStoreAuthorizationDialog'
+
+  authenticate: ->
+    @props.provider.authorize()
+
   render: ->
-    (div {}, "TODO: DocumentStoreAuthorizationDialog")
+    (div {},
+      (button {onClick: @authenticate}, 'Authorization Needed')
+    )

 class DocumentStoreProvider extends ProviderInterface

@@ -26,6 +35,73 @@

   authorized: (@authCallback) ->

+  authorize: ->
+    @_showLoginWindow()
+    @_checkLogin()
+
+  _loginSuccessful: (data) ->
+    if @_loginWindow then @_loginWindow.close()
+    alert("Welcome #{data.name}")
+    @authCallback true
+
+  _checkLogin: ->
+    provider = @
+    $.ajax
+      dataType: 'json'
+      url: checkLoginUrl
+      xhrFields:
+        withCredentials: true
+      success: (data) ->
+        provider._loginSuccessful(data)
+      error: ->
+        # nothing yet
+
+  _loginWindow: null
+
+  _showLoginWindow: ->
+    if @_loginWindow and not @_loginWindow.closed
+      @_loginWindow.focus()
+    else
+
+      computeScreenLocation = (w, h) ->
+        screenLeft = window.screenLeft or screen.left
+        screenTop  = window.screenTop  or screen.top
+        width  = window.innerWidth  or document.documentElement.clientWidth  or screen.width
+        height = window.innerHeight or document.documentElement.clientHeight or screen.height
+
+        left = ((width / 2) - (w / 2)) + screenLeft
+        top = ((height / 2) - (h / 2)) + screenTop
+        return {left, top}
+
+      width = 1000
+      height = 480
+      position = computeScreenLocation width, height
+      windowFeatures = [
+        'width=' + width
+        'height=' + height
+        'top=' + position.top or 200
+        'left=' + position.left or 200
+        'dependent=yes'
+        'resizable=no'
+        'location=no'
+        'dialog=yes'
+        'menubar=no'
+      ]
+
+      @_loginWindow = window.open(authorizeUrl, 'auth', windowFeatures.join())
+
+      pollAction = =>
+        try
+          href = @_loginWindow.location.href
+          if (href is window.location.href)
+            clearInterval poll
+            @_loginWindow.close()
+            @_checkLogin()
+        catch e
+          console.log e
+
+      poll = setInterval pollAction, 200
+
   renderAuthorizationDialog: ->
     (DocumentStoreAuthorizationDialog {provider: @, authCallback: @authCallback})


Add a ui reference to replace the upper/lower bound input selectors

--- src/coffee/cilantro/ui/field/controls/date.coffee
+++ src/coffee/cilantro/ui/field/controls/date.coffee
@@ -21,6 +21,8 @@

         ui:
             inOutSelect: '.btn-select'
+            lowerBound: '.range-from'
+            upperBound: '.range-to'

         onRender: ->
             @inOutSelect = new button.ButtonSelect
@@ -35,8 +37,8 @@

             @inOutSelect.render().$el.prependTo(@$el)
             @$('.help-block').hide()
-            @$('.range-from').datepicker({'autoclose': true})
-            @$('.range-to').datepicker({'autoclose': true})
+            @ui.lowerBound.datepicker({'autoclose': true})
+            @ui.upperBound.datepicker({'autoclose': true})

             @set(@context)

@@ -51,8 +53,8 @@
             return @model.id

         getOperator: ->
-            from = @$('.range-from').val()
-            to = @$('.range-to').val()
+            from = @ui.lowerBound.val()
+            to = @ui.upperBound.val()
             operator = ''

             if from != "" and to != ""
@@ -67,11 +69,11 @@
             return operator

         getValue: ->
-            from_text = @$('.range-from').val()
-            from_date = @$('.range-from').data('datepicker').getFormattedDate()
+            from_text = @ui.lowerBound.val()
+            from_date = @ui.lowerBound.data('datepicker').getFormattedDate()

-            to_text = @$('.range-to').val()
-            to_date = @$('.range-to').data('datepicker').getFormattedDate()
+            to_text = @ui.upperBound.val()
+            to_date = @ui.upperBound.data('datepicker').getFormattedDate()

             value = []

@@ -99,20 +101,20 @@
             switch @operator
                 when 'range', '-range'
                     if value[0]?
-                        @$('.range-from').data('datepicker').setDate(
+                        @ui.lowerBound.data('datepicker').setDate(
                             new Date(value[0]))
                     else
-                        @$('.range-from').val('')
+                        @ui.lowerBound.val(@minLowerBound ? '')
                     if value[1]?
-                        @$('.range-to').data('datepicker').setDate(
+                        @ui.upperBound.data('datepicker').setDate(
                             new Date(value[1]))
                     else
-                        @$('.range-to').val('')
+                        @ui.upperBound.val(@maxUpperBound ? '')
                 when 'gte'
-                    @$('.range-from').data('datepicker').setDate(
+                    @ui.lowerBound.data('datepicker').setDate(
                         new Date(value))
                 when 'lte'
-                    @$('.range-to').data('datepicker').setDate(
+                    @ui.upperBound.data('datepicker').setDate(
                         new Date(value))

     {DateControl}


Improvement: Allow empty ModelFieldView to be saved as null value with Enter key.

--- lib/ST/ModelFieldView.coffee
+++ lib/ST/ModelFieldView.coffee
@@ -131,7 +131,7 @@
             @selectedResult(0)
         event.preventDefault()
       when 13 # ENTER
-        @blur() if @_selectedResult >= 0
+        @blur() if @_selectedResult >= 0 || @_inputValue == ''
         event.preventDefault()
       when 27 # Escape
         @blur()


Fix ModelFieldView submission on enter when no result selected.

--- lib/ST/ModelFieldView.coffee
+++ lib/ST/ModelFieldView.coffee
@@ -130,7 +130,7 @@
             @selectedResult(0)
         event.preventDefault()
       when 13 # ENTER
-        @blur()
+        @blur() if @_selectedResult >= 0
         event.preventDefault()
       when 27 # Escape
         @blur()


improve storage docs

improve storage docs

--- lib/argumenta/storage.coffee
+++ lib/argumenta/storage.coffee
@@ -1,7 +1,39 @@
 Base       = require '../argumenta/base'
 User       = require '../argumenta/user'

+# Storage persists users and objects to a backend store.
+
 class Storage extends Base
+
+  # Construction
+  # ------------
+
+  # Constructs a new Storage instance, backed by a particular type of store.
+  #
+  # Create a memory-backed storage instance:
+  #
+  #     storage = new Storage
+  #       storageType: 'local'
+  #
+  # Create a mongo-backed storage instance:
+  #
+  #     storage = new Storage
+  #       storageType: 'mongo',
+  #       storageUrl: 'mongodb://localhost:27017'
+  #
+  # @param [Object] opts The storage backing options.
+  # @param [String] opts.storageType The backing type: 'local' or 'mongo'.
+  # @param [String] opts.storageUrl  The backing url, if needed.
+  constructor: (opts={}) ->
+    {storageType} = opts
+
+    LocalStore = require './storage/local_store'
+
+    switch storageType
+      when 'local'
+        @store = new LocalStore()
+      else
+        throw new @Error "Construction error: Invalid storageType: #{storageType}"

   # Errors
   # ------
@@ -13,24 +45,13 @@
   # ie, attempts to overwrite existing users or objects.
   ConflictError: @ConflictError = @Errors.StorageConflict

-  # Construction
-  # ------------
+  # Instance Methods
+  # ----------------

-  # Constructs a new storage instance, backed by a particular type of store.
-  constructor: (@options = {}) ->
-    {storageType} = @options
-
-    LocalStore = require './storage/local_store'
-
-    switch storageType
-#     when 'mongo'
-#       @store = new MongoStore {storageUrl: options.storageUrl}
-      when 'local'
-        @store = new LocalStore()
-      else
-        throw new @Error "Construction error: Invalid storageType: #{storageType}"
-
-  # Add a user to the store, given a valid `user`.
+  # Add a user to the store.
+  #
+  # @param [User] user A valid user with a unique username.
+  # @param [Function] cb(err) Called on completion or error.
   addUser: (user, cb) ->
     unless user instanceof User
       return cb new @Error "User instance required to add user."
@@ -43,24 +64,34 @@
       return cb null

   # Delete *all* entities from the store.
+  #
+  # @param [Function] cb(err) Called on completion or error.
   clearAll: (cb) ->
     @store.clearAll (err) ->
       return cb err

-  # Get a user by `username`, omitting sensitive information.
-  # See: `getPasswordHash()`
+  # Get a user by username, omitting sensitive fields.
+  #
+  # @see getPasswordHash()
+  # @param [String]   username Of the user to retrieve.
+  # @param [Function] cb Called on completion or error.
   getUserByName: (username, cb) ->
     @store.getUserByName username, (err, user) ->
       return cb new @Error("Failed getting user: " + username, err) if err
       return cb null, user

-  # Get a user's password hash, given `username`.
+  # Get a user's password hash.
+  #
+  # @param [String]   username Of the user whose hash to retrieve.
+  # @param [Function] cb Called on completion or error.
   getPasswordHash: (username, cb) ->
     @store.getPasswordHash username, (err, password_hash) ->
       return cb new @Error("Failed getting user: "  + username, err) if err
       return cb null, password_hash

-  # Get an array of all users, omitting sensitive information.
+  # Get an array of all users, omitting sensitive fields.
+  #
+  # @param [Function] cb Called on completion or error.
   getAllUsers: (cb) ->
     @store.getAllUsers (err, users) ->
       return cb new @Error "Failed getting all users from store." if err


Fix typo in the script description

Fix typo in the script description

Script description describes command top-commiters, when actual command is top-commiter

--- src/scripts/github-commiters.coffee
+++ src/scripts/github-commiters.coffee
@@ -10,7 +10,7 @@
 #
 # Commands:
 #   hubot repo commiters <repo> - shows commiters of repository
-#   hubot repo top-commiters <repo> - shows top commiters of repository
+#   hubot repo top-commiter <repo> - shows top commiters of repository
 #
 # Notes:
 #   HUBOT_GITHUB_API allows you to set a custom URL path (for Github enterprise users)


Removing keymap.cson

Removing keymap.cson

--- keymap.cson
+++ keymap.cson
@@ -1,8 +0,0 @@
-'.CodeMirror.vim-mode.insert-mode textarea':
-  'ctrl-[': 'vim:exit-insert-mode'
-  'j k': 'vim:exit-insert-mode'
-  'k j': 'vim:exit-insert-mode'
-'.CodeMirror.vim-mode.replace-mode textarea':
-  'ctrl-[': 'vim:exit-insert-mode'
-  'j k': 'vim:exit-insert-mode'
-  'k j': 'vim:exit-insert-mode'


touch the session rather than setting the expires, same result

--- services/web/app/coffee/infrastructure/Server.coffee
+++ services/web/app/coffee/infrastructure/Server.coffee
@@ -64,7 +64,7 @@

 	# Measure expiry from last request, not last login
 	app.use (req, res, next) ->
-		req.session.expires = Date.now() + cookieSessionLength
+		req.session.touch()
 		next()

 	app.use (req, res, next) ->


Debugging.

Debugging.

--- src/tucker.coffee
+++ src/tucker.coffee
@@ -92,10 +92,10 @@
   robot.hear /tucker/i, (res) ->
     message = res.message.text
     name = message.search(robot.name)
-    rn = robot.name
+    re = new RegExp robot.name, 'i'
     robot.logger.info "name match = #{name}"
     robot.logger.info "robot name is = #{rn}"
-    if message.search(robot.name) is -1
+    if message.search(re) is -1
       quote = res.random allQuotes
       if process.env.HUBOT_TUCKER_BYLINE
         res.send "\"#{quote}\" -- Malcolm Tucker"


fix problem getting user avatar images. Fixes #186

--- src/ui/app.coffee
+++ src/ui/app.coffee
@@ -44,7 +44,7 @@
 # events from hangupsjs
 require('./events').forEach (n) -> ipc.on n, (ev, data) -> action n, data
 # response from getentity
-ipc.on 'getentity:result', (r, data) ->
+ipc.on 'getentity:result', (ev, r, data) ->
     action 'addentities', r.entities, data?.add_to_conv

 ipc.on 'resize', (ev, dim) -> action 'resize', dim