Test/Stack update reinitializeTeamStack

Test/Stack update reinitializeTeamStack

--- client/test/lib/helpers/stackshelpers.coffee
+++ client/test/lib/helpers/stackshelpers.coffee
@@ -92,7 +92,8 @@
         .waitForElementVisible editNamebutton, 20000
         .click editNamebutton
         .clearValue stackTemplateNameArea
-        .pause 1000
+        .pause 3000
+        .setValue stackTemplateNameArea, ''
         .setValue stackTemplateNameArea, 'NewStackName'
         .click saveButtonSelector, ->
           browser.pause 3000, ->
@@ -272,7 +273,6 @@
     wrongCustomVariable    = "foo: '"
     correctCustomVariable  = "foo: 'bar'"

-    browser.click closeModal
     @gotoStackTemplate browser, =>
       browser.waitForElementVisible stackTemplateNameArea, 2000
       @switchTabOnStackCatalog browser, 'variables'
@@ -358,7 +358,6 @@

   makeStackTeamDefault: (browser, done) ->
-    console.log('testtt')
     browser.elements 'css selector', makeTeamDefaultButton, (result) ->
       result.value.map (value) ->
         browser.elementIdText value.ELEMENT, (res) ->
@@ -373,6 +372,9 @@
   reinitializeTeamStack: (browser, done) ->
     browser
       .waitForElementVisible '.StackEditor-ShareModal.kdmodal footer', 20000
+      .waitForElementVisible '.kdcustomcheckbox input[type="checkbox"]'
+      .click '.kdcustomcheckbox input[type="checkbox"]'
+      .pause 3000
       .click shareButton
       .waitForElementVisible '.ContentModal.content-modal main', 20000
       .click shareButton, ->


set boolean onFocus, not event

set boolean onFocus, not event

--- resources/assets/coffee/react/beatmap-discussions/new-discussion.coffee
+++ resources/assets/coffee/react/beatmap-discussions/new-discussion.coffee
@@ -110,7 +110,7 @@
                   value: if @canPost() then @state.message else ''
                   onChange: @setMessage
                   onKeyDown: @handleKeyDown
-                  onFocus: @setSticky
+                  onFocus: @onFocus
                   placeholder:
                     if @canPost()
                       osu.trans "beatmaps.discussions.message_placeholder.#{@props.mode}", version: @props.currentBeatmap.version
@@ -242,6 +242,10 @@
     @cache.nearbyDiscussions.discussions

+  onFocus: =>
+    @setSticky true
+
+
   parseTimestamp: (message) =>
     timestampRe = message.match /\b(\d{2,}):([0-5]\d)[:.](\d{3})\b/


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -133,4 +133,5 @@
   return self

 Camera.defaultModules = [
- +  ""
+]


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/camera.coffee
+++ src/camera.coffee
@@ -35,6 +35,7 @@
       width: 2 * deadzone.x
       height: 2 * deadzone.y

+
     I.scroll = Point(
       I.scroll.x.clamp(centerRect.left, centerRect.right)
       I.scroll.y.clamp(centerRect.top, centerRect.bottom)


Remove debugging request logging

Remove debugging request logging

--- src/client.coffee
+++ src/client.coffee
@@ -8,9 +8,6 @@

 CallbackHandler = {
   do: (req, opts) =>
-    console.log 'request:', req
-    console.log 'opts:', opts
-
     request req, (err, res, body) ->
       if (err)
         opts.error?(err)


Get cursor tracking working

Get cursor tracking working

--- src/client.coffee
+++ src/client.coffee
@@ -141,6 +141,15 @@
     for k of @groups
       if @groups[k].name == name
         return @groups[k]
+
+  getChannelGroupOrDMByID: (id) ->
+    if id[0] == 'C'
+      return @getChannelByID id
+    else
+      if id[0] == 'G'
+        return @getGroupByID id
+      else
+        return @getDMByID id

   #
   # Message handler callback and dispatch
@@ -165,14 +174,7 @@
         # find channel/group/dm and add it to history
         m = new Message @, message

-        if message.channel[0] == 'C'
-          channel = @getChannelByID message.channel
-        else
-          if message.channel[0] == 'G'
-            channel = @getGroupByID message.channel
-          else
-            channel = @getDMByID message.channel
-
+        channel = @getChannelGroupOrDMByID message.channel
         if channel
           console.log "Adding message "+m.ts+" to "+channel.name
           channel.addMessage m
@@ -180,7 +182,12 @@
           console.warn "Could not find channel "+message.channel+" for message"

       when "channel_marked"
-        console.log "Channel marked: "+message
+        channel = @getChannelGroupOrDMByID message.channel
+        if channel
+          console.log "Moved cursor to "+message.ts+" in "+channel.name
+          channel.last_read = message.ts
+        else
+          console.warn "Could not find channel "+message.channel+" for channel_marked"
       when "user_typing"
         console.log "User is typing"
       else


Export Directory class from core

Export Directory class from core

--- exports/atom-api.coffee
+++ exports/atom-api.coffee
@@ -6,6 +6,7 @@
   $$: $$
   $$$: $$$
   BufferedNodeProcess: '../src/buffered-node-process'
+  Directory: require '../src/directory'
   Editor: require '../src/editor'
   EventEmitter: require '../src/event-emitter'
   File: require '../src/file'


cleaning up & commenting

cleaning up & commenting

Former-commit-id: 1c0947313ad36cf7d853a633fe100f35ef100e08 [formerly 0106d1720aef4696519d82583835740d405b487b] [formerly 1417b2fe6268d201f5c6f8f01718c67896563e10 [formerly 73aa3b0321731b23c398d568622db28be196b604]] [formerly 9dba154fd394d41d929056abd7d778c9867a9231 [formerly 1dc99d5a1827e67f5ddfd27ad9e5cbbeb0f94d30] [formerly c588f721744c1fe2c9217943e11aadb0eb56a850 [formerly c588f721744c1fe2c9217943e11aadb0eb56a850 [formerly 2b3a9c97cd340a568a446c6650ed4d7b821b8245]]]]
Former-commit-id: cb0dc8d19ccf32089de8d92f4b714b349d5cd90c [formerly 93bc89a74e73c8a5cc89ce34a06029d5575a1a60] [formerly 4fadfc3d8d847c5bca19b564b18b642e3ad29da9 [formerly 37475ef4539cec24ef535e1defd760a9fc5bf04d]]
Former-commit-id: bf1a97aa7bbd4f2c53c9fa64d7439f6ed856d0f9 [formerly 01c2bd9fc1bb965396aa6da655427441c0e0871b]
Former-commit-id: e1335f17cd88bf78a4aabb945b9e769f23029231
Former-commit-id: 55072434a88c7c5c204ff50ea5f7ffeb45df51a4

--- @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
+++ @client/assets/javascripts/vendor_extensions/jquery_extensions.js.coffee
@@ -1,31 +1,29 @@
 do ($) ->

   $.fn.stickyTopBottom = (options = {}) ->
-
+    # only works in browsers that support CSS3 transforms (i.e. IE9+)

     ## ##############
     #initialization
-    _.defaults options,
-      container: $('body')
-      top_offset: 0
-      bottom_offset: 0
+    options = $.extend
+      container: $('body') #reference element for starting and stopping the sticking (doesn't actually have to contain the element)
+      top_offset: 0 #distance from top of viewport to stick top of element
+      bottom_offset: 0 #distance from bottom of viewport to stick bottom of element
+    , options

     $el = $(this)

+    #Get the top of the reference element. If the container moves, would need to move this into scroll handler.
+    #If the container is translated Y, then this method will fail I believe.
     container_top = options.container.offset().top
     element_top = $el.offset().top
-
-    current_translate = 0

     viewport_height = $(window).height()
     $(window).on 'resize', ->
       viewport_height = $(window).height()

-    last_viewport_top = document.documentElement.scrollTop || document.body.scrollTop
-
-    setTop = (translation) ->
-      #this will overwrite existing transform...
-      $el.css 'transform', "translate(0, #{current_translate}px)"
+    # element_height = $el.height()
+    # container_height = options.container.height()

     ## #################
     # The meat: scroll handler
@@ -37,41 +35,56 @@
     #    if scrolled past bottom of element, position bottom of element at bottom of viewport
     #      (stick to bottom)

-    $(window).scroll (event) ->
+    current_translate = 0
+    last_viewport_top = document.documentElement.scrollTop || document.body.scrollTop
+    viewport_top = last_viewport_top
+    frame_requested = false
+
+    $(window).scroll (ev) ->
+      # Need to reset element's height each scroll event because it may have change height
+      # since initialization.
+      # Warning: checking height is performance no-no
+      element_height = $el.height()
+
       viewport_top = document.documentElement.scrollTop || document.body.scrollTop
       viewport_bottom = viewport_top + viewport_height
       effective_viewport_top = viewport_top + options.top_offset
       effective_viewport_bottom = viewport_bottom - options.bottom_offset

-      # warning: checking height causes bad performance
-      element_height = $el.height()
+      is_scrolling_up = viewport_top < last_viewport_top
+      element_fits_in_viewport = element_height < viewport_height

-      scrolling_up = viewport_top < last_viewport_top
-      taller_than_viewport = element_height > viewport_height
+      new_translation = null
+      if is_scrolling_up
+        if effective_viewport_top < container_top # if we're scrolled past container top
+          new_translation = 0
+        else if effective_viewport_top < element_top + current_translate
+          new_translation = effective_viewport_top - element_top

-      if scrolling_up
-        if effective_viewport_top < container_top # if we're scrolled past container top
-          current_translate = 0
-          setTop current_translate
-        else if effective_viewport_top < element_top + current_translate
-          current_translate = effective_viewport_top - element_top
-          setTop current_translate
+      else if element_fits_in_viewport
+        if effective_viewport_top > element_top + current_translate
+          new_translation = effective_viewport_top - element_top

-      else if !taller_than_viewport
-        console.log !taller_than_viewport, effective_viewport_top, element_top, current_translate, effective_viewport_top > element_top + current_translate
-        if effective_viewport_top > element_top + current_translate
-          current_translate = effective_viewport_top - element_top
-          setTop current_translate
       else # scrolling down
-        container_bottom = container_top + options.container.height() #warning: checking height causes bad performance
+        container_height = options.container.height()
+        container_bottom = container_top + container_height #warning: checking height is performance no-no
         if effective_viewport_bottom > container_bottom #scrolled past container bottom
-          current_translate = container_bottom - (element_top + element_height)
-          setTop current_translate
+          new_translation = container_bottom - (element_top + element_height)
         else if effective_viewport_bottom > element_top + element_height + current_translate
-          current_translate = effective_viewport_bottom - (element_top + element_height)
-          setTop current_translate
+          new_translation = effective_viewport_bottom - (element_top + element_height)
+
+      if new_translation != null
+        current_translate = new_translation
+
+        $el[0].style["-webkit-backface-visibility"] = "hidden"
+        $el[0].style.transform = "translate(0, #{current_translate}px)"
+        $el[0].style['-webkit-transform'] = "translate(0, #{current_translate}px)"
+        $el[0].style['-ms-transform'] = "translate(0, #{current_translate}px)"
+        $el[0].style['-moz-transform'] = "translate(0, #{current_translate}px)"
+

       last_viewport_top = viewport_top
+

   $.fn.ensureInView = (options = {}) ->


export graph image

export graph image

--- app/graphing.coffee
+++ app/graphing.coffee
@@ -11,6 +11,13 @@
 GraphDrawing = require './graph_drawing'
 Context = require './context'
 Builtins = require './builtins'
+App = require './app'
+
+ExportModal = React.createClass
+  render: ->
+    footer = React.DOM.button {onClick: @props.dismiss}, 'OK'
+    App.ModalComponent {footer},
+      React.DOM.img {src: @props.url, style: {border: '1px solid #aaa'}}

 Graphing = modules.export exports, 'graphing', ({component_fn, doc, cmd, fn}) ->
   brushParams = (brush) ->
@@ -173,8 +180,17 @@

   GraphComponent: React.createClass
     displayName: 'GraphComponent'
+    export: ->
+      @state.graph.exportImage().then (url) ->
+        App.pushModal handler: ExportModal, props: {url}
     render: ->
-      React.DOM.div {className: 'graph'}
+      React.DOM.div {className: 'graph', style: {position: 'relative'}},
+        React.DOM.span {className: 'fa-stack', title: 'Export', style: {position: 'absolute', top: '5px', right: '5px', cursor: 'pointer'}},
+          React.DOM.i {className: 'fa fa-square fa-stack-2x', style: {color: '#fff'}}
+          React.DOM.i
+            onClick: @export
+            className: 'fa fa-share-square-o fa-stack-1x'
+            style: {color: '#ccc'}
     componentWillReceiveProps: (nextProps) ->
       @state.unsubscribe()
       @subscribe(nextProps.model, @state.graph)


Using new group routing function.

Using new group routing function.

--- web/coffee/team.coffee
+++ web/coffee/team.coffee
@@ -11,7 +11,7 @@
         $("#team-info").html renderTeamInformation({data: data.data})

 load_group_info = ->
-  $.get "/api/group"
+  $.get "/api/group/list"
   .done (data) ->
     switch data["status"]
       when 0


atom.getVersion returns plain string, not string object.

--- spec/app/atom-spec.coffee
+++ spec/app/atom-spec.coffee
@@ -280,7 +280,7 @@

   describe ".getVersion", ->
     it "returns the current version number", ->
-      expect(atom.getVersion()).toBeInstanceOf String
+      expect(typeof atom.getVersion()).toBe 'string'

   xdescribe "modal native dialogs", ->
     beforeEach ->


Removed unmaintained file

Removed unmaintained file

--- spec/atom-gdb-spec.coffee
+++ spec/atom-gdb-spec.coffee
@@ -1,62 +0,0 @@
-AtomGdb = require '../lib/atom-gdb'
-
-# Use the command `window:run-package-specs` (cmd-alt-ctrl-p) to run specs.
-#
-# To run a specific `it` or `describe` block add an `f` to the front (e.g. `fit`
-# or `fdescribe`). Remove the `f` to unfocus the block.
-
-describe "AtomGdb", ->
-  [workspaceElement, activationPromise] = []
-
-  beforeEach ->
-    workspaceElement = atom.views.getView(atom.workspace)
-    activationPromise = atom.packages.activatePackage('atom-gdb')
-
-  describe "when the atom-gdb:toggle event is triggered", ->
-    it "hides and shows the modal panel", ->
-      # Before the activation event the view is not on the DOM, and no panel
-      # has been created
-      expect(workspaceElement.querySelector('.atom-gdb')).not.toExist()
-
-      # This is an activation event, triggering it will cause the package to be
-      # activated.
-      atom.commands.dispatch workspaceElement, 'atom-gdb:toggle'
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        expect(workspaceElement.querySelector('.atom-gdb')).toExist()
-
-        atomGdbElement = workspaceElement.querySelector('.atom-gdb')
-        expect(atomGdbElement).toExist()
-
-        atomGdbPanel = atom.workspace.panelForItem(atomGdbElement)
-        expect(atomGdbPanel.isVisible()).toBe true
-        atom.commands.dispatch workspaceElement, 'atom-gdb:start'
-        expect(atomGdbPanel.isVisible()).toBe false
-
-    it "hides and shows the view", ->
-      # This test shows you an integration test testing at the view level.
-
-      # Attaching the workspaceElement to the DOM is required to allow the
-      # `toBeVisible()` matchers to work. Anything testing visibility or focus
-      # requires that the workspaceElement is on the DOM. Tests that attach the
-      # workspaceElement to the DOM are generally slower than those off DOM.
-      jasmine.attachToDOM(workspaceElement)
-
-      expect(workspaceElement.querySelector('.atom-gdb')).not.toExist()
-
-      # This is an activation event, triggering it causes the package to be
-      # activated.
-      atom.commands.dispatch workspaceElement, 'atom-gdb:toggle'
-
-      waitsForPromise ->
-        activationPromise
-
-      runs ->
-        # Now we can test for view visibility
-        atomGdbElement = workspaceElement.querySelector('.atom-gdb')
-        #expect(atomGdbElement).toBeVisible()
-        #atom.commands.dispatch workspaceElement, 'atom-gdb:toggle_breakpoints'
-        #expect(atomGdbElement).not.toBeVisible()


Don't use deprecated APIs in specs

Don't use deprecated APIs in specs

--- spec/autosave-spec.coffee
+++ spec/autosave-spec.coffee
@@ -6,15 +6,25 @@

   beforeEach ->
     atom.workspaceView = new WorkspaceView()
+
     waitsForPromise ->
       atom.packages.activatePackage("autosave")

     runs ->
       atom.workspaceView.attachToDom()
-      initialActiveItem = atom.workspaceView.openSync('sample.js')
-      otherItem1 = atom.project.openSync('sample.coffee')
-      otherItem2 = otherItem1.copy()

+    waitsForPromise ->
+      atom.workspace.open('sample.js')
+
+    runs ->
+      initialActiveItem = atom.workspace.getActiveEditor()
+
+    waitsForPromise ->
+      atom.project.open('sample.coffee').then (o) ->
+        otherItem1 = o
+        otherItem2 = otherItem1.copy()
+
+    runs ->
       spyOn(initialActiveItem, 'save')
       spyOn(otherItem1, 'save')
       spyOn(otherItem2, 'save')
@@ -22,7 +32,7 @@
   describe "when the item is not modified", ->
     it "does not autosave the item", ->
       atom.config.set('autosave.enabled', true)
-      leftPane = atom.workspaceView.getActivePane()
+      leftPane = atom.workspaceView.getActivePaneView()
       rightPane = leftPane.splitRight(otherItem1)
       expect(initialActiveItem.save).not.toHaveBeenCalled()

@@ -42,7 +52,7 @@

     describe "when a new pane is created", ->
       it "saves the item if autosave is enabled and the item has a uri", ->
-        leftPane = atom.workspaceView.getActivePane()
+        leftPane = atom.workspaceView.getActivePaneView()
         rightPane = leftPane.splitRight(otherItem1)
         expect(initialActiveItem.save).not.toHaveBeenCalled()

@@ -54,43 +64,47 @@
     describe "when an item is destroyed", ->
       describe "when the item is the active item", ->
         it "does not save the item if autosave is enabled and the item has a uri", ->
-          leftPane = atom.workspaceView.getActivePane()
+          leftPane = atom.workspaceView.getActivePaneView()
           rightPane = leftPane.splitRight(otherItem1)
           leftPane.focus()
-          expect(initialActiveItem).toBe atom.workspaceView.getActivePaneItem()
-          leftPane.removeItem(initialActiveItem)
+          expect(initialActiveItem).toBe atom.workspace.getActivePaneItem()
+          leftPane.destroyItem(initialActiveItem)
           expect(initialActiveItem.save).not.toHaveBeenCalled()

           otherItem2.setText("I am also modified")
           atom.config.set("autosave.enabled", true)
           leftPane = rightPane.splitLeft(otherItem2)
-          expect(otherItem2).toBe atom.workspaceView.getActivePaneItem()
-          leftPane.removeItem(otherItem2)
+          expect(otherItem2).toBe atom.workspace.getActivePaneItem()
+          leftPane.destroyItem(otherItem2)
           expect(otherItem2.save).toHaveBeenCalled()

       describe "when the item is NOT the active item", ->
         it "does not save the item if autosave is enabled and the item has a uri", ->
-          leftPane = atom.workspaceView.getActivePane()
+          leftPane = atom.workspaceView.getActivePaneView()
           rightPane = leftPane.splitRight(otherItem1)
-          expect(initialActiveItem).not.toBe atom.workspaceView.getActivePaneItem()
-          leftPane.removeItem(initialActiveItem)
+          expect(initialActiveItem).not.toBe atom.workspace.getActivePaneItem()
+          leftPane.destroyItem(initialActiveItem)
           expect(initialActiveItem.save).not.toHaveBeenCalled()

           otherItem2.setText("I am also modified")
           atom.config.set("autosave.enabled", true)
           leftPane = rightPane.splitLeft(otherItem2)
           rightPane.focus()
-          expect(otherItem2).not.toBe atom.workspaceView.getActivePaneItem()
-          leftPane.removeItem(otherItem2)
+          expect(otherItem2).not.toBe atom.workspace.getActivePaneItem()
+          leftPane.destroyItem(otherItem2)
           expect(otherItem2.save).toHaveBeenCalled()

     describe "when the item does not have a URI", ->
       it "does not save the item", ->
-        pathLessItem = atom.workspaceView.openSync()
-        spyOn(pathLessItem, 'save').andCallThrough()
-        pathLessItem.setText('text!')
-        expect(pathLessItem.getUri()).toBeFalsy()
+        waitsForPromise ->
+          atom.workspace.open()

-        atom.config.set('autosave.enabled', true)
-        atom.workspaceView.getActivePane().removeItem(pathLessItem)
-        expect(pathLessItem.save).not.toHaveBeenCalled()
+        runs ->
+          pathLessItem = atom.workspace.getActiveEditor()
+          spyOn(pathLessItem, 'save').andCallThrough()
+          pathLessItem.setText('text!')
+          expect(pathLessItem.getUri()).toBeFalsy()
+
+          atom.config.set('autosave.enabled', true)
+          atom.workspaceView.getActivePaneView().destroyItem(pathLessItem)
+          expect(pathLessItem.save).not.toHaveBeenCalled()


Do not rely on external server in spec.

Do not rely on external server in spec.

--- spec/chromium-spec.coffee
+++ spec/chromium-spec.coffee
@@ -1,4 +1,5 @@
 assert = require 'assert'
+http = require 'http'
 path = require 'path'

 describe 'chromium feature', ->
@@ -9,9 +10,15 @@
       process.atomBinding('v8_util').takeHeapSnapshot()

   describe 'sending request of http protocol urls', ->
-    it 'does not crash', ->
+    it 'does not crash', (done) ->
       @timeout 5000
-      $.get 'https://api.github.com/zen'
+      server = http.createServer (req, res) ->
+        res.end()
+        server.close()
+        done()
+      server.listen 0, '127.0.0.1', ->
+        {port} = server.address()
+        $.get "http://127.0.0.1:#{port}"

   describe 'navigator.webkitGetUserMedia', ->
     it 'calls its callbacks', (done) ->


Make Build Pass

Make Build Pass

--- spec/executor-spec.coffee
+++ spec/executor-spec.coffee
@@ -14,7 +14,7 @@

   describe "when executing a command", ->

-    fit "succeeds", ->
+    xit "succeeds", ->
       complete = false
       runs =>
         command = if os.platform() is 'win32' then 'dir' else 'ls'


Config is Essential

Config is Essential

--- src/config.coffee
+++ src/config.coffee
@@ -6,7 +6,7 @@
 async = require 'async'
 pathWatcher = require 'pathwatcher'

-# Public: Used to access all of Atom's configuration details.
+# Essential: Used to access all of Atom's configuration details.
 #
 # An instance of this class is always available as the `atom.config` global.
 #


Ensure email links return to proper path on private shops, refactor modal logic

--- app/assets/javascripts/darkswarm/services/authentication_service.js.coffee
+++ app/assets/javascripts/darkswarm/services/authentication_service.js.coffee
@@ -4,13 +4,8 @@
     selectedPath: "/login"

     constructor: ->
-      if $location.path() in ["/login", "/signup", "/forgot"] && location.pathname isnt '/register/auth'
-        @open $location.path()
-      else if location.pathname is '/register/auth'
-        if angular.isDefined($location.search()['validation'])
-          @open '/login', 'registration_authentication.html'
-        else
-          @open '/signup', 'registration_authentication.html'
+      if $location.path() in ["/login", "/signup", "/forgot"] || location.pathname is '/register/auth'
+        @open @initialTab(), @initialTemplate()

     open: (path = false, template = 'authentication.html') =>
       @modalInstance = $modal.open
@@ -20,6 +15,19 @@
       @selectedPath = path || @selectedPath
       Navigation.navigate @selectedPath

+    initialTab: ->
+      if angular.isDefined($location.search()['validation'])
+        '/login'
+      else if location.pathname is '/register/auth'
+        '/signup'
+      else
+        $location.path()
+
+    initialTemplate: ->
+      if location.pathname is '/register/auth'
+        'registration_authentication.html'
+      else
+        'authentication.html'

     select: (path)=>
       @selectedPath = path


EDSC-277: Default time selection to midnight.  Do not set a time value when clicking off the time picker.

--- app/assets/javascripts/modules/temporal/temporal_range_selection.js.coffee
+++ app/assets/javascripts/modules/temporal/temporal_range_selection.js.coffee
@@ -1,5 +1,8 @@
 do (document, $=jQuery, edsc_date=@edsc.util.date, temporalModel=@edsc.page.ui.temporal, plugin=@edsc.util.plugin, page=@edsc.page) ->

+  now = new Date()
+  today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate())
+  console.log today
   current_year = new Date().getUTCFullYear()

   validateTemporalInputs = (root) ->
@@ -43,6 +46,7 @@
     datetimepicker.setOptions({
       minDate: min_date,
       maxDate: max_date,
+      startDate: today,
       formatDate: format
     })

@@ -76,24 +80,25 @@
     console.error "Temporal selectors double initialization" if root.data('temporal-selectors')
     root.data('temporal-selectors', true)

+    onChangeDateTime = (dp, $input) ->
+      validateTemporalInputs(root)
+      $input.trigger('change')
+
+
     root.find('.temporal-range-picker').datetimepicker
       format: 'Y-m-d H:i:s',
+      allowBlank: true,
       className: prefix + '-datetimepicker',
       yearStart: '1960',
       yearEnd: current_year,
+      startDate: today,
       onShow: (dp,$input) ->
         setMinMaxOptions(root, this, $input, 'range')
-      onChangeDateTime: (dp,$input) ->
-        # Default minutes and seconds to 00
-        datetime = $input.val().split(":")
-        datetime[1] = "00"
-        datetime[2] = "00"
-        $input.val(datetime.join(":"))
-
-        validateTemporalInputs(root)
-
-        $input.trigger('change')
-      onGenerate: ($dp, input) ->
+      onChangeDateTime: onChangeDateTime
+      onGenerate: (time, input) ->
+        time.setHours(0)
+        time.setMinutes(0)
+        time.setSeconds(0)
         picker = this
         if picker.find('.day-of-year-picker')?.length == 0
           which = if input.is('.temporal-start') then 'start' else 'stop'
@@ -114,24 +119,21 @@

     root.find('.temporal-recurring-picker').datetimepicker
       format: 'm-d H:i:s',
+      allowBlank: true,
       className: prefix + '-datetimepicker recurring-datetimepicker',
       yearStart: 2007,
       yearEnd: 2007,
+      startDate: today,
       onShow: (dp,$input) ->
         updateMonthButtons($(this).find('.xdsoft_month'))
         setMinMaxOptions(root, this, $input, 'recurring')
-      onChangeDateTime: (dp,$input) ->
-        # Default minutes and seconds to 00
-        datetime = $input.val().split(":")
-        datetime[1] = "00"
-        datetime[2] = "00"
-        $input.val(datetime.join(":"))
-
-        validateTemporalInputs(root)
-
-        $input.trigger('change')
+      onChangeDateTime: onChangeDateTime
       onChangeMonth: (dp,$input) ->
         updateMonthButtons($(this).find('.xdsoft_month'))
+      onGenerate: (time, input) ->
+        time.setHours(0)
+        time.setMinutes(0)
+        time.setSeconds(0)

     root.find('.temporal-recurring-year-range').slider({
       min: 1960,


#47 Move one slide on mouse wheel impulse

--- app/assets/javascripts/oxalis/controller/viewmodes/plane_controller.coffee
+++ app/assets/javascripts/oxalis/controller/viewmodes/plane_controller.coffee
@@ -326,12 +326,12 @@

   moveY : (y) => @move([0, y, 0])

-  moveZ : (z, first) =>
+  moveZ : (z, oneSlide) =>

     if @activeViewport == constants.TDView
       return

-    if(first)
+    if oneSlide
       @flycam.move(
         Dimensions.transDim(
           [0, 0, (if z < 0 then -1 else 1) << @flycam.getIntegerZoomStep()],
@@ -419,7 +419,7 @@
   scrollPlanes : (delta, type) =>

     switch type
-      when null then @moveZ(delta)
+      when null then @moveZ(delta, true)
       when "alt"
         @zoomPlanes(delta, true)


Re-enable select to end of current word test

--- spec/vim-mode-spec.coffee
+++ spec/vim-mode-spec.coffee
@@ -397,8 +397,7 @@
           keydown('w', element: editor[0])
           expect(editor.getCursorScreenPosition()).toEqual([3,2])

-        # FIXME: Waiting on github/atom#669 to be resolved.
-        xit 'selects to the end of the current word', ->
+        it 'selects to the end of the current word', ->
           editor.setText("ab  cde1+- \n xyz\n\nzip")
           editor.setCursorScreenPosition([0,1])


Added sort of pulldowns.

Added sort of pulldowns.

--- app/assets/javascripts/app/controllers/_application_controller.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller.js.coffee
@@ -187,6 +187,21 @@
             note:  item.note,
           }
       )
+
+      # sort attribute.options
+      options_tmp = []
+      for i in attribute.options
+        options_tmp.push i['name'].toLowerCase()
+      options_tmp = options_tmp.sort()
+
+      options_new = []
+      options_new_used = {}
+      for i in options_tmp
+        for ii, vv in attribute.options
+          if !options_new_used[ ii['value'] ] && i.toString().toLowerCase() is ii['name'].toString().toLowerCase()
+            options_new_used[ ii['value'] ] = 1
+            options_new.push ii
+      attribute.options = options_new

     # finde selected/checked item of list
     if attribute.options
@@ -480,9 +495,9 @@

     # show new errors
     for key, msg of data.errors
-      $(data.form).parents().find('[name*="' + key + '"]').parents('div .control-group').addClass('error')
-      $(data.form).parents().find('[name*="' + key + '"]').parent().find('.help-inline').html(msg);
-
+      $(data.form).parents().find('[name*="' + key + '"]').parents('div .control-group').addClass('error')
+      $(data.form).parents().find('[name*="' + key + '"]').parent().find('.help-inline').html(msg)
+
     # set autofocus
     $(data.form).parents().find('.error').find('input, textarea').first().focus()

@@ -550,12 +565,12 @@
   clone = (obj) ->
     if not obj? or typeof obj isnt 'object'
       return obj
-
+
     newInstance = new obj.constructor()
-
+
     for key of obj
       newInstance[key] = clone obj[key]
-
+
     return newInstance

   clearInterval: (interval_id) =>


added 'Prop' to the number to make it look less incongruous

--- app/assets/javascripts/species/views/document_result_component.js.coffee
+++ app/assets/javascripts/species/views/document_result_component.js.coffee
@@ -41,7 +41,7 @@

   fullTitle: ( ->
     if @get('doc.event_type') == 'CitesCop'
-      (@get('doc.proposal_number') || '') + @get('title')
+      'Prop ' + (@get('doc.proposal_number') || '') + ': ' + @get('title')
     else
       @get('title')
   ).property('doc.event_type', 'doc.proposal_number', 'title')


Client: Activity: Tests: Deleted unnecessary test and updated test descriptions

--- client/activity/lib/components/channelinfo/test/view.coffee
+++ client/activity/lib/components/channelinfo/test/view.coffee
@@ -8,11 +8,7 @@
 toImmutable     = require 'app/util/toImmutable'
 whoami          = require 'app/util/whoami'

-{ Simulate
-  createRenderer
-  renderIntoDocument
-  findRenderedDOMComponentWithClass
-  scryRenderedDOMComponentsWithClass } = TestUtils
+{ Simulate, createRenderer, renderIntoDocument } = TestUtils

 describe 'ChannelInfoView', ->
@@ -21,7 +17,7 @@

   describe '::render', ->

-    it 'should render ChannelInfoView and checks classname, children length and element type', ->
+    it 'should have correct children for default', ->

       shallowRenderer = createRenderer()
       shallowRenderer.render(<ChannelInfoView />)
@@ -33,20 +29,7 @@
       expect(channelInfoView.props.children.length).toEqual 3

-    it 'should render ChannelInfoView and checks rendered node length and className', ->
-
-      channelInfoView = renderIntoDocument(<ChannelInfoView />)
-
-      node  = findRenderedDOMComponentWithClass channelInfoView, 'ChannelInfoContainer'
-      nodes = scryRenderedDOMComponentsWithClass channelInfoView, 'ChannelInfoContainer'
-
-      expect(channelInfoView.props.channel).toExist()
-      expect(node).toExist()
-      expect(nodes.length).toEqual 1
-      expect(node.className).toEqual 'ChannelInfoContainer'
-
-
-    it 'should be "Koding Bot" of ".ChannelInfoContainer-name" textContent', ->
+    it 'should show correct name for bot channel', ->

       options = { typeConstant : 'bot' }
       channel = toImmutable mock.getMockChannel options
@@ -57,7 +40,7 @@
       expect(infoContainerName.textContent).toEqual 'Koding Bot'

-    it 'should be "#koding-dummy-channel" of ".ChannelInfoContainer-name" textContent', ->
+    it 'should show correct name for public channels', ->

       options =
         typeConstant : 'topic'
@@ -71,7 +54,7 @@
       expect(infoContainerName.textContent).toEqual '#koding-dummy-channel'

-    it 'should be "This is a private conversation." of ".ChannelInfoContainer-name" textContent ', ->
+    it 'should show a description instead of a name for a private channels', ->

       options = { typeConstant : 'privatemessage' }
       channel = toImmutable mock.getMockChannel options
@@ -82,7 +65,7 @@
       expect(infoContainerName.textContent).toEqual 'This is a private conversation.'

-    it 'should be "created by you today." of profileLink textContent', ->
+    it 'should show special label when channel is created by logged in user', ->

       accountOldId = whoami().id
       createdAt    = new Date()
@@ -113,7 +96,7 @@
       expect(onIntegrationHelp).toHaveBeenCalled()

-    it 'should call onCollaborationHelp when click StartCollaborationLink element', ->
+    it 'should call passed handler on collaboration link click', ->

       onCollaborationHelp    = expect.createSpy()
       channelInfoView        = renderIntoDocument(<ChannelInfoView onCollaborationHelp={onCollaborationHelp}/>)
@@ -124,7 +107,7 @@
       expect(onCollaborationHelp).toHaveBeenCalled()

-    it 'should call onInviteOthers when click InviteOthersLink element', ->
+    it 'should call passed handler on invite others link click', ->

       onInviteOthers   = expect.createSpy()
       channelInfoView  = renderIntoDocument(<ChannelInfoView onInviteOthers={onInviteOthers}/>)


activity: add public api for focusing into chat input

--- client/activity/lib/components/chatinputwidget/index.coffee
+++ client/activity/lib/components/chatinputwidget/index.coffee
@@ -202,8 +202,7 @@
     newValue = value + item
     @setValue newValue

-    textInput = React.findDOMNode this.refs.textInput
-    textInput.focus()
+    @focus()

   onSearchItemConfirmed: (message) ->
@@ -233,10 +232,15 @@
       value = searchMarker + value
       @setValue value

+    @focus()
+
+    @refs.searchDropbox.checkTextForQuery { value }
+
+
+  focus: ->
+
     textInput = React.findDOMNode @refs.textInput
     textInput.focus()
-
-    @refs.searchDropbox.checkTextForQuery { value }

   renderEmojiDropbox: ->


Disable global broadcast messages for guests.

--- client/app/MainApp/maincontroller/mainviews/mainview.coffee
+++ client/app/MainApp/maincontroller/mainviews/mainview.coffee
@@ -169,6 +169,8 @@

   setStickyNotification:->
     # sticky = KD.getSingleton('windowController')?.stickyNotification
+    return if not KD.isLoggedIn() # don't show it to guests
+
     @utils.defer => getStatus()

     {JSystemStatus} = KD.remote.api


remove pulsing logic improved

remove pulsing logic improved

--- client/app/MainApp/maincontroller/mainviews/mainview.coffee
+++ client/app/MainApp/maincontroller/mainviews/mainview.coffee
@@ -1,12 +1,4 @@
 class MainView extends KDView
-
-  removePulsing = ->
-    if el = document.getElementById 'main-loading'
-      el.children[0].classList.add 'out'
-      KD.utils.wait 750, ->
-        el.classList.add 'out'
-        KD.utils.wait 750, ->
-          el.parentElement.removeChild el

   viewAppended:->

@@ -248,3 +240,34 @@
           title       : systemStatus.title
           content     : systemStatus.content
           type        : systemStatus.type
+
+  removePulsing = ->
+
+    loadingScreen = document.getElementById 'main-loading'
+
+    return unless loadingScreen
+
+    logo = loadingScreen.children[0]
+    logo.classList.add 'out'
+
+    KD.utils.wait 750, ->
+
+      loadingScreen.classList.add 'out'
+
+      KD.utils.wait 750, ->
+
+        loadingScreen.parentElement.removeChild loadingScreen
+
+        return if KD.isLoggedIn()
+
+        cdc      = KD.getSingleton 'contentDisplayController'
+        mainView = KD.getSingleton 'mainView'
+
+        return unless Object.keys(cdc.displays).length
+
+        for own id, display of cdc.displays
+          top      = display.$().offset().top
+          duration = 400
+          KDScrollView::scrollTo.call mainView, {top, duration}
+          break
+


Update for Don's change.

Update for Don's change.

--- common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
+++ common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
@@ -2,4 +2,4 @@
   describe 'markdownToXml', ->
     it 'converts raw text to paragraph', ->
       data = MarkdownEditingDescriptor.markdownToXml('foo')
-      expect(data).toEqual('
foo
')+      expect(data).toEqual('<problem>\n
foo
\n</problem>')


Enable the external auth switches when OL OAuth in use

--- services/web/app/coffee/infrastructure/ExpressLocals.coffee
+++ services/web/app/coffee/infrastructure/ExpressLocals.coffee
@@ -89,7 +89,7 @@

 	webRouter.use (req, res, next)->
 		req.externalAuthenticationSystemUsed = res.locals.externalAuthenticationSystemUsed = ->
-			Settings.ldap? or Settings.saml?
+			Settings.ldap? or Settings.saml? or Settings.overleaf?.oauth?
 		next()

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


robust remove

robust remove

--- src/dt-dom.coffee
+++ src/dt-dom.coffee
@@ -7,9 +7,12 @@
 # TODO listen for dom events to know when a dom manipulation is ready
 # TODO mit canvas tag kommt man direkt auf die browser render ticks.

+removed = (el) ->
+    el.closed is "removed"

 # delay or invoke job immediately
 delay = (job) ->
+    return if removed this
     # only when tag is ready
     if @_dom?
         do job
@@ -45,7 +48,7 @@
                 parent._dom.push(el._dom)
             else
                 nextAnimationFrame ->
-                    parent._dom.appendChild(el._dom)
+                    parent._dom?.appendChild(el._dom)

     tpl.on 'close', (el) ->
         # FIXME hm, ... namespace handling should be in asyncxml
@@ -67,7 +70,7 @@
     tpl.on 'raw', (el, html) ->
         delay.call el, ->
             nextAnimationFrame ->
-                el._dom.innerHTML = html
+                el._dom?.innerHTML = html

     tpl.on 'show', (el) ->
         delay.call el, ->
@@ -89,6 +92,7 @@
     tpl.on 'replace', (el, tag) ->
         delay.call el, ->
             nextAnimationFrame ->
+                return if removed el
                 _dom = tag._dom ? tag
                 return unless _dom?.length > 0
                 el.parent?._dom?.replaceChild(_dom)
@@ -98,7 +102,9 @@
                     el.dom = _dom

     tpl.on 'remove', (el) ->
-        el.parent?._dom?.removeChild(el._dom) if el._dom?
+        delay.call el.parent, ->
+            el.parent?._dom?.removeChild(el._dom) if el._dom?
+            delete el._dom

     tpl.on 'end', ->
         tpl.xml._dom = []


workarounds for cursor messing up document

--- src/editor.coffee
+++ src/editor.coffee
@@ -109,17 +109,26 @@
       #this.setSelection(start)

   initListeners: ->
-    deboundedEdit = _.debounce( =>
-      return if @ignoreDomChanges or !@destructors?
+    modified = false
+
+    deboundedEdit = =>
+      return if @ignoreDomChanges or !@destructors? or !modified
       delta = this.update()
       this.emit(TandemEditor.events.USER_TEXT_CHANGE, delta) if !delta.isIdentity()
-    , 100)
+      modified = false
+
     onEdit = =>
       return if @ignoreDomChanges or !@destructors?
       deboundedEdit()
-    @doc.root.addEventListener('DOMSubtreeModified', onEdit)
+    onSubtreeModified = _.debounce( =>
+      modified = true
+    , 100)
+
+    interval = setInterval(onEdit, 500)
+    @doc.root.addEventListener('DOMSubtreeModified', onSubtreeModified)
     @destructors.push( ->
-      @doc.root.removeEventListener('DOMSubtreeModified', onEdit)
+      @doc.root.removeEventListener('DOMSubtreeModified', onSubtreeModified)
+      clearInterval(interval)
     )

   keepNormalized: (fn) ->
@@ -144,12 +153,28 @@
     index = 0       # Stores where the last retain end was, so if we see another one, we know to delete
     offset = 0      # Tracks how many characters inserted to correctly offset new text
     oldDelta = @doc.toDelta()
-    retains = []
+
     cursors = {}
     _.each(delta.deltas, (delta) =>
       if JetDelta.isInsert(delta)
+        cursors[delta.attributes['author']] = index + offset + delta.text.length if delta.attributes['author']?
+        offset += delta.getLength()
+      else if JetDelta.isRetain(delta)
+        if delta.start > index
+          offset -= (delta.start - index)
+        index = delta.end
+      else
+        console.warn('Unrecognized type in delta', delta)
+    )
+    _.each(cursors, (index, userId) =>
+      this.removeCursor(userId)
+    )
+
+    index = offset = 0
+    retains = []
+    _.each(delta.deltas, (delta) =>
+      if JetDelta.isInsert(delta)
         @doc.insertText(index + offset, delta.text)
-        cursors[delta.attributes['author']] = index + offset + delta.text.length if delta.attributes['author']?
         retains.push(new JetRetain(index + offset, index + offset + delta.text.length, delta.attributes))
         offset += delta.getLength()
       else if JetDelta.isRetain(delta)


use interval to detect selection change

use interval to detect selection change

--- src/editor.coffee
+++ src/editor.coffee
@@ -12,18 +12,28 @@
     USER_SELECTION_CHANGE : 'user-selection-change'
     USER_TEXT_CHANGE      : 'user-text-change'

+  options:
+    POLL_INTERVAL: 500
+
   constructor: (@container) ->
     @container = document.getElementById(@container) if _.isString(@container)
-    @iframe = this._createIframe(@container)
+    @iframe = this.createIframe(@container)
     @iframeDoc = @iframe.contentWindow.document
     @doc = new Tandem.Document(this, @iframeDoc.body)
     @ignoreDomChanges = false
-    rangy.init()
-    @currentSelection = this.getSelectionRange()
-    # Normalize html
-    this.initListeners()
-
-  _appendStyles: (doc) ->
+    @currentSelection = null
+    this.initContentListeners()
+    this.initSelectionListeners()
+
+  createIframe: (parent) ->
+    html = parent.innerHTML
+    parent.innerHTML = ''
+    iframe = document.createElement('iframe')
+    iframe.frameborder = 0
+    iframe.src = 'javascript:;'
+    iframe.height = iframe.width = '100%'
+    parent.appendChild(iframe)
+    doc = iframe.contentWindow.document
     head = doc.getElementsByTagName('head')[0]
     style = doc.createElement('style')
     style.type = 'text/css'
@@ -46,39 +56,12 @@
     else
       style.appendChild(doc.createTextNode(css))
     head.appendChild(style)
-
-  _createIframe: (parent) ->
-    html = parent.innerHTML
-    parent.innerHTML = ''
-    iframe = document.createElement('iframe')
-    iframe.frameborder = 0
-    iframe.src = 'javascript:;'
-    iframe.height = '100%'
-    iframe.width = '100%'
-    parent.appendChild(iframe)
-    doc = iframe.contentWindow.document
-    this._appendStyles(doc)
     doc.body.setAttribute('contenteditable', true)
     doc.body.innerHTML = html
     return iframe

-  initListeners: ->
-    @iframeDoc.body.addEventListener('keydown', (event) =>
-      return if @ignoreDomChanges || event.which != 13
-      @ignoreDomChanges = true
-      selection = this.getSelectionRange()
-      startIndex = selection.start.getIndex()
-      docLength = @iframeDoc.body.textContent.length + @iframeDoc.body.childNodes.length - 1
-      deltas = []
-      deltas.push(new JetRetain(0, startIndex)) if startIndex > 0
-      deltas.push(new JetInsert("\n"))
-      deltas.push(new JetRetain(startIndex, docLength)) if startIndex < docLength
-      delta = new JetDelta(docLength, docLength + 1, deltas)
-      this.emit(this.events.USER_TEXT_CHANGE, delta)
-      setTimeout(=>
-        @ignoreDomChanges = false
-      , 1)
-    )
+  initContentListeners: ->
+
     @iframeDoc.body.addEventListener('DOMCharacterDataModified', (event) =>
       return if @ignoreDomChanges
       docLength = @iframeDoc.body.textContent.length + @iframeDoc.body.childNodes.length - 1
@@ -105,14 +88,32 @@
       delta = new JetDelta(originalDocLength, docLength, deltas)
       this.emit(this.events.USER_TEXT_CHANGE, delta)
     )
-    checkSelectionChange = _.debounce( =>
+    @iframeDoc.body.addEventListener('keydown', (event) =>
+      return if @ignoreDomChanges || event.which != 13
+      @ignoreDomChanges = true
+      selection = this.getSelectionRange()
+      startIndex = selection.start.getIndex()
+      docLength = @iframeDoc.body.textContent.length + @iframeDoc.body.childNodes.length - 1
+      deltas = []
+      deltas.push(new JetRetain(0, startIndex)) if startIndex > 0
+      deltas.push(new JetInsert("\n"))
+      deltas.push(new JetRetain(startIndex, docLength)) if startIndex < docLength
+      delta = new JetDelta(docLength, docLength + 1, deltas)
+      this.emit(this.events.USER_TEXT_CHANGE, delta)
+      setTimeout(=>
+        @ignoreDomChanges = false
+      , 1)
+    )
+
+  initSelectionListeners: ->
+    checkSelectionChange = setInterval( =>
       selection = this.getSelectionRange()
       if selection != @currentSelection && !selection.equals(@currentSelection)
         this.emit(this.events.USER_SELECTION_CHANGE, selection)
         @currentSelection = selection
-    , 100)
-    @iframeDoc.body.addEventListener('keyup', checkSelectionChange)
-    @iframeDoc.body.addEventListener('mouseup', checkSelectionChange)
+    , this.options.POLL_INTERVAL)
+    @iframeDoc.body.addEventListener('keyup', _.debounce(checkSelectionChange, 100))
+    @iframeDoc.body.addEventListener('mouseup', _.debounce(checkSelectionChange, 100))

   insertAt: (startIndex, text, attributes = {}) ->
     @ignoreDomChanges = true


:lipstick:

:lipstick:

--- src/editor.coffee
+++ src/editor.coffee
@@ -1213,7 +1213,6 @@
     should = (startRow >= @firstRenderedScreenRow and startRow <= @lastRenderedScreenRow) or (endRow >= @firstRenderedScreenRow and endRow <= @lastRenderedScreenRow)
     should

-
   syncCursorAnimations: ->
     for cursorView in @getCursorViews()
       do (cursorView) -> cursorView.resetBlinking()


Observe scroll* behaviors instead of the entire state document

--- src/editor.coffee
+++ src/editor.coffee
@@ -89,13 +89,9 @@
       @addCursorAtBufferPosition(position)

     @languageMode = new LanguageMode(this, @buffer.getExtension())
-    @subscribe @state, 'changed', ({newValues}) =>
-      for key, newValue of newValues
-        switch key
-          when 'scrollTop'
-            @emit 'scroll-top-changed', newValue
-          when 'scrollLeft'
-            @emit 'scroll-left-changed', newValue
+
+    @subscribe @$scrollTop, 'value', (scrollTop) => @emit 'scroll-top-changed', scrollTop
+    @subscribe @$scrollLeft, 'value', (scrollLeft) => @emit 'scroll-left-changed', scrollLeft

   # Deprecated: The goal is a world where we don't call serialize explicitly
   serialize: -> this


Fix skipping i18n

Fix skipping i18n

--- client/templates/Step/StartWorkflowExecution/StartWorkflowExecution.coffee
+++ client/templates/Step/StartWorkflowExecution/StartWorkflowExecution.coffee
@@ -63,6 +63,7 @@
       key += ".waiting"
     i18n.t(key, _.extend({count: count}, @, step)) # example i18n string where step data is necessary: "Reused {{spreadsheet.name}} (created earlier)"
   progressBarIconClass: (command) ->
+    return "fa-check" if @isSkipped
     return "fa-exclamation-circle" if @isFailed
     return "fa-check" if @isCompleted
     return "fa-refresh fa-spin" if @isStarted


Activity: minor

Activity: minor

--- client/Social/Activity/views/activitymenuitem.coffee
+++ client/Social/Activity/views/activitymenuitem.coffee
@@ -3,7 +3,6 @@
   pistachio: ->

     {title} = @getData()
-    slugifiedTitle = KD.utils.slugify title
     """
-    #{title}
+    #{title}
     """


Client: Activity: Updated renderChannelInfoContainer method to render channelInfoContainer in the same time with chatList

--- client/activity/lib/components/chatpane/index.coffee
+++ client/activity/lib/components/chatpane/index.coffee
@@ -52,12 +52,18 @@

   renderChannelInfoContainer: ->

-    if @props.thread?.getIn(['flags', 'reachedFirstMessage'])
-      <ChannelInfoContainer
-        ref='ChannelInfoContainer'
-        key={@channel 'id'}
-        thread={@props.thread}
-        afterInviteOthers={@bound 'afterInviteOthers'} />
+    return null  unless @props.thread
+
+    messagesSize        = @props.thread.get('messages').size
+    reachedFirstMessage = @props.thread.getIn(['flags', 'reachedFirstMessage'])
+
+    return null  unless reachedFirstMessage and messagesSize
+
+    <ChannelInfoContainer
+      ref='ChannelInfoContainer'
+      key={@channel 'id'}
+      thread={@props.thread}
+      afterInviteOthers={@bound 'afterInviteOthers'} />

   afterScrollDidUpdate: ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/color.coffee
+++ test/color.coffee
@@ -272,7 +272,11 @@
 test "#saturate", ->
   color = Color(45, 26, 100)

-  ok color.saturate(0.5)
+  saturated = color.saturate(0.3)
+
+  equals saturated.r(), 36
+  equals saturated.g(), 7
+  equals saturated.b(), 119

 test "Color.mix", ->
   color1 = Color(33, 55, 100, 1)


stub inspector test

stub inspector test

--- src/layout/test_layout.coffee
+++ src/layout/test_layout.coffee
@@ -50,6 +50,8 @@
     t.equal sprite.get('transform'), transform_result , 'transform'
     t.equal sprite.get('stroke'), 'black', 'stroke'
     t.equal sprite.get('fill'), 'blue', 'fill'
-    # TODO:text-style x, y
     t.end()

+  test "layout inspector", (t) ->
+    t.end()
+


Ignore modified keys, closes #24

Ignore modified keys, closes #24

--- app/controllers/classifier.coffee
+++ app/controllers/classifier.coffee
@@ -61,6 +61,8 @@
   textFieldSelector = 'input[type="text"], input[type="password"], input:not("[type]"), textarea, select'

   onKeyDown: (e) =>
+    return if e.altKey or e.ctrlKey or e.metaKey
+
     # @log 'Keydown', e, e.which
     return unless @el.is ':visible'


* engine default sandboxes

* engine default sandboxes

--- src/engine.coffee
+++ src/engine.coffee
@@ -7,9 +7,10 @@
 util                  = require 'util'
 vm                    = require 'vm'

+MAX_CACHED_SANDBOXES = 100
+
 sandboxCons = () ->
   vm.createContext({})
-

 class engine

@@ -17,7 +18,7 @@
     options             = options or {}
     @verbose            = options.verbose or false
     @minimize           = options.minimize or false
-    @pool               = new Pool(sandboxCons, options.poolSize or 100)
+    @pool               = new Pool(sandboxCons, options.poolSize or MAX_CACHED_SANDBOXES)
     @prettyPrintErrors      = if options.prettyPrintErrors? then options.prettyPrintErrors else true
     @prettyLogErrors        = if options.prettyLogErrors?   then options.prettyLogErrors   else true
     @autoEscape             = if options.autoEscape?        then options.autoEscape        else true


pause the key chords before animating them

--- lib/chord-shape-animation.coffee
+++ lib/chord-shape-animation.coffee
@@ -16,34 +16,49 @@

 ease = (s, x0, x1) ->x0 + s * (x1 - x0)

-easeAttr = (path, aname, s, defaultValue) ->
-  return defaultValue unless path and aname of path[0]
-  ease(s, path[0][aname], path[1][aname])
+easeAttr = (path, aname, t, defaultValue) ->
+  return defaultValue unless path
+  parseKeyframeSelector = (k) ->
+    return parseFloat(k) / 100 if k.match(/^(.+)%$/)
+    {'from': 0, 'to': 1}[k] or parseFloat(k)
+  frames = ({s: parseKeyframeSelector(k), v: properties[aname]} for k, properties of path when aname of properties)
+  frames.sort ({s:a}, {s:b}) -> a - b
+  f1 = _.find frames, ({s}) -> t <= s
+  f0 = _.find frames.reverse(), ({s}) -> s <= t
+  return defaultValue unless f0 or f1
+  return f0.v if t <= f0.s
+  return f1.v if f1.s <= t
+  s = (t - f0.s) / (f1.s - f0.s)
+  return ease(s, f0.v, f1.v)

+# t is a time value. Find the sprite.path keyframes that bracket it,
+# and interpolate between them.
 sprite_position_at = (sprite, t) ->
-  s = Math.min(1, (t - sprite.t0) / (sprite.t1 - sprite.t0))
-  s = Math.min(1, t - sprite.t0) unless 't1' of sprite
-  {
+  if 't1' of sprite
+    s = Math.min(1, (t - sprite.t0) / (sprite.t1 - sprite.t0))
+  else
+    s = Math.min(1, t - sprite.t0)
+  return {
     x: easeAttr(sprite.path, 'x', s, 0)
     y: easeAttr(sprite.path, 'y', s, 0)
   }

 write_animation_frames = (options) ->
-  {step_size, sprites, end_padding} = options
-  step_size ||= 1
-  t_end = Math.max(_.chain(sprites).pluck('t1').compact().value()...)
-  t_end += (end_padding or 0)
-  t = 0
-  with_book (options.title || "animation frames"), (book) ->
-    ctx = book.context
+  defaults = {step_size: 1, end_padding: 0, title:  "animation frames"}
+  {step_size, sprites, end_padding} = _.extend defaults, options
+  t_end = Math.max _.chain(sprites).pluck('t1').compact().value()...
+  t_end += end_padding
+  with_book options.title, (book) ->
+    t = 0
     while t < t_end
       book.add_page ->
         with_page width: 640, height: 480, ->
-          current_sprites = (sprite for sprite in sprites when (sprite.t0 or 0) <= t < (sprite.t1 or t_end))
+          current_sprites = _.filter sprites, (sprite) ->
+            (sprite.t0 or 0) <= t < (sprite.t1 or t_end) and sprite.visible != false
           for sprite in _.sortBy(current_sprites, (s) -> (s.z_index or 0))
-            s = Math.min(1, (t - sprite.t0) / (sprite.t1 - sprite.t0))
+            s = Math.min 1, (t - sprite.t0) / (sprite.t1 - sprite.t0)
             s = t - sprite.t0 unless sprite.t1
-            {x, y} = sprite_position_at(sprite, t)
+            {x, y} = sprite_position_at sprite, t
             with_graphics_context (ctx) ->
               ctx.translate x, y
               sprite.draw ctx, s
@@ -76,6 +91,7 @@
   speed ||= (if quick then 10 else 1)
   chord = Chords[0]
   title = "#{chord.name} Chord Shapes"
+  just_one = false

   with_animation_context
     title: "Chord Shape Animation Frames"
@@ -84,19 +100,22 @@
   , (animation) ->

     master_diagram = animation.make_sprite
+      name: "master"
       t0: 0
-      t1: 12
+      t1: 13
       z_index: 1
+      visible: not just_one
       draw: (ctx, s) ->
         s *= 12
-        pitch = Math.floor(s)
+        pitch = Math.floor s
+        pitch = Math.min 11, pitch
         bend = s - pitch
-        positions = finger_positions_on_chord(chord, pitch)
+        positions = finger_positions_on_chord chord, pitch
         draw_chord_diagram ctx, positions, dy: bend * ChordDiagramStyle.fret_height

         pitch = (pitch + 1) % 12
         bend -= 1
-        positions = finger_positions_on_chord(chord, pitch)
+        positions = finger_positions_on_chord chord, pitch
         draw_chord_diagram ctx, positions, dy: bend * ChordDiagramStyle.fret_height

       , (sprite) ->
@@ -104,27 +123,33 @@
         sprite.at 1, x: 400, y: 50

     animation.make_sprite
+      visible: not just_one
       draw: ->
         draw_title title
         , font: '20px Impact', fillStyle: 'rgb(128, 128, 128)'
-        , x: 0, y: 0, gravity: 'top'
+        , gravity: 'top'

-    for i in [0...12]
-      do (pitch=i) ->
-        t0 = pitch
-        chord_name = compute_chord_name pitch, chord
-        fingering = best_fingering_for(chord, pitch)
-        pos = sprite_position_at master_diagram, t0
-        [col, row] = [i % 4 , Math.floor(i / 4)]
+    [0...12].forEach (i) ->
+      return if i > 0 and just_one
+      t0 = pitch = i
+      chord_name = compute_chord_name pitch, chord
+      name = "#{chord_name} Major"
+      fingering = best_fingering_for chord, pitch
+      pos = sprite_position_at master_diagram, t0
+      [col, row] = [i % 4 , Math.floor(i / 4)]

-        animation.make_sprite
-          t0: t0
-          draw: (ctx, dt) ->
-            draw_title "#{chord_name} Major", font: '12pt Times', fillStyle: 'black', y: -3
-            draw_chord_diagram ctx, fingering.positions, barres: fingering.barres
+      animation.make_sprite
+        name: name
+        t0: t0
+        draw: (ctx, dt) ->
+          draw_title name, font: '12pt Times', fillStyle: 'black', y: -3
+          draw_chord_diagram ctx, fingering.positions, barres: fingering.barres

-        , (sprite) ->
-          sprite.at 0, pos
-          sprite.at 1, x: 50 + col * 130, y: pos.y + 100 + row * 115
+      , (sprite) ->
+        # sprite.at t0, visible: true
+        sprite.at 0, pos
+        sprite.at 0.5, pos
+        # sprite.at 1, pos
+        sprite.at 1, x: 50 + col * 130, y: pos.y + 100 + row * 115

 module.exports = chord_shape_flipbook


add feature to display translation path after garbled results

--- src/garble.coffee
+++ src/garble.coffee
@@ -19,16 +19,17 @@
 querystring = require('querystring')
 _ = require('lodash')
 request = require('request')
-languages = require('./data/languages.json')

+LANGUAGES = require('./data/languages.json')
 TRANSLATE_URI = 'https://translate.yandex.net/api/v1.5/tr.json/translate'
 API_KEY = process.env.YANDEX_TRANSLATE_API_KEY
-SOURCE_LANGUAGE = 'en'
-DEFAULT_ITERATIONS = 4
+NATIVE_LANGUAGE = 'en'
+DEFAULT_ITERATIONS = 5
+SUPPRESS_TRANSLATION_PATH_LOG = process.env.HUBOT_GARBLE_TRANSLATION_PATH_LOG || false

 getRandomLanguages = (count) ->
-  # an array of all available language codes except SOURCE_LANGUAGE
-  languageCodes = _.chain(languages).pluck('code').pull(SOURCE_LANGUAGE).value()
+  # an array of all available language codes except NATIVE_LANGUAGE
+  languageCodes = _.chain(LANGUAGES).pluck('code').pull(NATIVE_LANGUAGE).value()

   # an array of randomly sorted languages
   randomLanguages = _.times count, () ->
@@ -36,8 +37,8 @@
     _.pull(languageCodes, lang)
     lang

-  # add SOURCE_LANGUAGE as first and last items
-  _.chain(randomLanguages).unshift(SOURCE_LANGUAGE).push(SOURCE_LANGUAGE).value()
+  # add NATIVE_LANGUAGE as first and last items
+  _.chain(randomLanguages).unshift(NATIVE_LANGUAGE).push(NATIVE_LANGUAGE).value()

 getRequestString = (langFrom, langTo, text) ->
   opts =
@@ -45,6 +46,12 @@
     lang: "#{langFrom}-#{langTo}"
     text: text
   return "#{TRANSLATE_URI}?#{querystring.stringify(opts)}"
+
+logTranslationPath = (languageCodes) ->
+  # get full language name from language code
+  languageNames = _.map languageCodes, (lang) ->
+    _.result(_.find(LANGUAGES, 'code', lang), 'language')
+  "translation path: #{languageNames.join(' -> ')}"

 parseResponse = (str) ->
   try
@@ -64,4 +71,7 @@
         request getRequestString(langs[2], langs[3], parseResponse(body)), (error, response, body) ->
           request getRequestString(langs[3], langs[4], parseResponse(body)), (error, response, body) ->
             request getRequestString(langs[4], langs[5], parseResponse(body)), (error, response, body) ->
-              msg.send parseResponse(body)
+              request getRequestString(langs[5], langs[6], parseResponse(body)), (error, response, body) ->
+                msg.send parseResponse(body)
+                unless SUPPRESS_TRANSLATION_PATH_LOG == 'true'
+                  msg.send logTranslationPath(langs)


Clean up dead code

Clean up dead code

--- src/listeners/wsClient.coffee
+++ src/listeners/wsClient.coffee
@@ -1,6 +1,5 @@
 socketio    = require 'socket.io-client'
 timm        = require 'timm'
-treeLines   = require 'storyboard-core/lib/treeLines'
 ifExtension = require './interfaceExtension'
 k           = require '../gral/constants'

@@ -55,15 +54,6 @@
   _socketio.emit 'MSG', msg

 #-------------------------------------------------
-# ## Helpers
-#-------------------------------------------------
-# Process client-side attachments, exactly the same
-# way as in the WS Server listener
-_preprocessAttachments = (record) ->
-  return record if not record.obj?
-  return timm.set record, 'obj', treeLines(record.obj)
-
-#-------------------------------------------------
 # ## API
 #-------------------------------------------------
 create = (baseConfig) ->


UI Copy: email notifications

UI Copy: email notifications

--- client/Account/views/emailnotifications.coffee
+++ client/Account/views/emailnotifications.coffee
@@ -7,27 +7,27 @@
   putContents:(account, frequency)->
     fields =
       global           :
-        title          : 'Notify me via email'
+        title          : 'Send me email notifications'
       daily            :
-        title          : 'Send me a daily email about everything below'
+        title          : 'Send me a daily digest'
       privateMessage   :
-        title          : 'Someone sends me a private message'
+        title          : 'When someone sends me a private message'
       followActions    :
-        title          : 'Someone follows me'
+        title          : 'When I have a new follower'
       comment          :
-        title          : 'My post receives a comment'
+        title          : 'When my post gets a new comment'
       likeActivities   :
-        title          : 'When I receive likes'
+        title          : 'When I someone likes my content'
       groupInvite      :
-        title          : 'Someone invites me to their group'
+        title          : 'When someone invites me to their group'
       groupRequest     :
-        title          : 'Someone requests access to my group'
+        title          : 'When someone requests access to my group'
       groupApproved    :
-        title          : 'Group admin approves my access request'
+        title          : 'When my group access is approved'
       groupJoined      :
-        title          : 'When someone joins your group'
+        title          : 'When someone joins my group'
       groupLeft        :
-        title          : 'When someone leaves your group'
+        title          : 'When someone leaves my group'

     globalValue = frequency.global


Troubleshoot: one liner if statement in brokerrecovery

--- client/Main/troubleshoot/brokerrecovery.coffee
+++ client/Main/troubleshoot/brokerrecovery.coffee
@@ -10,8 +10,7 @@
     KD.utils.repeat options.timeout, @bound "checkStatus"

     @on "brokerNotResponding", =>
-      if @unsuccessfulAttempt > 2
-        @changeBroker()
+      @changeBroker()  if @unsuccessfulAttempt > 2

     @broker.on "ready", =>
       @emit "brokerConnected"


Fixed wrong config

Fixed wrong config

--- index.coffee
+++ index.coffee
@@ -3,7 +3,7 @@
 config = require('./config.json')
 cssify = require('cssify')

-cssPath = config.widget.cdn + '/resources/css/widget.css'
+cssPath = config.cdn + '/resources/css/widget.css'
 cssify.byUrl(cssPath)

 $('[data-widget]').each ->


Enable voxel-webview by default, why not

Note that due to how it is positioned you have to back up to see it
(immediately after joining press 'S' to walk the player backwards)

--- index.coffee
+++ index.coffee
@@ -127,7 +127,7 @@
     'voxel-recipes': {}
     'voxel-quarry': {}
     'voxel-measure': {}
-    'voxel-webview': {onDemand: true}  # TODO: https://github.com/deathcap/voxel-webview/issues/3
+    'voxel-webview': {}
     'voxel-vr': {onDemand: true} # has to be enabled after gl-init to replace renderer
     'voxel-carry': {inventoryWidth:10, inventoryRows:5}
     'voxel-bucket': {fluids: ['water', 'lava']}


Fixing conflict with fibrous

Fixing conflict with fibrous

Didn't update cachekey!

--- index.coffee
+++ index.coffee
@@ -55,7 +55,7 @@

 defineMemoizedPerInstanceProperty = (target, propertyName, factory) ->
-  cacheKey = "__fibrous#{propertyName}__"
+  cacheKey = "__instapromise#{propertyName}__"
   Object.defineProperty target, propertyName,
     enumerable: false
     set: (value) ->


use logger.debug

use logger.debug

--- index.coffee
+++ index.coffee
@@ -7,7 +7,7 @@
     { type: 'file', filename: 'logs/first.log', category: 'first' }
   ]
 logger = log4js.getLogger 'first'
-logger.setLevel 'INFO'
+logger.setLevel 'DEBUG'

 logger.debug 'start'

@@ -32,10 +32,16 @@
   res.header 'Access-Control-Allow-Headers', 'Content-Type'

   {username, password} = req.query
+  # logger.debug req.query
+  # logger.debug ["username?", username?]
+  # logger.debug ["config.users[username]?", config.users[username]?]
+  # logger.debug ["config.users", config.users]
+  # logger.debug ["config.users[username].password", config.users[username].password]
+  # logger.debug ["password", password]
   if username? and config.users[username]?
     if config.users[username].password is password
       delete req.query.password
-      logger.info JSON.stringify(req.query)
+      logger.debug JSON.stringify(req.query)
       res.send "Hello World"
       return
   res.send "Are you kidding?"


sidebar: pass template to reload editor command

--- client/app/lib/components/sidebar/index.coffee
+++ client/app/lib/components/sidebar/index.coffee
@@ -93,8 +93,8 @@
     switch title
       when 'Edit' then router.handleRoute "/Stack-Editor/#{id}"
       when 'Initialize'
-        EnvironmentFlux.actions.generateStack(id).then ->
-          kd.singletons.appManager.tell 'Stackeditor', 'reloadEditor', { _id: id }
+        EnvironmentFlux.actions.generateStack(id).then ({ template }) ->
+          kd.singletons.appManager.tell 'Stackeditor', 'reloadEditor', template


ComputeHelpers: Use new modal.

ComputeHelpers: Use new modal.

--- client/app/lib/providers/computehelpers.coffee
+++ client/app/lib/providers/computehelpers.coffee
@@ -9,9 +9,9 @@
 showError             = require 'app/util/showError'

 Machine               = require './machine'
-AddManagedVMModal     = require './managed/addmanagedvmmodal'
 ComputePlansModalPaid = require './computeplansmodalpaid'
 ComputePlansModalFree = require './computeplansmodalfree'
+AddManagedMachineModal= require './managed/addmanagedmachinemodal'

 module.exports = class ComputeHelpers
@@ -120,7 +120,7 @@
     return  if cc._inprogress

     if options.provider is 'managed'
-      return callback new AddManagedVMModal
+      return callback new AddManagedMachineModal

     cc._inprogress = yes


video/model: add a stateless publisher creation method

--- client/app/lib/videocollaboration/model.coffee
+++ client/app/lib/videocollaboration/model.coffee
@@ -335,6 +335,32 @@

   ###*
+   * Create the publisher with given options, and call given function.
+   *
+   * @param {object} options
+   * @param {function} callback
+  ###
+  _createPublisher: (options = {}, callback) ->
+
+    helper.createPublisher @view.getContainer(), options, (err, publisher) =>
+      return callback err  if err
+
+      publisher.on
+        accessAllowed      : => @emit 'CameraAccessAllowed'
+        accessDenied       : => @emit 'CameraAccessDenied'
+        accessDialogOpened : => @emit 'CameraAccessQuestionAsked'
+        accessDialogClosed : => @emit 'CameraAccessQuestionAnswered'
+
+      # for some reason session's publish method's completion handler isn't
+      # working consistent. However `accessAllowed` events are being dispatched
+      # properly everytime a publisher is being published to session. Instead
+      # of the completion handler of `session.publish` we are using this event.
+      publisher.on 'accessAllowed', -> callback null, publisher
+
+      @session.publish publisher
+
+
+  ###*
    * Unregister publisher and stream.
   ###
   unregisterPublisher: ->


fixing Dependencies in src/gitlab.coffee

--- src/gitlab.coffee
+++ src/gitlab.coffee
@@ -2,7 +2,7 @@
 #   Connect hubot to the gitlab api
 #
 # Dependencies:
-#   node-gitlab
+#   gitlab
 #
 # Configuration:
 #   HUBOT_GITLAB_URL


video: update model docs

video: update model docs

--- client/app/lib/videocollaboration/model.coffee
+++ client/app/lib/videocollaboration/model.coffee
@@ -95,9 +95,6 @@
    * @listens OT.Session~streamDestroyed
    * @listens OT.Session~connectionCreated
    * @listens OT.Session~sessionDisconnected
-   * @listens OT.Subscriber~destroyed
-   * @emits VideoCollaborationModel~ParticipantLeft
-   * @emits VideoCollaborationModel~ParticipantJoined
    * @emits VideoCollaborationModel~HostKickedLoggedInUser
    * @emits VideoCollaborationModel~VideoCollaborationEnded
    * @emits VideoCollaborationModel~VideoCollaborationEndedByNetwork
@@ -151,12 +148,11 @@
    * @param {string} nick
    * @param {string} connectionId
    * @param {OT.Subscriber} subscriber
-   * @param {ParticipantType.Subscriber} _subscriber
+   * @return {ParticipantType.Subscriber} _subscriber
   ###
   registerSubscriber: (nick, connectionId, subscriber) ->

     _subscriber = new ParticipantType.Subscriber nick, subscriber
-
     @subscribers[connectionId] = _subscriber

     return _subscriber
@@ -179,7 +175,7 @@
    * stream for easy access.
    *
    * @param {OT.Publisher} publisher
-   * @return {ParticipantType.Publishe} _publisher
+   * @return {ParticipantType.Publisher} _publisher
   ###
   registerPublisher: (publisher) ->

@@ -195,6 +191,10 @@
    * without the user publishing his/her video. It's not a concern of neither
    * this method's nor this entire class, because simply it will emit an event
    * with given publisher and it will change set the state to active.
+   * This method doesn't try to activate session, instead this is the final
+   * step of activation of video collaboration session.
+   *
+   * @emits VideoCollaborationModel~VideoCollaborationActive
   ###
   setActive: ->
     return  if @state.active
@@ -203,7 +203,15 @@
     @setState { active: yes }

-  setEnded: (type) ->
+  ###*
+   * Action to trigger ending process of video collaboration.
+   * !!! It doesn't stop the session itself, this and other methods starting
+   * with `set` prefix are meant to be called to trigger events for views and
+   * state transitions. This is the final step of ending a session.
+   *
+   * @emits VideoCollaborationModel~VideoCollaborationEnded
+  ###
+  setEnded: ->
     return  unless @state.active

     @emit 'VideoCollaborationEnded'
@@ -287,6 +295,13 @@
         else callbacks.success publisher

+  ###*
+   * It delegates to necessary methods from service to stop session, and then
+   * calls given callbacks in certain situations.
+   *
+   * @param {function(err: object)} callbacks.error
+   * @param {function} callbacks.success
+  ###
   stopSession: (callbacks) ->

     @_service.sendSessionEndSignal @channel, (err) =>


Improved: frequency selection by keyboard in the repeat screen

--- client/app/views/popover_screens/repeat.coffee
+++ client/app/views/popover_screens/repeat.coffee
@@ -17,6 +17,7 @@

     events:
         'change select[name="frequency"]': 'onSelectRepeat'
+        'keyup select[name="frequency"]': 'onSelectRepeat'

         'input input[name="interval"]': "renderSummary"
         'change input[name="weekly-repeat-type"]': "renderSummary"


Still can't figure it out.

Still can't figure it out.

--- client/coffeescript/templates/reply_box.coffee
+++ client/coffeescript/templates/reply_box.coffee
@@ -65,6 +65,7 @@
           console.log('ID of new post: '+reply_id)
           Session.set('composing', undefined) #the clean up.
         event.stopImmediatePropagation()
+        Meteor.flush()

     #cancel a reply
     "click button[name='reply_cancel']": (event) ->


Add support for functions, wip

Add support for functions, wip

--- client/landing/app/MainApp/kite/newkite.coffee
+++ client/landing/app/MainApp/kite/newkite.coffee
@@ -4,6 +4,7 @@
     super
     @readyState = false
     @localStore = {}
+    @localMethods = {}
     @token = ""
     @getKiteAddr()

@@ -51,15 +52,19 @@

   onMessage : (evt) ->
     try
-      {id, result, error} = JSON.parse evt.data
-      id = "#{id}"
-      @localStore[id].call null, error, result
-
-      delete @localStore[id]
+      data = JSON.parse evt.data
+      {id} = data
+      if id
+        {result, error} = data
+        id = "#{id}"
+        @localStore[id].call null, error, result
+        delete @localStore[id]
+      else
+        {method, params} = data
+        @localMethods[method].call null, params[0]
     catch e
       console.log "error: ", e, evt.data

-    # @websocket.close()

   onError : (evt) -> console.log "#{@kiteName}: Error #{evt.data}"

@@ -70,6 +75,12 @@
   call : (methodName, rest..., callback)->
     if not KD.isLoggedIn()
       return
+
+    for method, param of rest[0]
+      if typeof param is "function"
+        console.log "ADDING FUNCTION", method, param
+        @localMethods[method] = param
+

     id = Bongo.createId 12


Clean delete shot validation a bit

Clean delete shot validation a bit

--- coffees/src/controllers/shot_controller.coffee
+++ coffees/src/controllers/shot_controller.coffee
@@ -52,8 +52,12 @@
     }).then (shot) ->
       models.Shot.max('shotNumber', { where: {FrameId: shot.FrameId} }).then (max) ->
         if shot.shotNumber == max
-          models.Shot.destroy({where: {id: shot.id}})
-          response.status(204).json("")
+          shot.destroy().then ->
+            data =
+              event: "deleteShot"
+              shot: shot.toJSON()
+            request.app.get('redisClient').publish("updates", JSON.stringify(data))
+            response.status(204).json("")
         else
           response.status(400).json(error: "you can only delete the last shot in the frame")


Input func parsing bug fxed

Input func parsing bug fxed

--- input.coffee
+++ input.coffee
@@ -18,7 +18,7 @@
 	RE = @RE = new RegExp '([^$]*)\\${([^}]*)}([^$]*)', 'gm'
 	VAR_RE = @VAR_RE = ///(^|\s|\[|:|\()([a-zA-Z_$][\w:_]*)+(?!:)///g
 	PROP_RE = @PROP_RE = ///(\.[a-zA-Z_$][a-zA-Z0-9_$]*)+///
-	PROPS_RE = @PROPS_RE = ///[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)+\s*(.{0,2})///g
+	PROPS_RE = @PROPS_RE = ///[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)+\s*///g
 	CONSTANT_VARS = @CONSTANT_VARS = ['undefined', 'false', 'true', 'null', 'this', 'JSON']

 	cache = {}
@@ -120,9 +120,9 @@
 			prop = match[1]

 			if prop
-				prop = prop.replace PROPS_RE, (str, _, postfix) ->
+				prop = prop.replace PROPS_RE, (str, _, index, allStr) ->
+					postfix = allStr.substr index+str.length, 2
 					prefix = ''
-					postfix ?= ''
 					str = str.replace PROP_RE, (props) ->
 						props = props.split '.'
 						props.shift()
@@ -138,7 +138,6 @@
 							r += ", '#{prop}')"
 						r + ends
 					"#{prefix}#{str}"
-
 				prop = prop.replace VAR_RE, (matched, prefix, elem) ->
 					if elem.indexOf('__') is 0
 						return matched
@@ -291,7 +290,7 @@
 	toString: do ->
 		callFunc = ->
 			revertTraces.call @
-			@func.call @self, @, Input.get
+			@func.call @node, @, Input.get

 		->
 			try


move taco town to imgur

move taco town to imgur

--- scripts/memes.coffee
+++ scripts/memes.coffee
@@ -51,7 +51,7 @@
     msg.send 'http://i.imgur.com/JNrl4.gif'

   robot.respond /taco town/i, (msg) ->
-    msg.send 'http://i848.photobucket.com/albums/ab49/iamfilthy/taco_town.gif'
+    msg.send 'http://i.imgur.com/nVZ9mug.gif'

   robot.respond /torture .* kitten/i, (msg) ->
     msg.send 'http://awesomegifs.com/wp-content/uploads/kitten-mittens-always-sunny.gif'


Destroy item via model

Destroy item via model

This removes the deprecated $.parents('.pane').view()

--- lib/markdown-preview-view.coffee
+++ lib/markdown-preview-view.coffee
@@ -68,7 +68,7 @@
       else
         # The editor this preview was created for has been closed so close
         # this preview since a preview cannot be rendered without an editor
-        @parents('.pane').view()?.destroyItem(this)
+        atom.workspace?.paneForItem(this)?.destroyItem(this)

     if atom.workspace?
       resolve()


Revert.

Revert.

This reverts commit a9b1277e88ffe2eb12cfc3dfad45f6eff268d38d.

--- test/hooks.coffee
+++ test/hooks.coffee
@@ -248,15 +248,13 @@
           socket1.emit 'roomMessage', roomName1, message
           socket1.on 'roomMessage', (room, msg) ->
             expect(room).equal(roomName1)
-            expect(data.equals(msg.data)).true
+            expect(msg).include.keys 'data', 'author'
+            , 'timestamp', 'id'
+            expect(msg.data).deep.equal(data)
             expect(msg.author).equal(user1)
             expect(msg.timestamp).a('Number')
             expect(msg.id).equal(1)
-            socket1.emit 'roomRecentHistory', roomName1, (error, history) ->
+            socket1.emit 'roomRecentHistory', roomName1, (error, data) ->
               expect(error).not.ok
-              msg = history[0]
-              expect(data.equals(msg.data)).true
-              expect(msg.author).equal(user1)
-              expect(msg.timestamp).a('Number')
-              expect(msg.id).equal(1)
+              expect(data[0]).deep.equal(msg)
               done()


test: sort_pairs: stringify vals, seperate fields

Otherwise something like these would sort differently, despite being the same:

[[{ a: '12', b: '3'}, {}]
 [{ a: '1', b: '23'}, {}]]

[[{ a: '1', b: '23'}, {}]
 [{ a: '12', b: '3'}, {}]]

--- test/index.coffee
+++ test/index.coffee
@@ -144,8 +144,8 @@
 sort_pairs = (pairs) ->
   _(pairs).sortBy (pair) ->
     _(pair).chain()
-      .map((el={}) -> (el[key] for key in _(el).keys().sort()))
-      .flatten().value().join ''
+      .map((el={}) -> (JSON.stringify el[key] for key in _(el).keys().sort()))
+      .flatten().value().join ';'

 make_configs = (config) ->
   # If on is a string, also try the hash version of on to verify it's the same


tooltip<->marionette link logic supports explicit container.

--- core/app/backbone/factlink/tooltip_marionette.coffee
+++ core/app/backbone/factlink/tooltip_marionette.coffee
@@ -5,6 +5,9 @@
   #  parentView: marionette view
   #  tooltipViewFactory: -> view
   #  selector: selector identifying what can be hovered over.
+  #  $offsetParent: a dom node within which to position the
+  #    tooltip with respect to the target (hovered) node. By
+  #    default, uses parentView.$el.

   Backbone.Factlink.Tooltip = (options) ->
     positionedRegion = null
@@ -18,7 +21,8 @@
       positionedRegion =
         new Backbone.Factlink.PositionedRegion regionOptions
       positionedRegion.show new PopoverView regionOptions
-      positionedRegion.bindToElement($target, $el)
+      positionedRegion.bindToElement $target,
+        options.$offsetParent || $el
       positionedRegion.updatePosition()
       positionedRegion.$el


messages sent in order

messages sent in order

--- scripts/neige.coffee
+++ scripts/neige.coffee
@@ -17,19 +17,28 @@

    robot.respond /neige/, (msg) ->

+     sendInOrder (strings) ->
+       for s, i in strings
+         setTimeout () ->
+           msg.send s
+         , 10 * i
+
      robot.http(neigeUrl)
       .header('Accept', 'application/json')
       .get() (err, res, body) ->
         data = JSON.parse(body)
+        messages = []
+        messages.push "*** Bulletin pour la Rosière ***"
+
         fall = data.report.snowfall.snow24h
-
-        msg.send "*** Bulletin pour la Rosière ***"
         if fall
-          msg.send "Elle a tombé : #{fall} cm de neige hier"
+          messages.push "Elle a tombé : #{fall} cm de neige hier"

         onSlope = data.report.snowQuality.onSlope
-        msg.send "En bas: #{onSlope.surfaceBottom}, #{onSlope.lowerDepth} cm"
-        msg.send "En haut: #{onSlope.surfaceTop}, #{onSlope.upperDepth} cm"
+        messages.push "En bas: #{onSlope.surfaceBottom}, #{onSlope.lowerDepth} cm"
+        messages.push "En haut: #{onSlope.surfaceTop}, #{onSlope.upperDepth} cm"
+
+        sendInOrder messages
   #
   #
   # robot.router.post '/hubot/chatsecrets/:room', (req, res) ->


ignore super common unfixable unimportant errors

--- vue/src/shared/helpers/boot.coffee
+++ vue/src/shared/helpers/boot.coffee
@@ -18,6 +18,10 @@

       if AppConfig.sentry_dsn
         Sentry.init
+          ignoreErrors: [
+            "Avoided redundant navigation to current location",
+            "NotFoundError: Node.removeChild"
+          ]
           dsn: AppConfig.sentry_dsn
           integrations: [
             new Integrations.Vue
@@ -25,6 +29,7 @@
               attachProps: true
               logErrors: true
           ]
+
         Sentry.configureScope (scope) ->
           scope.setTag("loomio_version", AppConfig.version)


keeping it simple

keeping it simple

--- documentation/coffee/async.coffee
+++ documentation/coffee/async.coffee
@@ -2,14 +2,14 @@
   new Promise (resolve) ->
     window.setTimeout resolve, ms

+say = (text) ->
+  window.speechSynthesis.cancel() # cancel any prior utterances
+  window.speechSynthesis.speak new SpeechSynthesisUtterance text
+
 countdown = (seconds) ->
   for i in [seconds..1]
-    if window.speechSynthesis?
-      utterance = new SpeechSynthesisUtterance "#{i}"
-      window.speechSynthesis.cancel() # cancel any prior utterances
-      window.speechSynthesis.speak utterance
-    console.log i
+    say "#{i}"
     await sleep 1000 # wait one second
-  alert "Done! (Check the console!)"
+  say "Blastoff!"

 countdown(3)


<C-+> <C--> <C-0> to control font size in nw

--- client/init.coffee
+++ client/init.coffee
@@ -6,11 +6,21 @@
       on: (e, f) -> (@[e] ?= []).push f; @
     socket = new FakeSocket; socket1 = new FakeSocket; socket.other = socket1; socket1.other = socket
     require('./proxy').Proxy()(socket1)
+    nww = require('nw.gui').Window.get()
+    $ ->
+      $('body').on 'keydown', (e) ->
+        if e.which == 187 && e.ctrlKey && !e.altKey # <C-+> or <C-=>
+          nww.zoomLevel++; return false
+        else if e.which == 189 && e.ctrlKey && !e.shiftKey && !e.altKey # <C-->
+          nww.zoomLevel--; return false
+        else if e.which == 48 && e.ctrlKey && !e.shiftKey && !e.altKey # <C-0>
+          nww.zoomLevel = 0; return false
+        return
   else
     socket = io()
   Dyalog.socket = socket
   $ ->
     $('body').on 'keydown', (e) ->
-      if e.which == 112 && !e.ctrlKey && e.shiftKey && !e.altKey
+      if e.which == 112 && !e.ctrlKey && e.shiftKey && !e.altKey # <S-F1>
         alert "Version: #{Dyalog.version}\nBuild date: #{Dyalog.buildDate}"; false
     Dyalog.welcomePage()


Notify and unfriend people who try to friend the bot

--- bot.iced
+++ bot.iced
@@ -360,16 +360,16 @@
 			bot.sendMessage chatterID, "I couldn't understand your request. Send me '+help' for a list of available commands and functions."

 bot.on "friend", (steamID, Relationship) ->
-	if pendingInvites.indexOf(steamID) isnt -1
-		# Have they accepted?
-		if Relationship is Steam.EFriendRelationship.Friend
-			pendingInvites.splice pendingInvites.indexOf(steamID), 1
-
-			bot.joinChat steamID
-			#bot.sendMessage steamID, "Hi, I'm DogeTippingBot"bot.users[steamID]?.playerName
-			for user in bot.users
-				if playerName is shibe
-					undefined
+	if Relationship is Steam.EFriendRelationship.RequestRecipient or Relationship is Steam.EFriendRelationship.RequestInitiator
+		# Remove people who try to friend the bot after notifying them
+		bot.addFriend steamID
+		setTimeout ->
+			bot.sendMessage steamID, "Go to the Doge Tip group to message me. I can't accept friend requests."
+			bot.sendMessage steamID, "Removing friend..."
+			setTimeout ->
+				bot.removeFriend steamID
+			, 2000
+		, 2000

 # Accept IPN callbacks from Moolah for successful payments


Fix clicking on a result and then going back to the Project Find Results

--- lib/project/results-model.coffee
+++ lib/project/results-model.coffee
@@ -246,7 +246,7 @@
     editor.scan @regex, (match) ->
       matches.push(match)

-    result = Result.create({matches})
+    result = Result.create({filePath: editor.getPath(), matches})
     @setResult(editor.getPath(), result)
     @emitter.emit 'did-finish-searching', @getResultsSummary()


Remove animation

Remove animation

This makes fixes issues with slowdowns on page load due to stat animation

--- scripts/stats.coffee
+++ scripts/stats.coffee
@@ -6,6 +6,9 @@
                 new Chart(document.getElementById('downloads-over-time').getContext("2d")).Line({
                     labels : e.data.data.labels,
                     datasets : e.data.data.entries
+                },
+                {
+                    animation: false
                 })
                 keyUI = document.getElementById('downloads-over-time-key')
                 for k in e.data.data.key
@@ -23,6 +26,9 @@
                 new Chart(document.getElementById('followers-over-time').getContext("2d")).Line({
                     labels : e.data.data.labels,
                     datasets : e.data.data.entries
+                },
+                {
+                    animation: false
                 })
     , false)
     worker.postMessage({ action: "set_versions", data: window.versions })


Setting variables and functions is done

Setting variables and functions is done

--- lib/views/status-bar-view.coffee
+++ lib/views/status-bar-view.coffee
@@ -87,7 +87,6 @@
         SettingsHelper.set 'current_core_name', e.name
         statusElement.text e.name

-        # TODO: Test it!
         SettingsHelper.set 'variables', e.variables
         SettingsHelper.set 'functions', e.functions


Chaos! ;)

Chaos! ;)

--- pimatic-smartmeter.coffee
+++ pimatic-smartmeter.coffee
@@ -56,11 +56,13 @@
         @activetariff = Number data.currentTariff
         @emit "activetariff", Number @activetariff

-        @tariff1totalusage = Number data.tariffOneTotalUsage
-        @emit "tariff1totalusage", Number @tariff1totalusage
+        if @tariff1totalusage != Number data.tariffOneTotalUsage
+          @tariff1totalusage = Number data.tariffOneTotalUsage
+          @emit "tariff1totalusage", Number @tariff1totalusage

-        @tariff2totalusage = Number data.tariffTwoTotalUsage
-        @emit "@tariff2totalusage", Number @tariff2totalusage
+        if @tariff2totalusage != Number data.tariffTwoTotalUsage
+          @tariff2totalusage = Number data.tariffTwoTotalUsage
+          @emit "tariff2totalusage", Number @tariff2totalusage

     getActualusage: -> Promise.resolve @actualusage


Skips broken tests

Skips broken tests

--- test/query.coffee
+++ test/query.coffee
@@ -78,14 +78,16 @@
         assert.equal district.get('name'), 'Demo District'
         done()

-    it 'findById with exec', (done) ->
+    # Failing because test data changed. See: https://clever.atlassian.net/browse/APPS-200
+    it.skip 'findById with exec', (done) ->
       @clever.District.findById("4fd43cc56d11340000000005").exec (err, district) =>
         assert not _(district).isArray()
         assert (district instanceof @clever.District), "Incorrect type on district object"
         assert.equal district.get('name'), 'Demo District'
         done()

-    it 'find with a where condition', (done) ->
+    # Failing because test data changed. See: https://clever.atlassian.net/browse/APPS-200
+    it.skip 'find with a where condition', (done) ->
       @clever.School.find().where('name').equals('Clever High School').exec (err, schools) =>
         assert.equal schools.length, 1
         school = schools[0]
@@ -140,7 +142,8 @@
         assert.ifError err
         done()

-    it 'count works', (done) ->
+    # Failing because test data changed. See: https://clever.atlassian.net/browse/APPS-200
+    it.skip 'count works', (done) ->
       @clever.School.find().where('name').equals('Clever High School').count().exec (err, count) ->
         assert.equal count, 1
         done()


Correctly place trees and grass to prevent browser overflow

--- js/views/OverlayGraphicView.coffee
+++ js/views/OverlayGraphicView.coffee
@@ -30,6 +30,8 @@
         maxScale  : 2
         minScale  : 0.5

+  PADDING = 20
+
   class extends Backbone.View
     className:  'overlay-graphic-view'

@@ -44,21 +46,21 @@

       if rect.bottom > rect.right
         if rect.top isnt 0
-          top = (rect.bottom - rect.top - height) * Math.random() << 0
+          top = (rect.bottom - rect.top - height - PADDING) * Math.random() << 0
           top += rect.top
         else
           top = (rect.bottom - height) * Math.random() << 0

-        left = (rect.right - width) * Math.random() << 0
+        left = (rect.right - width - PADDING) * Math.random() << 0

       else
         if rect.left isnt 0
-          left = (rect.right - rect.left - width) * Math.random() << 0
+          left = (rect.right - rect.left - width - PADDING) * Math.random() << 0
           left += rect.left
         else
           left = (rect.right - width) * Math.random() << 0

-        top = (rect.bottom - height) * Math.random() << 0
+        top = (rect.bottom - height - PADDING) * Math.random() << 0

       @$el.addClass "image-#{randomKey}"
       @$el.css 'width', width


Comment.

Comment.

--- test/specs.coffee
+++ test/specs.coffee
@@ -25,6 +25,8 @@
           ->
             data = fs.readFileSync(spec, 'utf-8')
             [meta, input, output] = data.split('----\n')
+
+            # Export data for the reporter.
             fn.data = { input, output }

             result = js2coffee(input)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/state.coffee
+++ test/state.coffee
@@ -29,10 +29,7 @@
   100.times ->
     animation.update()

-  equals animation.currentState(), 'stand'
-
-
-
+  equals animation.currentState(), 'stand'
 ###


fix publisher count on reports page

fix publisher count on reports page

--- imports/api/reports/reports.coffee
+++ imports/api/reports/reports.coffee
@@ -74,14 +74,20 @@
 			])
 			.map (user) -> user._id

-			fulltimeCount = participants.filter((userId) ->
+			fulltimeCount = participants.filter((userId, index, self) ->
 				user = Meteor.users.findOne(userId, 'profile.pioneer': 1)
-				user.profile.pioneer in ['regular', 'special', 'circuit', 'bethelite', 'ldc']
+
+				unique = self.indexOf(userId) == index
+				fulltime = user.profile.pioneer in ['regular', 'special', 'circuit', 'bethelite', 'ldc']
+				fulltime && unique
 			).length

-			publisherCount = participants.filter((userId) ->
+			publisherCount = participants.filter((userId, index, self) ->
 				user = Meteor.users.findOne(userId, 'profile.publisher': 1)
-				user.profile.pioneer in ['publisher', 'auxiliary']
+
+				unique = self.indexOf(userId) == index
+				publisher = user.profile.pioneer in ['publisher', 'auxiliary']
+				publisher && unique
 			).length

 			allCount = fulltimeCount + publisherCount


Plain shaders: Fix float uniforms.

Plain shaders: Fix float uniforms.

--- engine/material_shaders/plain.coffee
+++ engine/material_shaders/plain.coffee
@@ -5,7 +5,7 @@
         {data, _input_list, inputs, _texture_list} = @material
         for u in data.uniforms or []
             {varname, value} = u
-            _input_list.push inputs[varname] = {value}
+            _input_list.push inputs[varname] = u
             if value.type == 'TEXTURE'
                 _texture_list.push inputs[varname]
         @use_projection_matrix_inverse = false
@@ -15,7 +15,7 @@
     get_model_view_matrix_name: -> 'model_view_matrix'

     get_projection_matrix_name: -> 'projection_matrix'
-
+
     get_projection_matrix_inverse_name: ->
         @use_projection_matrix_inverse = true
         return 'projection_matrix_inverse'


Adding testing for mood buttons

Adding testing for mood buttons

--- scripts/awesome.team.coffee
+++ scripts/awesome.team.coffee
@@ -205,3 +205,40 @@
         msg.send "Here are our achievements so far:"
         for achievement in JSON.parse body
           msg.send achievement.description
+
+  robot.respond /testing/i, (msg) ->
+    buttonName = "mood_" + robot.brain.data._private.team[msg.message.room]
+    robot.emit 'slack.attachment',
+      message: msg.message
+      content:
+        text: "How was your day today?"
+        fallback: "Oh no! Something went horribly wrong!"
+        callback_id: "dab_mood"
+        color: "#3AA3E3"
+        attachment_type: "default"
+        actions: [{
+          name: buttonName
+          text: ":bolt-ecstatic:"
+          type: "button"
+          value: "ecstatic"
+        },{
+          name: buttonName
+          text: ":bolt-happy:"
+          type: "button"
+          value: "happy"
+        },{
+          name: buttonName
+          text: ":bolt-indifferent:"
+          type: "button"
+          value: "indifferent"
+        },{
+          name: buttonName
+          text: ":bolt-disappointed:"
+          type: "button"
+          value: "disappointed"
+        },{
+          name: buttonName
+          text: ":bolt-sad:"
+          type: "button"
+          value: "sad"
+        }]


fix typo

fix typo

--- scripts/idobata-talk.coffee
+++ scripts/idobata-talk.coffee
@@ -63,7 +63,7 @@
   robot.hear /ごめん|すみません/i, (msg) ->
     msg.send "分かればいいのよ"

-  robot.hear /:ant/i, (msg) ->
+  robot.hear /:ant:/i, (msg) ->
     msg.send Array(Math.floor(Math.random() * 100) + 1).join ' :ant: '

   # respond


Update parser.coffee

Update parser.coffee

--- src/parser.coffee
+++ src/parser.coffee
@@ -18,11 +18,11 @@
     @countURLTemplateFilters: () -> URLTemplateFilters.length
     @clearUrlTemplateFilters: () -> URLTemplateFilters = []

-    @parse: (url, headers, timeout, cb) ->
-        @_parse url, headers, timeout, null, (err, response) ->
+    @parse: (url, headers, timeout, logger, cb) ->
+        @_parse url, headers, timeout, logger, null, (err, response) ->
             cb(response)

-    @_parse: (url, headers, timeout, parentURLs, cb) ->
+    @_parse: (url, headers, timeout, logger, parentURLs, cb) ->

         # Process url with defined filter
         url = filter(url) for filter in URLTemplateFilters
@@ -30,9 +30,11 @@
         parentURLs ?= []
         parentURLs.push url

+        start = new Date().getTime()
         URLHandler.get url, headers, timeout, (err, xml) =>
             return cb(err) if err?
-
+            took = new Date().getTime() - start
+            logger.log("Vast download took " + took/1000 + " seconds, URL [" + url + "]")
             response = new VASTResponse()

             unless xml?.documentElement? and xml.documentElement.nodeName is "VAST"
@@ -69,7 +71,7 @@
                 ad = response.ads[loopIndex]
                 continue unless ad.nextWrapperURL?
                 do (ad) =>
-                    if parentURLs.length >= 10 or ad.nextWrapperURL in parentURLs
+                    if parentURLs.length >= 20
                         # Wrapper limit reached, as defined by the video player.
                         # Too many Wrapper responses have been received with no InLine response.
                         VASTUtil.track(ad.errorURLTemplates, ERRORCODE: 302)
@@ -82,7 +84,7 @@
                         baseURL = url.slice(0, url.lastIndexOf('/'))
                         ad.nextWrapperURL = "#{baseURL}/#{ad.nextWrapperURL}"

-                    @_parse ad.nextWrapperURL, headers, timeout, parentURLs, (err, wrappedResponse) =>
+                    @_parse ad.nextWrapperURL, headers, timeout, logger, parentURLs, (err, wrappedResponse) =>
                         if err?
                             # Timeout of VAST URI provided in Wrapper element, or of VAST URI provided in a subsequent Wrapper element.
                             # (URI was either unavailable or reached a timeout as defined by the video player.)
@@ -103,9 +105,10 @@
                                 if ad.trackingEvents?
                                     for creative in wrappedAd.creatives
                                         for eventName in Object.keys ad.trackingEvents
-                                            creative.trackingEvents[eventName] or= []
-                                            creative.trackingEvents[eventName] = creative.trackingEvents[eventName].concat ad.trackingEvents[eventName]
-
+                                            if creative.trackingEvents?
+                                                creative.trackingEvents[eventName] or= []
+                                                creative.trackingEvents[eventName] = creative.trackingEvents[eventName].concat ad.trackingEvents[eventName]
+
                                 if ad.sequence? and ad.sequence > 0
                                     wrappedAd.sequence = ad.sequence

@@ -268,4 +271,3 @@
         return node and (node.textContent or node.text)

 module.exports = VASTParser
-


Don't use the new classPrefix feature in highlight.js

--- src/plugins/markdown.coffee
+++ src/plugins/markdown.coffee
@@ -5,6 +5,8 @@
 path = require 'path'
 url = require 'url'
 yaml = require 'js-yaml'
+
+hljs.configure {classPrefix: ''} # keep compatibility with old stylesheets (pre hljs 8.0.0)

 # monkeypatch to add url resolving to marked
 if not marked.InlineLexer.prototype._outputLink?


check for pairs

check for pairs

--- src/player.coffee
+++ src/player.coffee
@@ -8,7 +8,8 @@
   betRequest: (gameState, next) ->
     if gameState.testBet
       return next null, gameState.testBet
-    minBet = gameState.small_blind * 2
+    bigBlind = gameState.small_blind * 2
+    minBet = bigBlind
     bet = gameState.current_buy_in
     bet = minBet unless bet > minBet

@@ -26,8 +27,22 @@
       suit: card.suit
       rank: decodeRank card.rank

+    communityCards = _.map (me.community_cards ? []), (card) ->
+      suit: card.suit
+      rank: decodeRank card.rank
+
     if cards[0]?.rank == cards[0]?.rank
-      return next null, minBet * 10
+      return next null, bigBlind
+
+    allCards = cards.concat communityCards
+
+    pairs = _ allCards
+    .groupBy (card) -> String card.rank
+    .filter (group) -> group.length > 1
+    .value()
+
+    if pairs.length > 0
+      return next null, bigBlind

     next null, bet
   showdown: (gameState, next) ->