Grrr type coercion.

Grrr type coercion.

--- src/quote.coffee
+++ src/quote.coffee
@@ -109,9 +109,10 @@
       m = line.match /(\S+) \[(\d?\d):(\d\d) ([AP]M)\]/
       if m?
         [x, speaker, hours, minutes, ampm] = m
+        [hours, minutes] = [parseInt(hours), parseInt(minutes)]
         hours += 12 if ampm is 'PM'
         msg.reply "hours: #{hours} minutes: #{minutes}"
-        timestamp = moment({hours: parseInt(hours), minutes: parseInt(minutes)})
+        timestamp = moment({hours: hours, minutes: minutes})
         ts = timestamp.format('h:mm A d MMM YYYY')
       else if speaker? and line.length > 0
         processed.push "[#{ts}] #{speaker}: #{line}"


more tomdoc

more tomdoc

--- src/robot.coffee
+++ src/robot.coffee
@@ -142,6 +142,11 @@

   # Public: Helper for making quick HTTP GET requests.
   # otherwise, use @http for the rad Node 0.4 HTTP Client.
+  #
+  # url - String URL to GET.
+  # cb  - Function callback that is triggered with the HTTP response.
+  #
+  # Returns nothing.
   fetch: (url, cb) ->
     uri = Url.parse url
     body = ''


Correct the logger.error output when loadFile fails

It was `Unable to load #{path}`, but `path` is a directory, not the
full path to the file. It should be `full` to get directory & filename.

--- src/robot.coffee
+++ src/robot.coffee
@@ -152,7 +152,7 @@
         require(full) @
         @parseHelp "#{path}/#{file}"
       catch error
-        @logger.error "Unable to load #{path}: #{error}\n#{error.stack}"
+        @logger.error "Unable to load #{full}: #{error}\n#{error.stack}"

   # Public: Load scripts specfic in the `hubot-scripts.json` file.
   #


fix robot.respond bug where a '|' in a regex cause any string to match

--- src/robot.coffee
+++ src/robot.coffee
@@ -48,9 +48,9 @@

     pattern = re.join("/") # combine the pattern back again
     if @enableSlash
-      newRegex = new RegExp("^(?:\/|#{@name}:?)\\s*#{pattern}", modifiers)
+      newRegex = new RegExp("^(?:\/|#{@name}:?)\\s*(?:#{pattern})", modifiers)
     else
-      newRegex = new RegExp("^#{@name}:?\\s*#{pattern}", modifiers)
+      newRegex = new RegExp("^#{@name}:?\\s*(?:#{pattern})", modifiers)

     console.log newRegex.toString()
     @listeners.push new TextListener(@, newRegex, callback)


fix for express 3

fix for express 3

--- src/robot.coffee
+++ src/robot.coffee
@@ -409,7 +409,7 @@

   requireHubSignature: (req, res, next) ->
     return next() if req.hubVerified
-    res.sendStatus 401
+    res.status(401).send "Unauthorized"

   # Setup the Express server's defaults.
   #


one refactor

one refactor


Fix head and tail description. It was exactly opposite.

--- src/marker.coffee
+++ src/marker.coffee
@@ -15,8 +15,8 @@
 #
 # Head and Tail:
 # Markers always have a *head* and sometimes have a *tail*. If you think of a
-# marker as an editor selection, the head is the part that's stationary and the
-# tail is the part that moves when the mouse is moved. A marker without a tail
+# marker as an editor selection, the tail is the part that's stationary and the
+# head is the part that moves when the mouse is moved. A marker without a tail
 # always reports an empty range at the head position. A marker with a head position
 # greater than the tail is in a "normal" orientation. If the head precedes the
 # tail the marker is in a "reversed" orientation.


add TODO

add TODO

--- src/space.coffee
+++ src/space.coffee
@@ -23,6 +23,7 @@
     .then (users) =>
       userId = (u.id for u in users when u.name is backlogUsername)[0]
       return unless userId?
+      # TODO: check resolved
       @getGitHubUrl issueKey
       .then (url) ->
         @backlog.updateIssue issueKey,


bring API more inline with rest of library

--- src/timer.coffee
+++ src/timer.coffee
@@ -8,12 +8,15 @@
     #helper function (closed over)
     currentTime = -> (new Date()).getTime()

-    @DEFAULTS:
+    @defaults:
         startImmediately: true

-    constructor: (startImmediately = Timer.DEFAULTS.startImmediately) ->
+    constructor: (config = {}) ->
+        for own key, value of Timer.defaults
+            config[key] ?= value
+
         @_elapsedTime = 0
-        @_startTime = if startImmediately then currentTime() else null
+        @_startTime = if config.startImmediately then currentTime() else null

     #getters
     isRunning: ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -15,6 +15,7 @@
   self.bind "update", ->
     for property, data of I.activeTweens
       if I.age >= data.endTime
+        I[property]
       f = Easing[data.easing](data.start, data.end)
       I[property] = f()


fix scroll sync for file imports

fix scroll sync for file imports

--- lib/md.coffee
+++ lib/md.coffee
@@ -121,7 +121,6 @@
 getRealDataLine = (lineNo)->
   return lineNo if !HEIGHTS_DELTA.length
   i = HEIGHTS_DELTA.length - 1
-  count = 0
   while i >= 0
     {realStart, start, height, acc} = HEIGHTS_DELTA[i]
     if lineNo == start
@@ -132,10 +131,9 @@
         # console.log(lineNo, HEIGHTS_DELTA, realStart)
         return realStart
       else
-        # console.log(lineNo, HEIGHTS_DELTA, lineNo - acc - height + realStart - i)
-        return lineNo - acc - height + realStart - i
+        # console.log(lineNo, HEIGHTS_DELTA, lineNo - acc - height + i + 1)
+        return lineNo - acc - height + i + 1
     i -= 1
-    count += 1
   return lineNo


Debug output payloads too

Debug output payloads too

--- src/runner.coffee
+++ src/runner.coffee
@@ -11,14 +11,14 @@

 debugReceivedMessages = (client) ->
   client.on 'graph', ({command, payload}) ->
-    debug 'recv graph', command
+    debug 'recv graph', command, payload
   client.on 'network', ({command, payload}) ->
-    debug 'recv network', command
+    debug 'recv network', command, payload
     console.log payload.message if command is 'output' and payload.message
   client.on 'runtime', ({command, payload}) ->
-    debug 'recv runtime', command
+    debug 'recv runtime', command, payload
   client.on 'component', ({command, payload}) ->
-    debug 'recv component', command
+    debug 'recv component', command, payload
   client.on 'execution', (status) ->
     debug 'recv execution', status


drop-ins: Implemented Dropbox drop-ins feature in file tree.

--- client/app/MainApp/filetree/controllers/findertreecontroller.coffee
+++ client/app/MainApp/filetree/controllers/findertreecontroller.coffee
@@ -503,6 +503,8 @@
   cmCloneRepo:     (nodeView, contextMenuItem)-> @cloneRepo nodeView
   cmPublish:       (nodeView, contextMenuItem)-> @publishApp nodeView
   cmCodeShare:     (nodeView, contextMenuItem)-> @createCodeShare nodeView
+  cmDropboxChooser:(nodeView, contextMenuItem)-> @chooseFromDropbox nodeView
+  cmDropboxSaver:  (nodeView, contextMenuItem)-> @saveToDropbox nodeView
   cmOpenTerminal:  (nodeView, contextMenuItem)-> @openTerminalFromHere nodeView
   cmShowOpenWithModal: (nodeView, contextMenuItem)-> @showOpenWithModal nodeView
   cmOpenFileWithApp: (nodeView, contextMenuItem)-> @openFileWithApp  nodeView, contextMenuItem
@@ -744,3 +746,82 @@
         nodeView
         apps
       }
+
+  chooseFromDropbox: (nodeView) ->
+    fileItemViews     = []
+    filePath          = FSHelper.plainPath nodeView.getData().path
+    modal             = null
+    kallback          = ->
+      file            = fileItemViews[0]
+      if file
+        file.emit "FileNeedsToBeDownloadad", filePath
+        file.on   "FileDownloadDone", ->
+          fileItemViews.shift()
+          if fileItemViews.length
+            kallback()
+          else
+            modal.destroy()
+            new KDNotificationView
+              title    : "Your download has been completed"
+              type     : "mini"
+              cssClass : "success"
+              duration : 4000
+
+    Dropbox.choose
+      linkType         : "direct"
+      multiselect      : true
+      success          : (files) ->
+        modal          = new KDModalView
+          overlay      : yes
+          title        : "Download from Dropbox"
+          buttons      :
+            Start      :
+              title    : "Start"
+              cssClass : "modal-clean-green"
+              callback : -> kallback()
+            Cancel     :
+              title    : "Cancel"
+              cssClass : "modal-cancel"
+              callback : -> modal.destroy()
+
+        for file in files
+          fileItemView = modal.addSubView new DropboxDownloadItemView {}, file
+          fileItemViews.push fileItemView
+
+  saveToDropbox: (nodeView) ->
+    notification   = null
+    kiteController = KD.getSingleton "kiteController"
+    plainPath      = FSHelper.plainPath nodeView.getData().path
+    tmpFileName    = "tmp#{Date.now()}"
+    isFolder       = nodeView.getData().type is "folder"
+    kallback       = ->
+      fileName     = FSHelper.getFileNameFromPath plainPath
+      if isFolder
+        fileName   = "#{fileName}.zip"
+      options      =
+        files : [
+          {
+            filename: fileName
+            url     : "http://#{KD.getSingleton('vmController').defaultVmName}/#{tmpFileName}"
+          }
+        ]
+        success: ->
+          notification.notificationSetTitle "Your file has been uploaded"
+          notification.notificationSetTimer 4000
+          notification.setClass "success"
+
+      Dropbox.save options
+
+    if isFolder
+      notification = new KDNotificationView
+        title      : "Zipping your folder"
+        type       : "mini"
+        duration   : 120000
+
+      kiteController.run "mkdir -p Web ; zip -r /home/#{KD.nick()}/Web/#{tmpFileName}.zip #{plainPath}", (err, res) =>
+        return  warn err if err
+        kallback()
+    else
+      kiteController.run "cp #{plainPath} /home/#{KD.nick()}/Web/#{tmpFileName}", (err, res) =>
+        return  warn err if err
+        kallback()


ComputeProvider.test: update test names

ComputeProvider.test: update test names

--- workers/social/lib/social/models/computeproviders/computeprovider.test.coffee
+++ workers/social/lib/social/models/computeproviders/computeprovider.test.coffee
@@ -31,7 +31,7 @@
 # here we have actual tests
 runTests = -> describe 'workers.social.models.computeproviders.computeprovider', ->

-  describe '#fetchProviders()', ->
+  describe '::fetchProviders', ->

     it 'should fetch providers successfully', (done) ->

@@ -43,7 +43,7 @@
         done()

-  describe '#ping()', ->
+  describe '::ping', ->

     it 'should be able to ping for the given provider', (done) ->

@@ -60,14 +60,14 @@
         , done

-  describe '#ping$()', ->
+  describe '::ping$', ->

     it 'should not be able to ping if user doesnt have the right to ping', (done) ->

       expectAccessDenied ComputeProvider, 'ping$', { provider : PROVIDERS.aws }, done

-  describe '#create()', ->
+  describe '::create', ->

     it 'should be able to create a new compute provider', (done) ->

@@ -101,14 +101,14 @@
           done()

-  describe '#create$()', ->
+  describe '::create$', ->

     it 'should fail if user doesnt have permission', (done) ->

       expectAccessDenied ComputeProvider, 'create$', {}, done

-  describe '#fetchAvailable()', ->
+  describe '::fetchAvailable', ->

     it 'should be able to fetch availabe object', (done) ->

@@ -162,7 +162,7 @@
         , done

-  describe '#fetchUsage', ->
+  describe '::fetchUsage', ->

     it 'should be able to fetch the usage', (done) ->

@@ -174,7 +174,7 @@
           done()

-  describe '#fetchPlans', ->
+  describe '::fetchPlans', ->

     it 'should fail if user doesnt have permission', (done) ->

@@ -190,7 +190,7 @@
           done()

-  describe '#fetchTeamPlans', ->
+  describe '::fetchTeamPlans', ->

     it 'should fail if user doesnt have a valid session', (done) ->

@@ -216,7 +216,7 @@
           done()

-  describe '#updateGroupStackUsage', ->
+  describe '::updateGroupStackUsage', ->

     testGroup = null

@@ -275,7 +275,7 @@
             done()

-  describe '#updateGroupInstanceUsage', ->
+  describe '::updateGroupInstanceUsage', ->

     testGroup = null

@@ -342,7 +342,7 @@
             done()

-  describe '#updateGroupResourceUsage', ->
+  describe '::updateGroupResourceUsage', ->

     options = null

@@ -417,7 +417,7 @@
             done()

-  describe '#update()', ->
+  describe '::update', ->

     it 'should if no machine id is given', (done) ->

@@ -448,7 +448,7 @@
           done()

-  describe '#remove()', ->
+  describe '::remove', ->

     it 'should fail if not implemented yet', (done) ->

@@ -485,7 +485,7 @@
       , done

-  describe 'createGroupStack', ->
+  describe '::createGroupStack', ->

     it 'should be able to create group stack', (done) ->


Update reference resolution for optional values in product Json.

--- src/coffee/product-import.coffee
+++ src/coffee/product-import.coffee
@@ -94,37 +94,37 @@

   _prepareNewProduct: (product) ->
     Promise.all [
-      @_resolveReference(@client.productTypes, 'productType', product.productType?, "name=\"#{product.productType?.id}\"")
-      @_resolveProductCategories(product.categories?)
-      @_resolveReference(@client.taxCategories, 'taxCategory', product.taxCategory?, "name=\"#{product.taxCategory?.id}\"")
+      @_resolveReference(@client.productTypes, 'productType', product.productType, "name=\"#{product.productType?.id}\"")
+      @_resolveProductCategories(product.categories)
+      @_resolveReference(@client.taxCategories, 'taxCategory', product.taxCategory, "name=\"#{product.taxCategory?.id}\"")
     ]
-    .spread(prodType, prodCats, taxCat) =>
-      if not prodType.isRejected
+    .spread (prodTypeId, prodCatsIds, taxCatId) =>
+      if prodTypeId
         product.productType =
-          id: prodType.value
+          id: prodTypeId
           typeId: 'product-type'
-      if not prodCats.isRejected
-        product.categories = _.map prodCats.value, (catId) =>
-          id: catId
-          typeId: 'category'
-      if not taxCat.isRejected
+      if taxCatId
         product.taxCategory =
-          id: taxCat.value
-          typeId: 'tax-category'
+            id: taxCatId
+        typeId: 'tax-category'
+      product.categories = _.map prodCatsIds, (catId) =>
+        id: catId
+        typeId: 'category'
+      Promise.resolve product

   _resolveProductCategories: (cats) ->
-    new Promise (resolve, reject) =>
+    new Promise (resolve) =>
       if _.isEmpty(cats)
-        reject("Product categories are undefined.")
+        resolve([])
       else
         Promise.all cats.map (cat) =>
           @_resolveReference(@client.categories, 'categories', cat, "externalId=\"#{cat.id}\"")
-        .then (result) -> resolve(result)
+        .then (result) -> resolve(result.filter(c => c))

   _resolveReference: (service, refKey, ref, predicate) ->
-    new Promise (resolve, reject) =>
+    new Promise (resolve) =>
       if not ref
-        reject("Missing #{refKey}")
+        resolve()
       else if @_cache[refKey][ref.id]
         resolve(@_cache[refKey][ref.id])
       else


Improve devices table ordering

Improve devices table ordering

--- lib/actions/device.coffee
+++ lib/actions/device.coffee
@@ -15,6 +15,7 @@
 			delete device.uuid
 			delete device.download_progress
 			return device
+		, [ 'ID', 'Name', 'Device Type', 'IP Address', 'Application', 'Status', 'Last Seen' ]

 	.catch (error) ->
 		throw error


#85 use project.isPathModified

#85 use project.isPathModified

--- lib/ui.coffee
+++ lib/ui.coffee
@@ -329,10 +329,8 @@
     true

   getModifiedFilePathsInChanges: (changes) ->
-    modifiedEditors = atom.workspace.getTextEditors().filter (editor) -> editor.isModified()
-    modifiedFiles = _.uniq(modifiedEditors.map (editor) -> editor.getPath())
-    updatingFiles = _.uniq(changes.map ({item}) -> item.filePath)
-    updatingFiles.filter (filePath) -> filePath in modifiedFiles
+    _.uniq(changes.map ({item}) -> item.filePath).filter (filePath) ->
+      atom.project.isPathModified(filePath)

   observeStopChanging: ->
     @editor.onDidStopChanging =>


Fixes #121

Fixes #121

config should be @config

--- src/connection.coffee
+++ src/connection.coffee
@@ -183,7 +183,7 @@
     if !@config.options.port && !@config.options.instanceName
       @config.options.port = DEFAULT_PORT
     else if @config.options.port && @config.options.instanceName
-      throw new Error("Port and instanceName are mutually exclusive, but #{config.options.port} and #{config.options.instanceName} provided")
+      throw new Error("Port and instanceName are mutually exclusive, but #{@config.options.port} and #{@config.options.instanceName} provided")

   createDebug: ->


fix #3

fix #3

--- lib/ensime-startup.coffee
+++ lib/ensime-startup.coffee
@@ -97,7 +97,11 @@

   javaHome = atom.config.get('ensime.JAVA_HOME')
   toolsJar = "#{javaHome}/lib/tools.jar"
-  scalaVersion = scalaVersionOfProjectDotEnsime()
+
+  projectPath = atom.project.getPath()
+  ensimeConfigFile = projectPath + '/.ensime'
+
+  scalaVersion = scalaVersionOfProjectDotEnsime(ensimeConfigFile)
   ensimeServerVersion = atom.config.get('ensime.ensimeServerVersion')

   classpathFileName = classpathFile(scalaVersion, ensimeServerVersion)


Rename: shorter function names

Rename: shorter function names

--- src/controller.coffee
+++ src/controller.coffee
@@ -3371,7 +3371,7 @@

     @redrawMain()

-  Editor::markBlock = (line, col, style) ->
+  Editor::mark = (line, col, style) ->
     parent = @tree.getBlockOnLine line; head = parent.start
     # Find the necessary indent for this line
     chars = 0
@@ -3397,7 +3397,7 @@

     return key

-  Editor::unmarkBlock = (key) ->
+  Editor::unmark = (key) ->
     delete @markedBlocks[key]
     return true


fix: give modules name when exporting to AMD

--- src/lib/expose.coffee
+++ src/lib/expose.coffee
@@ -17,7 +17,7 @@

   # AMD / RequireJS
   if define?.amd
-    define [], -> lib
+    define lib_name, [], -> lib

   # windowed browser
   if typeof window is 'object'


Fix unit tests

Fix unit tests

--- src/controller.coffee
+++ src/controller.coffee
@@ -1381,7 +1381,7 @@
     w: MAX_DROP_DISTANCE * 2
     h: MAX_DROP_DISTANCE * 2
   }, (point) =>
-    unless (point.acceptLevel is helper.DISCOURAGE) and not event.shiftKey
+    unless (point.acceptLevel is helper.DISCOURAGE) and not event?.shiftKey
       # Find a modified "distance" to the point
       # that weights horizontal distance more
       distance = mainPoint.from(point)


allow pinkyurl fallback

allow pinkyurl fallback

--- models/team.coffee
+++ models/team.coffee
@@ -30,6 +30,7 @@
     technical: Boolean
     alert: Boolean
     videoURL: String
+    pinkyurl: Boolean
   emails:
     type: [ mongoose.SchemaTypes.Email ]
     validate: [ ((v) -> v.length <= 4), 'max' ]
@@ -216,15 +217,19 @@

 TeamSchema.virtual('screenshot').get ->
   return unless url = @entry.url
-  qs = querystring.stringify
-    url: url
-    viewport: '1024x595'
-    thumbnail_max_width: '320'
-  md5 = crypto.createHash 'md5'
-  md5.update qs.toString(), 'ascii'
-  md5.update env.secrets.url2png, 'ascii'
-  token = md5.digest 'hex'
-  "//alpha.url2png.com/v6/P50A14826D8629/#{token}/png/?#{qs}"
+  if @entry.pinkyurl
+    qs = querystring.stringify url: url, resize: '320x186', 'out-format': 'png'
+    "http://pinkyurl.com/i?#{qs}"
+  else
+    qs = querystring.stringify
+      url: url
+      viewport: '1024x595'
+      thumbnail_max_width: '320'
+    md5 = crypto.createHash 'md5'
+    md5.update qs.toString(), 'ascii'
+    md5.update env.secrets.url2png, 'ascii'
+    token = md5.digest 'hex'
+    "http://alpha.url2png.com/v6/P50A14826D8629/#{token}/png/?#{qs}"

 TeamSchema.method 'updateScreenshot', (callback) ->
   return unless @entry.url


Add more docco.

Add more docco.

--- src/lib/packet.coffee
+++ src/lib/packet.coffee
@@ -272,7 +272,16 @@
             break if @offset == @terminal
             return @_bytesRead - start if offset is end

-        # Unpack the field value.
+        # Unpack the field value. Perform our basic transformations. That is,
+        # convert from a byte array to a JavaScript primitive, or turn a byte
+        # array into a hex string.
+        #
+        # Resist the urge to implement these conversions with pipelines. It
+        # keeps occuring to you, but those transitions are at a higher level of
+        # abstraction, primairly for operations on gathered byte arrays. These
+        # transitions need to take place immediately to populate those arrays.
+
+        # By default, value is as it is.
         bytes = value = @value

         # Create a hex string.
@@ -292,7 +301,7 @@
           if (bytes[bytes.length - 1] & 0x80) == 0x80
             top = bytes.length - 1
             for i in [0...top]
-              value += (~bytes[i] & 0xff)  * Math.pow(256, i)
+              value += (~bytes[i] & 0xff) * Math.pow(256, i)
             # To get the two's compliment as a positive value you use
             # `~1 & 0xff == 254`. For exmaple: `~1 == -2`.
             value += (~(bytes[top] & 0x7f) & 0xff & 0x7f) * Math.pow(256, top)
@@ -311,12 +320,13 @@
       # If we've not yet hit our terminator, check for the terminator. If we've
       # hit the terminator, and we do not have a maximum size to fill, then
       # terminate by setting up the array to terminate.
+      #
+      # A maximum length value means to repeat until the terminator, but a
+      # specific length value means that the zero terminated string occupies a
+      # field that has a fixed length, so we need to skip the used bytes.
       if not @terminated
         if part.terminator.charCodeAt(0) == value
           @terminated = true
-          # A maximum length value means to repeat until the terminator, but a
-          # specific length value means that the zero terminated string occupies
-          # a field that has a fixed length, so we need to skip the used bytes.
           if @repeat == Number.MAX_VALUE
             @repeat = @index + 1
           else
@@ -329,12 +339,17 @@
       # array elements, we repeat the current field type.
       if ++@index <  @repeat
         @nextValue()
+
+      # Otherwise, we've got a complete field value, either a JavaScript
+      # primitive or raw bytes as an array or hex string.
       else
+
         # Push the field value after running it through the pipeline.
         if part.endianness isnt "x"
-          # If the field is a packed field, unpack the value.
+
+          # If the field is a packed field, unpack the values and push them onto
+          # the field list.
           if packing = part.packing
-            # Loop through the packed fields, unpacking the values.
             for pack, i in packing
               if pack.endianness is "b"
                 sum = 0
@@ -343,12 +358,17 @@
                 unpacked = Math.floor(value / Math.pow(2, sum))
                 unpacked = unpacked % Math.pow(2, pack.bits)
                 @_fields.push(unpacked)
+
+          # If the value is a length encoding, we set the repeat value for the
+          # subsequent array of values. If we have a zero length encoding, we
+          # push an empty array through the pipeline, and skip the repeated type.
           else if part.lengthEncoding
-            # If we have a zero length encoding, we push an empty array through
-            # the pipeline, and skip the repeated type.
             if (@pattern[@patternIndex + 1].repeat = value) is 0
               @_fields.push(@pipeline(part, []))
               @patternIndex++
+
+          # Otherwise, the value is what it is, so run it through the user
+          # supplied tranformation pipeline, and push it onto the list of fields.
           else
             value = @_arrayed if part.arrayed
             @_fields.push(@pipeline(part, value))
@@ -372,6 +392,7 @@
           @nextField()
           @nextValue()

+    # Return the number of bytes read in this iteration.
     @_bytesRead - start

   close: ->


Touch event draft

Touch event draft

--- src/controller.coffee
+++ src/controller.coffee
@@ -1302,7 +1302,7 @@
     # already there.
     unless state.clickedLassoSegment then @clearLassoSelection()

-    if state.consumedHitTest then return
+    if state.consumedHitTest or state.suppressLassoSelect then return

     # If the point was actually in the main canvas,
     # start a lasso select.
@@ -2341,21 +2341,80 @@
     'touchmove': 'mousemove'
     'touchend': 'mouseup'

+  # A timeout for selection
+  TOUCH_SELECTION_TIMEOUT = 2000
+
   # We will bind the same way as mouse events do,
   # wrapping to be compatible with a mouse event interface.
+  #
+  # When users drag with multiple fingers, we emulate scrolling.
+  # Otherwise, we emulate mousedown/mouseup
   hook 'populate', 0, ->
-    for touchEvent, mouseEvent of touchEvents then do (touchEvent, mouseEvent) =>
-      @tracker.addEventListener touchEvent, (event) =>
-        # We will take the first touch (we do not support multitouch)
-        trackPoint = @getPointRelativeToTracker event.touches[0]
-
-        # We keep a state object so that handlers
-        # can know about each other.
-        state = {}
-
-        # Call all the handlers.
-        for handler in editorBindings[mouseEvent]
-          handler.call this, trackPoint, event, state
+    @touchScrollAnchor = new draw.Point 0, 0
+    @lassoSelectStartTimeout = null
+
+    @tracker.addEventListener 'touchstart', (event) =>
+      clearTimeout @lassoSelectStartTimeout
+
+      trackPoint = @getPointRelativeToTracker event.touches[0]
+
+      # We keep a state object so that handlers
+      # can know about each other.
+      #
+      # We will suppress lasso select to
+      # allow scrolling.
+      state = {
+        suppressLassoSelect: true
+      }
+
+      # Call all the handlers.
+      for handler in editorBindings.mousedown
+        handler.call this, trackPoint, event, state
+
+      # If we did not hit anything,
+      # we may want to start a lasso select
+      # in a little bit.
+      unless state.consumedHitTest
+        @lassoSelectStartTimeout = setTimeout TOUCH_SELECTION_TIMEOUT, ->
+          state = {}
+
+          for handler in editorBindings.mousedown
+            handler.call this, trackPoint, event, state
+
+      event.preventDefault()
+
+    @tracker.addEventListener 'touchmove', (event) =>
+      clearTimeout @lassoSelectStartTimeout
+
+      trackPoint = @getPointRelativeToTracker event.touches[0]
+
+      # We keep a state object so that handlers
+      # can know about each other.
+      state = {}
+
+      # Call all the handlers.
+      for handler in editorBindings.mousedown
+        handler.call this, trackPoint, event, state
+
+      # If we are in the middle of some action,
+      # prevent scrolling.
+      if @clickedBlock? or @draggingBlock? or @lassoSelectAnchor?
+        event.preventDefault()
+
+    @tracker.addEventListener 'mouseup', (event) =>
+      clearTimeout @lassoSelectStartTimeout
+
+      trackPoint = @getPointRelativeToTracker event.touches[0]
+
+      # We keep a state object so that handlers
+      # can know about each other.
+      state = {}
+
+      # Call all the handlers.
+      for handler in editorBindings.mousedown
+        handler.call this, trackPoint, event, state
+
+      event.preventDefault()

   # CLOSING FOUNDATIONAL STUFF
   # ================================


change the trampoline a bit

change the trampoline a bit

--- library/membrane/duplex.coffee
+++ library/membrane/duplex.coffee
@@ -200,10 +200,10 @@
     # `upstreamAsync` mechanism to invoke the `connected` method at all
     # peers of the inheritance hierarchy. Refer to the method for info.
     trampoline: @provider (context, callback) ->
-        identify = @constructor.identify?()
-        isocket = "Notified from socket %s"
+        isocket = "Executed the trampoline on %s"
         message = "Inbound duplex connection at %s"
         request = "Acknowledged from request at %s"
+        assert identify = try @constructor.identify()
         logger.debug message.magenta, identify.underline
         logger.debug request.magenta, context.url.underline
         logger.debug isocket.magenta, callback.socket.id


исправлены ошибки в классе билдера

исправлены ошибки в классе билдера

--- lib/nbuild/builder.coffee
+++ lib/nbuild/builder.coffee
@@ -87,7 +87,7 @@
     * @field lock {Boolean}
     * @private
     ###
-    @lock = no
+    @_lock = no

     ###*
     * @field defaults {Object}
@@ -154,7 +154,7 @@
   * @public
   ###

-  lock: -> @lock = yes
+  lock: -> @_lock = yes

   ###*
   * Unlock class, continue command execution
@@ -162,8 +162,8 @@
   ###

   unlock: ->
-    @lock = no
-    while not @lock and @commandQue.length > 0
+    @_lock = no
+    while not @_lock and @commandQue.length > 0
       @commandQue.shift()()

@@ -233,11 +233,11 @@
     if @_lock
       @commandQue.push(=> @execConfig(name, options))
       return
-    type = options._type
+    type = options['@type']
     type = 'batch' unless type?
-    return unless @commands[type]?
+    return unless @types[type]?
     options = @_expandConfig(options)
-    @commands[type](this, name, options)
+    @types[type](name, options)
     @_saveState()

   ###*
@@ -290,7 +290,7 @@
   ###

   registerType: (name, func, obj = null) ->
-    if obj
+    if obj?
       @types[name] = _.bind(func, obj)
     else
       @types[name] = func
@@ -361,8 +361,6 @@
       continue if key[0] is '@'
       @defaults[key] = @_parseVars(val)

-
-
   _expandConfig: (config) ->
     result = {}
     for key, val of config


cleanup lines provider

cleanup lines provider

--- lib/provider/lines.coffee
+++ lib/provider/lines.coffee
@@ -1,4 +1,5 @@
 Base = require './base'
+{Point} = require 'atom'
 {padStringLeft} = require '../utils'
 settings = require '../settings'

@@ -6,20 +7,18 @@
 class Lines extends Base
   initialize: ->
     @subscribe @editor.onDidStopChanging =>
-      @items = null # invalidate cache.
+      [@items, @width] = []  # invalidate cache.
       @ui.refresh()

   getItems: ->
-    return @items if @items?
-    @items = []
-    filePath = @editor.getPath()
-    for line, i in @editor.getBuffer().getLines()
-      point = [i, 0]
-      text = line
-      @items.push({filePath, point, text})
-    @items
+    if @items?
+      @items
+    else
+      filePath = @editor.getPath()
+      @items = @editor.getBuffer().getLines().map (text, i) ->
+        {filePath, point: new Point(i, 0), text}

   viewForItem: (item) ->
-    width = String(@editor.getLastBufferRow()).length
-    row = item.point[0] + 1
-    padStringLeft(String(row), width) + ':' + item.text
+    @width ?= String(@editor.getLastBufferRow()).length
+    padString = padStringLeft(String(item.point.row + 1), @width)
+    "#{padString}:#{item.text}"


[core] Log stderr to console during dialtone

--- lib/pylprof/runner.coffee
+++ lib/pylprof/runner.coffee
@@ -32,6 +32,7 @@
   dialtone: (child) ->
     maxAttempts = 10
     curAttempt = 0
+    stderr = ''
     dialtoneKey = '@@@DIALTONE@@@'
     dialtoneCmd = 'print "' + dialtoneKey + '"\n'
     deferred = Q.defer()
@@ -50,7 +51,10 @@
         curAttempt += 1
     ), 1000

-    child.stderr.on 'data', ->
+    child.stderr.on 'data', (err) ->
+      err = err.toString()
+      stderr += err
+      console.log(err)

     child.stdout.on 'data', (data) ->
       if data.toString().indexOf(dialtoneKey) isnt -1


Updated the way models are managed in relationships - if it exists, replace it and optionally update the cache.

--- lib/relations/many.coffee
+++ lib/relations/many.coffee
@@ -98,7 +98,11 @@
     collection = @_ensureCollection(model)
     current_related_model = collection.get(related_model.id)
     return if current_related_model is related_model
-    throw new Error "\nModel added twice: #{util.inspect(current_related_model)}\nand\n#{util.inspect(related_model)}" if current_related_model
+
+    # TODO: this is needed for model lifecycle - not knowing when a model is actually disposed and not wanting to remove a model from a relationship
+    # throw new Error "\nModel added twice: #{util.inspect(current_related_model)}\nand\n#{util.inspect(related_model)}" if current_related_model
+    collection.remove(current_related_model) if current_related_model
+    @reverse_model_type.cache.set(related_model.id, related_model) if @reverse_model_type.cache and related_model.id # make sure the latest model is in the cache
     collection.add(related_model)

   remove: (model, related_model) ->


image tooltip preview

image tooltip preview

--- src/modules/image-tooltip.coffee
+++ src/modules/image-tooltip.coffee
@@ -25,6 +25,15 @@
         'text-align': 'center'
         'top': '40%'
         'width': '100%'
+      '.image-tooltip-container img':
+        'bottom': '0'
+        'left': '0'
+        'margin': 'auto'
+        'max-height': '100%'
+        'max-width': '100%'
+        'position': 'absolute'
+        'right': '0'
+        'top': '0'
       '.image-tooltip-container .input':
         'box-sizing': 'border-box'
         'width': '100%'
@@ -48,18 +57,32 @@
     @options.styles = _.defaults(@options.styles, Tooltip.DEFAULTS.styles)
     @options = _.defaults(@options, Tooltip.DEFAULTS)
     super(@quill, @editorContainer, @options)
+    @preview = @container.querySelector('.preview')
     @textbox = @container.querySelector('.input')
     DOM.addClass(@container, 'image-tooltip-container')
     this.initToolbar()
+    this.initListeners()
+
+  initListeners: ->
     DOM.addEventListener(@container.querySelector('.insert'), 'click', =>
       url = this._normalizeURL(@textbox.value)
       if @range
+        @preview.innerHTML = 'Preview'
+        @textbox.value = ''
         @quill.insertEmbed(@range.end, 'image', url, 'user')
         @quill.setSelection(@range.end + 1, @range.end + 1)
-      @textbox.value = ''
       this.hide()
     )
     DOM.addEventListener(@container.querySelector('.cancel'), 'click', _.bind(this.hide, this))
+    DOM.addEventListener(@textbox, 'input', =>
+      if this._matchImageURL(@textbox.value)
+        if @preview.firstChild.tagName == 'IMG'
+          @preview.firstChild.setAttribute('src', @textbox.value)
+        else
+          img = @preview.ownerDocument.createElement('img')
+          img.setAttribute('src', @textbox.value)
+          @preview.replaceChild(img, @preview.firstChild)
+    )

   initToolbar: ->
     @quill.onModuleLoad('toolbar', (toolbar) =>
@@ -76,6 +99,9 @@
       )
     )

+  _matchImageURL: (url) ->
+    return /^https?:\/\/.+\.(jp?g|gif|png)$/.test(url)
+
   _normalizeURL: (url) ->
     # For now identical to link-tooltip but will change when we allow data uri
     url = 'http://' + url unless /^https?:\/\//.test(url)


Handle ".." in asar path, fix #1982

Handle ".." in asar path, fix #1982

--- atom/common/lib/asar.coffee
+++ atom/common/lib/asar.coffee
@@ -20,6 +20,7 @@
 splitPath = (p) ->
   return [false] if typeof p isnt 'string'
   return [true, p, ''] if p.substr(-5) is '.asar'
+  p = path.normalize p
   index = p.lastIndexOf ".asar#{path.sep}"
   return [false] if index is -1
   [true, p.substr(0, index + 5), p.substr(index + 6)]


add a `__IS_CACHED` flag when yielding a cached response

--- src/plugins/cache-handler.coffee
+++ src/plugins/cache-handler.coffee
@@ -36,6 +36,8 @@
       cacheHandler = clientOptions.cacheHandler or @
       if status is 304 or status is 0
         {data, status} = cacheHandler.get(method, path)
+        # Set a flag on the object so users know this is a cached response
+        data.__IS_CACHED = ref.eTag or true
       else
         # Cache the response to reuse later
         if method is 'GET' and jqXHR.getResponseHeader('ETag')


Add enableTypes to example

Add enableTypes to example

--- src/scripts/configs/aloha.coffee
+++ src/scripts/configs/aloha.coffee
@@ -147,7 +147,7 @@
       ]
       # Disallow custom Example types
       example: [
-        # { label: 'Example', typeClass: 'example', hasTitle: true }
+         { label: 'Example', typeClass: 'example', enableTypes: false }
       ]
       block:
         dragdrop: "1"


Small tweaks to ES script

Small tweaks to ES script

--- src/scripts/elasticsearch.coffee
+++ src/scripts/elasticsearch.coffee
@@ -6,6 +6,8 @@
 #   hubot: elasticsearch cat nodes [cluster] - Gets the information from the cat nodes endpoint for the given server or alias
 #   hubot: elasticsearch cat indexes [cluster] - Gets the information from the cat indexes endpoint for the given server or alias
 #   hubot: elasticsearch clear cache [cluster] - Clears the cache for the specified cluster
+#   hubot: elasticsearch disable allocation [cluster] - disables shard allocation to allow nodes to be taken offline
+#   hubot: elasticsearch enable allocation [cluster] - renables shard allocation
 #   hubot: elasticsearch show aliases - shows the aliases for the list of ElasticSearch instances
 #   hubot: elasticsearch add alias [alias name] [url] - sets the alias for a given url
 #   hubot: elasticsearch clear alias [alias name] - please note that this needs to include any port numbers as appropriate
@@ -23,7 +25,7 @@
 module.exports = (robot) ->

   robot.brain.on 'loaded', ->
-    if robot.brain.data.elasticsearch_aliases?
+    if robot.brain.data.elasticsearch_aliases != null
       _esAliases = robot.brain.data.elasticsearch_aliases

   cluster_health = (msg, alias) ->


GH-3: Ensure intervals are cleared

GH-3: Ensure intervals are cleared

Previously the same interval ID was re-used for each check, which could
lead to conflicts if two Pingdom lookups were made simultaneously.

--- src/scripts/pingdom-tools.coffee
+++ src/scripts/pingdom-tools.coffee
@@ -25,7 +25,7 @@
         if body
           if res = JSON.parse body
             if res.poll_state_url
-              @intervalId = setInterval () ->
+              intervals[res.test_id] = setInterval () ->
                 msg.http(res.poll_state_url)
                   .get() (err, res, poll_body) ->
                     if poll_body
@@ -47,10 +47,10 @@
                           replies.push "#{result.page_bytes} bytes in #{result.page_load_time} ms."
                         if replies.length
                           msg.send(replies.join("\n"))
-                        clearInterval @intervalId
+                        clearInterval intervals[res.test_id]
                     if err
                       msg.send('Error running Pingdom Full Page Test on ' + fpt_site)
-                      clearInterval @intervalId
+                      clearInterval intervals[res.test_id]
               , 2000

 client = new PingdomTools


Implemented Serialzier `buffer`.

Implemented Serialzier `buffer`.

--- src/lib/packet.coffee
+++ src/lib/packet.coffee
@@ -2,13 +2,10 @@
 ieee754 = require "./ieee754"
 stream = require "stream"

-# Convert an array of bytes into an hex string, two characters for each byte.
+# Convert an array of bytes into an hex string, two characters for each byte. #
+# FIXME Why less than 10 when we're doing hex?
 hex = (bytes) ->
-  h = bytes.map (b) ->
-    if b < 10
-      "0" + b.toString(16)
-    else
-      b.toString(16)
+  h = bytes.map (b) -> if b < 10 then "0" + b.toString(16) else b.toString(16)
   h.join("")

 # Default callback, when no callback is provided.
@@ -48,6 +45,7 @@
     @callback = null
     @fields = []

+  # Excute the pipeline of transforms for the `pattern` on the `value`.
   pipeline: (pattern, value) ->
     # Run the piplines for parsing.
     if pattern.transforms
@@ -472,36 +470,51 @@
         @_buffer[i] = fill
       @_write @_buffer.slice 0, size

+  buffer: (shiftable...) ->
+    callback = @_reset(shiftable)
+    read = 0
+    while @pattern
+      if read is @_buffer.length
+        buffer = new Buffer buffer.length * 2
+        @_buffer.copy(buffer)
+        @_buffer = buffer
+      read += @write(@_buffer, read, @_buffer.length - read)
+    callback.call @self, @_buffer.slice 0, read
+
   serialize: (shiftable...) ->
-    if typeof shiftable[shiftable.length - 1] == 'function'
-      callback = shiftable.pop()
-
-    nameOrPattern = shiftable.shift()
-    packet        = @packets[nameOrPattern] or {}
-    pattern       = packet.pattern or parsePattern(nameOrPattern)
-    callback    or= packet.callback or noop
-
-    if not packet.pattern
-      for part in pattern
-        if part.transforms
-          part.transforms.reverse()
-
-    @pattern      = pattern
-    @callback     = callback
-    @patternIndex = 0
-    @outgoing     = shiftable
-
-    for value, i in @outgoing
-      @outgoing[i] = @pipeline(pattern[i], value)
-
-    @nextField()
-    @nextValue()
-
+    @callback = @_reset(shiftable)
     # Implementing pause requires callbacks.
     if @streaming
       while @pattern
         read = @write(@_buffer, 0, @_buffer.length)
         @_write @_buffer.slice 0, read
+
+  _reset: (shiftable) ->
+    if typeof shiftable[shiftable.length - 1] == 'function'
+      callback = shiftable.pop()
+
+    nameOrPattern = shiftable.shift()
+    packet        = @packets[nameOrPattern] or {}
+    pattern       = packet.pattern or parsePattern(nameOrPattern)
+    callback    or= packet.callback or noop
+
+    if not packet.pattern
+      for part in pattern
+        if part.transforms
+          part.transforms.reverse()
+
+    @pattern      = pattern
+    @callback     = noop
+    @patternIndex = 0
+    @outgoing     = shiftable
+
+    for value, i in @outgoing
+      @outgoing[i] = @pipeline(pattern[i], value)
+
+    @nextField()
+    @nextValue()
+
+    callback

   _write: (slice) ->
     if @_decoder
@@ -591,7 +604,7 @@
       # because the callback may specify a subsequent packet to parse.
       else if ++@patternIndex is @pattern.length
         @pattern = null
-        @callback.apply @self, [ this ]
+        @callback.call @self, this

       else


Right clicks on selections in catalog items.

--- client/lib/items.coffee
+++ client/lib/items.coffee
@@ -44,11 +44,15 @@
         pageX: event.pageX
         pageY: event.pageY

-      # Allow user to right click the link
-      return if event.button is 2
-
       # Temporarily hide the link to allow selection
       $(template.find '.full-item-link').hide()
+
+      if event.button is 2
+        # On right clicks, determine if user was trying to interact with selection or the link.
+        # If user didn't right click on an item that holds some part of the selection, show the
+        # link again so they can get a context menu for the link instead.
+        underMouse = document.elementFromPoint event.clientX, event.clientY
+        $(template.find '.full-item-link').show() unless rangy.getSelection().containsNode underMouse, true

     'mouseup': (event, template) ->
       # Don't redirect if user interacted with one of the actionable controls or a link on the item


edit dialog filled correctly

edit dialog filled correctly

--- client/neuropils.coffee
+++ client/neuropils.coffee
@@ -68,12 +68,14 @@
     doc.output_is_checked = false
     doc.input_is_checked = false

-    if myself.neuropils_unspecific? and myself.neuropils_unspecific.indexOf(doc._id) != -1
-      doc.unspecific_is_checked = true
-    if myself.neuropils_output? and myself.neuropils_output.indexOf(doc._id) != -1
-      doc.output_is_checked = true
-    if myself.neuropils_input? and myself.neuropils_input.indexOf(doc._id) != -1
-      doc.input_is_checked = true
+    for tmp in myself.neuropils
+      if tmp._id == doc._id
+        if "unspecific" in tmp.type
+          doc.unspecific_is_checked = true
+        if "output" in tmp.type
+          doc.output_is_checked = true
+        if "input" in tmp.type
+          doc.input_is_checked = true

     result.push doc
     return


Reduce line length.

Reduce line length.

--- client/room/room.coffee
+++ client/room/room.coffee
@@ -199,4 +199,6 @@
   error: ->
     Session.get 'recError'
   disabled: ->
-    'disabled' if Session.get('recWaiting') or Session.get('recError') or not Session.get 'recTitle'
+    'disabled' if Session.get('recWaiting') or
+      Session.get('recError') or
+      not Session.get 'recTitle'


Test the new `ready` event.

Test the new `ready` event.

--- test/test.coffee
+++ test/test.coffee
@@ -397,11 +397,16 @@

         describe "when calling clear", ->
             beforeEach ->
+                @publishReady = sinon.stub()
+                service.on("ready", @publishReady)
                 service.set("testkey1", 1)
                 service.set("testkey2", 2)
                 service.set("testkey3", 3)
                 service.clear()

+            afterEach ->
+                service.off("ready", @publishReady)
+
             it "should call storageProvider.getItem for the index", ->
                 spyCall = @getMetadata.getCall(0)
                 spyCall.args[0].should.equal("testns#index:primary")
@@ -422,6 +427,9 @@
             it "should result in an empty array for keys", ->
                 @keys = service.keys
                 @keys.length.should.equal(0)
+
+            it "should NOT publish a 'ready' event", ->
+                @publishReady.should.not.have.been.called

     describe "with one item of pre-loaded data", ->
@@ -488,11 +496,29 @@
             ).should.throw(StoreitError)

         describe "then once we call load()", ->
+            beforeEach ->
+                @publishReady = sinon.stub()
+                service.on("ready", @publishReady)
+                service.load()
+
+            afterEach ->
+                service.off("ready", @publishReady)
+
             it "should have `isInitialized` set to true", ->
-                service.load()
                 service.isInitialized.should.equal(true)
+            it "should publish a 'ready' event", ->
+                @publishReady.should.have.been.called

         describe "then once we call clear()", ->
+            beforeEach ->
+                @publishReady = sinon.stub()
+                service.on("ready", @publishReady)
+                service.clear()
+
+            afterEach ->
+                service.off("ready", @publishReady)
+
             it "should have `isInitialized` set to true", ->
-                service.clear()
                 service.isInitialized.should.equal(true)
+            it "should publish a 'ready' event", ->
+                @publishReady.should.have.been.called


add some pending tests

add some pending tests

--- test/test.coffee
+++ test/test.coffee
@@ -45,4 +45,25 @@

     it 'should ignore relative files in a subdirectory', (done) ->
         expect(result.foo['sub/baz.js']).contain "require('../script')"
+        done()
+
+describe 'transitive locals', ->
+
+    simple = fixture 'simple'
+    c = new Converter simple, Converter.RELATIVE, opts
+    result = c.start()
+
+    it.skip 'should be traversed', (done) ->
+        done()
+
+describe 'multiple lookup paths', ->
+
+    simple = fixture 'simple'
+    c = new Converter simple, Converter.RELATIVE, opts
+    result = c.start()
+
+    it.skip 'should work with 2 lookup paths in root component', (done) ->
+        done()
+
+    it.skip 'should use inner lookup path within local component', (done) ->
         done()


added null checks to mandrill

added null checks to mandrill


mp4: Fix calculation of sample times

mp4: Fix calculation of sample times

--- mp4.coffee
+++ mp4.coffee
@@ -146,7 +146,7 @@
     # Get next sample number
     stblBox = @videoTrakBox.child('mdia').child('minf').child('stbl')
     sttsBox = stblBox.child('stts') # TimeToSampleBox
-    videoSample = sttsBox.getSampleAtSeconds endSeconds
+    videoSample = sttsBox.getSampleAfterSeconds endSeconds
     if videoSample?
       videoSampleNumber = videoSample.sampleNumber
     else
@@ -220,44 +220,56 @@
     seq.wait 2, callback

   seek: (seekSeconds=0) ->
+    logger.debug "[mp4:#{@filename}] seek: seconds=#{seekSeconds}"
     @clearBuffers()

-    # Find out starting play time by video sample
-    stblBox = @videoTrakBox.child('mdia').child('minf').child('stbl')
-    sttsBox = stblBox.child('stts') # TimeToSampleBox
-    videoSample = sttsBox.getSampleAtSeconds seekSeconds
-    if videoSample?
-      videoSampleSeconds = videoSample.seconds
-      @currentPlayTime = videoSampleSeconds
-      videoSampleNumber = videoSample.sampleNumber
-    else
-      # No video sample left
-      @isVideoEOF = true
-      @currentPlayTime = @getDurationSeconds()
-      videoSampleNumber = @numVideoSamples + 1
-      videoSampleSeconds = @currentPlayTime
-
-    # Look for the corresponding audio sample
-    stblBox = @audioTrakBox.child('mdia').child('minf').child('stbl')
-    sttsBox = stblBox.child('stts') # TimeToSampleBox
-    audioSample = sttsBox.getSampleAtSeconds seekSeconds
-    if audioSample?
-      audioSampleNumber = audioSample.sampleNumber
-
-      if videoSampleSeconds <= audioSample.seconds
-        minTime = videoSampleSeconds
+    if @videoTrakBox?
+      # Seek video sample
+      stblBox = @videoTrakBox.child('mdia').child('minf').child('stbl')
+      sttsBox = stblBox.child('stts') # TimeToSampleBox
+      videoSample = sttsBox.getSampleAfterSeconds seekSeconds
+      if videoSample?
+        videoSampleSeconds = videoSample.seconds
+        @currentPlayTime = videoSampleSeconds
+        videoSampleNumber = videoSample.sampleNumber
       else
-        minTime = audioSample.seconds
-      if @currentPlayTime isnt minTime
-        @currentPlayTime = minTime
-    else
-      # No audio sample left
-      audioSampleNumber = @numAudioSamples + 1
-      @isAudioEOF = true
-
-    @consumedAudioSamples = audioSampleNumber - 1
-    @consumedVideoSamples = videoSampleNumber - 1
-
+        # No video sample left
+        @isVideoEOF = true
+        @currentPlayTime = @getDurationSeconds()
+        videoSampleNumber = @numVideoSamples + 1
+        videoSampleSeconds = @currentPlayTime
+    else
+      videoSampleNumber = null
+      videoSampleSeconds = null
+
+    if @audioTrakBox?
+      # Seek audio sample
+      stblBox = @audioTrakBox.child('mdia').child('minf').child('stbl')
+      sttsBox = stblBox.child('stts') # TimeToSampleBox
+      audioSample = sttsBox.getSampleAfterSeconds seekSeconds
+      if audioSample?
+        audioSampleNumber = audioSample.sampleNumber
+
+        if videoSampleSeconds? and (videoSampleSeconds <= audioSample.seconds)
+          minTime = videoSampleSeconds
+        else
+          minTime = audioSample.seconds
+        if @currentPlayTime isnt minTime
+          @currentPlayTime = minTime
+      else
+        # No audio sample left
+        audioSampleNumber = @numAudioSamples + 1
+        @isAudioEOF = true
+    else
+      audioSampleNumber = null
+
+    if audioSampleNumber?
+      @consumedAudioSamples = audioSampleNumber - 1
+
+    if videoSampleNumber?
+      @consumedVideoSamples = videoSampleNumber - 1
+
+    logger.debug "[mp4:#{@filename}] set current play time to #{@currentPlayTime}"
     return @currentPlayTime

   play: ->
@@ -1034,6 +1046,8 @@
     #       elst <- self
     mvhdBox = @findParent('moov').find('mvhd')

+    # We cannot get mdhd box at this time, since it is not parsed yet
+
     entryCount = bits.read_uint32()
     @entries = []
     for i in [1..entryCount]
@@ -1048,17 +1062,47 @@
       if mediaRateFraction isnt 0
         logger.warn "[mp4] warning: media_rate_fraction is not 0 in elst box: #{mediaRateFraction}"
       @entries.push
-        segmentDuration: segmentDuration
+        segmentDuration: segmentDuration  # in Movie Header Box (mvhd) timescale
         segmentDurationSeconds: segmentDuration / mvhdBox.timescale
-        mediaTime: mediaTime
+        mediaTime: mediaTime  # in media (mdhd) timescale
         mediaRate: mediaRateInteger + mediaRateFraction / 65536 # TODO: Is this correct?

     if bits.has_more_data()
       throw new Error "elst box has more data"

+  # Returns the starting offset for this track in mdhd timescale units
+  getEmptyDuration: ->
+    time = 0
+    for entry in @entries
+      if entry.mediaTime is -1  # empty edit
+        # moov
+        #   mvhd <- find target
+        #   iods
+        #   trak
+        #     tkhd
+        #     edts
+        #       elst <- self
+        mvhdBox = @findParent('moov').child('mvhd')
+
+        #   trak
+        #     tkhd
+        #     edts
+        #       elst <- self
+        #     mdia
+        #       mdhd <- find target
+        mdhdBox = @findParent('trak').child('mdia').child('mdhd')
+        if not mdhdBox?
+          throw new Error "cannot access mdhd box (not parsed yet?)"
+
+        # Convert segmentDuration from mvhd timescale to mdhd timescale
+        time += entry.segmentDuration * mdhdBox.timescale / mvhdBox.timescale
+      else
+        # mediaTime is already in mdhd timescale, so no conversion needed
+        return time + entry.mediaTime
+
   getDetails: (detailLevel) ->
-    @entries.map((entry) ->
-      "segmentDurationSeconds=#{entry.segmentDurationSeconds} mediaTime=#{entry.mediaTime} mediaRate=#{entry.mediaRate}"
+    @entries.map((entry, index) ->
+      "[#{index}]:segmentDuration=#{entry.segmentDuration},segmentDurationSeconds=#{entry.segmentDurationSeconds},mediaTime=#{entry.mediaTime},mediaRate=#{entry.mediaRate}"
     ).join(',')

   getTree: ->
@@ -1423,34 +1467,74 @@
       time += entry.sampleDelta * entry.sampleCount
     return time / mdhdBox.timescale

-  getSampleAtSeconds: (sec) ->
+  # Returns a sample which comes exactly at or immediately after
+  # the specified time (in seconds). If isExclusive=true, it excludes
+  # a sample whose timestamp is equal to the specified time.
+  # If there is no matching sample, this method returns null.
+  getSampleAfterSeconds: (sec, isExclusive=false) ->
     timescale = @findParent('mdia').find('mdhd').timescale
-
     remainingTime = sec * timescale
     sampleNumber = 1
     totalTime = 0
     for entry in @entries
-      samples = Math.ceil(remainingTime / entry.sampleDelta)
-      if samples <= entry.sampleCount
-        totalTime += samples * entry.sampleDelta
+      numSamples = Math.ceil(remainingTime / entry.sampleDelta)
+      if numSamples <= entry.sampleCount
+        totalTime += numSamples * entry.sampleDelta
+        totalSeconds = totalTime / timescale
+        if isExclusive and (totalSeconds <= sec)
+          numSamples++
+          totalTime += entry.sampleDelta
+          totalSeconds = totalTime / timescale
         return {
-          sampleNumber: sampleNumber + samples
+          sampleNumber: sampleNumber + numSamples
+          time: totalTime
+          seconds: totalSeconds
+        }
+      sampleNumber += entry.sampleCount
+      entryDuration = entry.sampleDelta * entry.sampleCount
+      totalTime += entryDuration
+      remainingTime -= entryDuration
+
+    # EOF
+    return null
+
+  # Returns a sample which represents the data at the specified time
+  # (in seconds). If there is no sample at the specified time, this
+  # method returns null.
+  getSampleAtSeconds: (sec) ->
+    timescale = @findParent('mdia').find('mdhd').timescale
+    remainingTime = sec * timescale
+    sampleNumber = 1
+    totalTime = 0
+    for entry in @entries
+      sampleIndexInChunk = Math.floor(remainingTime / entry.sampleDelta)
+      if sampleIndexInChunk < entry.sampleCount
+        totalTime += sampleIndexInChunk * entry.sampleDelta
+        return {
+          sampleNumber: sampleNumber + sampleIndexInChunk
           time: totalTime
           seconds: totalTime / timescale
         }
-      sampleNumber += samples
-      totalTime += samples * entry.sampleDelta
-      remainingTime -= entry.sampleDelta * entry.sampleCount
+      sampleNumber += entry.sampleCount
+      entryDuration = entry.sampleDelta * entry.sampleCount
+      totalTime += entryDuration
+      remainingTime -= entryDuration
+
     # EOF
     return null

   # Returns a decoding time for the given sample number.
   # The sample number starts at 1.
   getDecodingTime: (sampleNumber) ->
-    mdhdBox = @findParent('mdia').find('mdhd')
+    trakBox = @findParent('trak')
+    elstBox = trakBox.child('edts')?.child('elst')
+    mdhdBox = trakBox.child('mdia').child('mdhd')

     sampleNumber--
-    time = 0
+    if elstBox?
+      time = elstBox.getEmptyDuration()
+    else
+      time = 0
     for entry in @entries
       if sampleNumber > entry.sampleCount
         time += entry.sampleDelta * entry.sampleCount
@@ -1466,8 +1550,8 @@
   getDetails: (detailLevel) ->
     str = "entryCount=#{@entryCount}"
     if detailLevel >= 2
-      str += ' ' + @entries.map((entry) ->
-        "sampleCount=#{entry.sampleCount} sampleDelta=#{entry.sampleDelta}"
+      str += ' ' + @entries.map((entry, index) ->
+        "[#{index}]:sampleCount=#{entry.sampleCount},sampleDelta=#{entry.sampleDelta}"
       ).join(',')
     return str


Changes for MM 3.4+

Changes for MM 3.4+

--- src/matteruser.coffee
+++ src/matteruser.coffee
@@ -118,23 +118,24 @@

     message: (msg) =>
         @robot.logger.debug msg
-        return if msg.user_id == @self.id # Ignore our own output
-        @robot.logger.debug 'From: ' + msg.user_id + ', To: ' + @self.id
+        data = msg.data
+        return if data.post.user_id == @self.id # Ignore our own output
+        @robot.logger.debug 'From: ' + data.post.user_id + ', To: ' + @self.id

-        mmChannel = @client.getChannelByID msg.channel_id if msg.channel_id
-        mmUser = @client.getUserByID msg.user_id
-        mmPost = JSON.parse msg.data.post
+        mmChannel = @client.getChannelByID data.post.channel_id if data.post.channel_id
+        mmUser = @client.getUserByID data.post.user_id
+        mmPost = JSON.parse data.post

         @robot.logger.debug 'Received message from '+mmUser.username+': ' + mmPost.message
-        user = @robot.brain.userForId msg.user_id
-        user.room = msg.channel_id
+        user = @robot.brain.userForId data.post.user_id
+        user.room = data.post.channel_id

         text = mmPost.message
         if msg.data.channel_type == 'D' and !///^#{@robot.name} ///i.test(text) # Direct message
           text = "#{@robot.name} #{text}"
-          user.mm.dm_channel_id = msg.channel_id
+          user.mm.dm_channel_id = data.post.channel_id

-        @receive new TextMessage user, text, msg.id
+        @receive new TextMessage user, text, data.post.id
         @robot.logger.debug "Message sent to hubot brain."
         return true


CI-38 Append forms to body before submission

Chrome 55 adheres to the spec so we can no longer submit forms which are
not a part of the DOM.

It was technically illegal to put a <form> inside a <button>, even
thought it worked fine.

This changes it so that we attach forms to <body> before submission.

--- lib/assets/javascripts/buttons.js.coffee
+++ lib/assets/javascripts/buttons.js.coffee
@@ -31,7 +31,7 @@
           form = $('<form/>').attr(action: button.attr('href'), method: 'POST')
           $('<input/>').attr(type: 'hidden', name: '_method', value: button.data('sqmethod')).appendTo form
           $('<input/>').attr(type: 'hidden', name: $('meta[name=csrf-param]').attr('content'), value: $('meta[name=csrf-token]').attr('content')).appendTo form
-          form.appendTo button
+          form.appendTo document.body
           form.submit()
         else
           window.location = button.attr('href')


Use userChange logic on enter/leave

Use userChange logic on enter/leave

This should preserve existing data for this user

--- src/matteruser.coffee
+++ src/matteruser.coffee
@@ -112,13 +112,16 @@

     userAdded: (msg) =>
         mmUser = @client.getUserByID msg.user_id
-        user = @robot.brain.userForId msg.user_id, name: mmUser.username, room: msg.channel_id
+        @userChange user
+        user = @robot.brain.userForId msg.user_id
+        user.room = msg.channel_id
         @receive new EnterMessage user
         return true

     userRemoved: (msg) =>
         mmUser = @client.getUserByID msg.user_id
-        user = @robot.brain.userForId msg.user_id, name: mmUser.username, room: msg.channel_id
+        user = @robot.brain.userForId msg.user_id
+        user.room = msg.channel_id
         @receive new LeaveMessage user
         return true


Add support for Mattermost message attachments

See https://docs.mattermost.com/developer/message-attachments.html
A Mattermost message can include message attachments. This support will
add `msg.props` which includes the attachments. As well, text fields
from any attachments are appended to the message text against which
`bot.hear()` matches.

--- src/matteruser.coffee
+++ src/matteruser.coffee
@@ -6,6 +6,40 @@

     constructor: (@user, @text, @file_ids, @id) ->
         super @user, @text, @id
+
+# A TextMessage class that adds `msg.props` for Mattermost's properties.
+#
+# Text fields from message attachments are appended in @text for matching.
+# <https://docs.mattermost.com/developer/message-attachments.html>
+# The result is that `bot.hear()` will match against these attachment fields.
+#
+# As well, it is possible that some bot handlers could make use of other
+# fields on `msg.props`.
+#
+# Example raw props:
+#   {
+#       "attachments": [...],
+#       "from_webhook": "true",
+#       "override_username": "trenthere"
+#   }
+class TextAndPropsMessage extends TextMessage
+
+    constructor: (@user, @text, @props, @id) ->
+        @origText = @text
+        if @props.attachments?
+            separator = '\n\n--\n\n'
+            for attachment in @props.attachments
+                parts = []
+                for field in ['pretext', 'title', 'text']
+                    if attachment[field]
+                        parts.push(attachment[field])
+                if parts.length
+                    @text += separator + parts.join '\n\n'
+
+        super @user, @text, @id
+
+    match: (regex) ->
+        @text.match regex

 class Matteruser extends Adapter

@@ -188,6 +222,9 @@

         if mmPost.file_ids?
             @receive new AttachmentMessage user, text, mmPost.file_ids, mmPost.id
+        # If there are interesting props, then include them for bot handlers.
+        else if mmPost.props?.attachments?
+            @receive new TextAndPropsMessage user, text, mmPost.props, mmPost.id
         else
             @receive new TextMessage user, text, mmPost.id
         @robot.logger.debug "Message sent to hubot brain."


fix username problem closes #10

fix username problem closes #10

--- src/meteorchat.coffee
+++ src/meteorchat.coffee
@@ -46,7 +46,8 @@

         @chatdriver.setupReactiveMessageList (newmsg) =>
           @robot.logger.info "message receive callback"
-          user = @robot.brain.userForId 1, name: 'Shell', room: newmsg.rid
+          # use RocketChat schema for user for now
+          user = @robot.brain.userForId newmsg.u._id, name: newmsg.u.username, room: newmsg.rid
           text = new TextMessage(user, newmsg.msg, newmsg._id)
           @robot.receive text
     @emit 'connected'


update

update


Return an empty path instead of '.'

Return an empty path instead of '.'

--- src/nodes/file.coffee
+++ src/nodes/file.coffee
@@ -42,7 +42,9 @@
   # @return [String] the file path
   #
   getPath: ->
-    Path.dirname @getFullName()
+    path = Path.dirname @getFullName()
+    path = '' if path is '.'
+    path

   # Test if the file doesn't contain any top-level
   # methods and variables.


fix iteration logic

fix iteration logic

--- src/mkts-table.coffee
+++ src/mkts-table.coffee
@@ -409,7 +409,7 @@

 #-----------------------------------------------------------------------------------------------------------
 @_walk_pod_events = ( me, fieldhints_and_content_events ) ->
-  for [ field_designation, content, ] from @_walk_most_recent_field_designations me, fieldhints_and_content_events
+  for [ field_designation, content..., ] from @_walk_most_recent_field_designations me, fieldhints_and_content_events
     d           = me.pod_dimensions[ field_designation ]
     valign_tex  = @_get_valign_tex me, me.valigns[ field_designation ] ? me.valigns[ '*' ] ? 'center'
     halign_tex  = @_get_halign_tex me, me.haligns[ field_designation ] ? me.haligns[ '*' ] ? 'left'


Small fix for Blog model

Small fix for Blog model

--- src/model/Blog.coffee
+++ src/model/Blog.coffee
@@ -94,7 +94,6 @@
       attributes: ["login"]
     ]
     attributes: [
-      "userId"
       "title"
       "renderedContent"
       "createdAt"
@@ -107,7 +106,7 @@
       It's seems like there is no post with given slug: \"#{slug}\". Wrong url?
     "

-  postData.tags = (tag['tags.name'] for tag in postTagsData)
+  postData.tags = (tag["tags.name"] for tag in postTagsData)

   return postData


Added checking for rule existence in applyWithArgs and superApply

--- src/ometa-base.coffee
+++ src/ometa-base.coffee
@@ -165,6 +165,8 @@
     # note: _applyWithArgs and _superApplyWithArgs are not memoized, so they can't be left-recursive
     _applyWithArgs: (rule, args...) ->
       ruleFn = this[rule]
+      unless ruleFn?
+          throw 'tried to apply undefined rule "' + rule + '"'
       ruleFnArity = ruleFn.length
       # prepend "extra" arguments in reverse order
       for idx in [args.length - 1..ruleFnArity] by -1
@@ -176,6 +178,8 @@

     @_superApplyWithArgs: (recv, rule, args...) ->
       ruleFn = @prototype[rule]
+      unless ruleFn?
+          throw 'tried to apply undefined rule "' + rule + '"'
       ruleFnArity = ruleFn.length
       # prepend "extra" arguments in reverse order
       for idx in [args.length - 1...ruleFnArity] by -1


HComboBox pulldown widget styling

HComboBox pulldown widget styling

--- js/menus/combobox/combobox.coffee
+++ js/menus/combobox/combobox.coffee
@@ -37,14 +37,20 @@
         @menuItemView.drawRect()
       drawSubviews: ->
         @base()
+        ELEM.del(@markupElemIds.label)
+        delete @markupElemIds.label
         @setMarkupOfPart('bg','▾')
         @setStyleOfPart('bg',
           lineHeight: (@rect.height+2)+'px'
           verticalAlign: 'middle'
           borderLeft: '1px solid #ccc'
+          position: 'absolute'
+          fontSize: '13px'
+          textAlign: 'center'
+          backgroundColor: '#f9f9f9'
         )
       @refreshValue()
-    ).new( [null,-1,14,@rect.height,0,null], @, _menuOptions )
+    ).new( [null,1,14,@rect.height-2,1,null], @, _menuOptions )
     @_extraLabelRight += 14
     if @unitSuffix
       ELEM.setStyle(@unitSuffix.elemId,'paddingRight','14px')


fix specs

fix specs


Fix doc strings in Atom class

Fix doc strings in Atom class

--- src/atom.coffee
+++ src/atom.coffee
@@ -408,20 +408,19 @@
   # ## Examples
   #
   # ```coffee
-  #   atom.confirm
-  #     message: 'How you feeling?'
-  #     detailedMessage: 'Be honest.'
-  #     buttons:
-  #       Good: -> window.alert('good to hear')
-  #       Bad:  -> window.alert('bummer')
+  # atom.confirm
+  #   message: 'How you feeling?'
+  #   detailedMessage: 'Be honest.'
+  #   buttons:
+  #     Good: -> window.alert('good to hear')
+  #     Bad:  -> window.alert('bummer')
   # ```
   #
   # * `options` An {Object} with the following keys:
   #   * `message` The {String} message to display.
   #   * `detailedMessage` The {String} detailed message to display.
   #   * `buttons` Either an array of strings or an object where keys are
-  #               button names and the values are callbacks to invoke when
-  #               clicked.
+  #     button names and the values are callbacks to invoke when clicked.
   #
   # Returns the chosen button index {Number} if the buttons option was an array.
   confirm: ({message, detailedMessage, buttons}={}) ->


tuntisuunnitelma

tuntisuunnitelma

--- src/client.cjsx
+++ src/client.cjsx
@@ -27,33 +27,76 @@
         <h4 className="ui black header"> Creative teaching with MuSiika </h4>

-        MuSiika is a tool that combines music education with development of
-        pupils' eskills.
+        MuSiika is a tool that combines the music education with a development
+        of pupils' eskills.

         Why use Musiika?
         Musiika supports colaboration skills, creative thinking,

         How is that all possibly? Let us share an example
+
         Piece known as "Pachelbel's Canon" was composed by Johann Pachelbel in
         17th century. It belongs fundamentally to the history of music
-        representing the Baroque era. Moreover, it's elements can be found in
-        many pop music songs e.g. from chord progression. In respect of music
-        theory Pachelbel's Canon introduces the technique of canon and ground
-        bass.
+        representing the Baroque era. Moreover, it's elements (especially the
+        chord progression) can be found in many pop music song. In respect of
+        music theory Pachelbel's Canon introduces the technique of canon and
+        ground bass.

         We can use MuSiika to introduce this great piece of music in a manner
         that the pupils can really dive in the logics of music. The teacher can
         divide the class in groups. This supports peer learning and develops
         collaboration skills. Each group will compose Pachelbel's canon of
-        their own. MuSiika has been designed so that each pupil have their own
-        editor where they can compose melodies independently. Composing occurs
-        in terms of simple commands. One can program a set of notes and
-        test it by listening the melody created. it test it and when it's ready sent it to the teacher.
+        their own. MuSiika has been designed so that each pupil have an
+        editor where they can compose a part of their own independently. This
+        can be compared to an orchestra, one group member representing violins
+        another cellos. Composing occurs in terms of simple commands. One can
+        program a set of notes and listen the melody created. Ready version is
+        sent to the teacher. Teacher can combine different parts and play them
+        simultaneously. The symphony is ready!
+
+        The task can be performed at many levels.
+        1) Routine practice of musical notations and programming structures:
+          pupils can converts straight a sheet of music to commands that play
+          the music
+        2) pupils can create melodies of their own. Like in Pachelbel's canon
+        the teacher can set a chord progression that must be followed. That also
+        ensures that separately composed parts will play in harmony.
+
+        to  Each member has their own responsibility of their own part.
+
+

          This provides an environment of learning logical thinking,  programming conditions function

         Music production is strongly connected to technology. As the Even more in Music As the music techThe purpose of MuSiika is to enlarge the variety of teaching methods
+
+        Lesson plan: Music 60 min
+        Exercise given last week: find a piece that represents the Baroque style)
+
+        5 min
+        - Check the exercises
+        - Introduce of Pachelbel's canon in terms of Baroque music
+        15 min
+        - Introduce of the MuSiika tool
+          - Explanation of the basic commands and how does the cooperating work
+        10 min
+        - set up the groups
+          - Each group member should have a part of their own to compose
+        15 min
+        - Group working
+        10 min
+        - Pointing out the chord progression and ground bass, demonstration of
+          the use in pop music (e.g. Oasis - "Don't look back in Anger" )
+        5 min
+        - Exercise: Compose a new part to Pachelbel's Canon
+
+
+
+
+
+
+

       </div>
     </div>


Only keep the slugs for the meteoblue links. Fixes #3

--- src/data.coffee
+++ src/data.coffee
@@ -14,18 +14,18 @@

 data.REGION = {
-  bucegi:     {bbox: [25.36, 45.33, 25.58, 45.52], title: "Bucegi", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-bucegi_romania_683598"}
-  capatanii:  {bbox: [24.00, 45.17, 24.19, 45.34], title: "Căpățânii", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-c%C4%83p%C4%83%C5%A3%C3%A2nii_romania_682818"}
-  ceahlau:    {bbox: [25.85, 46.84, 26.08, 47.04], title: "Ceahlău", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/masivul-ceahl%C4%83u_romania_682481 "}
-  ciucas:     {bbox: [25.84, 45.43, 26.05, 45.56], title: "Ciucaș", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/ciucas_romania_7874266"}
-  crai:       {bbox: [25.15, 45.49, 25.30, 45.58], title: "Piatra Craiului", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/piatra-craiului_romania_670901"}
-  fagaras:    {bbox: [24.30, 45.47, 24.89, 45.73], title: "Făgăraș", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-f%C4%83g%C4%83ra%C5%9F_romania_678498"}
-  iezer:      {bbox: [24.85, 45.38, 25.10, 45.55], title: "Iezer", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-iezer_romania_675717"}
-  macin:      {bbox: [28.13, 45.07, 28.42, 45.29], title: "Măcin", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/muntii-macin_romania_7289808"}
-  nerei:      {bbox: [21.70, 44.80, 22.00, 45.03], title: "Cheile Nerei", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/44.946N21.863E"}
-  parang:     {bbox: [23.43, 45.31, 23.82, 45.41], title: "Parâng", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-par%C3%A2ng_romania_671333"}
-  piatramare: {bbox: [25.59, 45.48, 25.75, 45.60], title: "Piatra Mare", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/piatra-mare_romania_7874272"}
-  retezat:    {bbox: [22.72, 45.29, 23.00, 45.42], title: "Retezat", meteoLink: "https://www.meteoblue.com/en/weather/forecast/week/mun%C5%A3ii-retezatului_romania_668938"}
+  bucegi:     {bbox: [25.36, 45.33, 25.58, 45.52], title: "Bucegi", meteoLink: "mun%C5%A3ii-bucegi_romania_683598"}
+  capatanii:  {bbox: [24.00, 45.17, 24.19, 45.34], title: "Căpățânii", meteoLink: "mun%C5%A3ii-c%C4%83p%C4%83%C5%A3%C3%A2nii_romania_682818"}
+  ceahlau:    {bbox: [25.85, 46.84, 26.08, 47.04], title: "Ceahlău", meteoLink: "masivul-ceahl%C4%83u_romania_682481 "}
+  ciucas:     {bbox: [25.84, 45.43, 26.05, 45.56], title: "Ciucaș", meteoLink: "ciucas_romania_7874266"}
+  crai:       {bbox: [25.15, 45.49, 25.30, 45.58], title: "Piatra Craiului", meteoLink: "piatra-craiului_romania_670901"}
+  fagaras:    {bbox: [24.30, 45.47, 24.89, 45.73], title: "Făgăraș", meteoLink: "mun%C5%A3ii-f%C4%83g%C4%83ra%C5%9F_romania_678498"}
+  iezer:      {bbox: [24.85, 45.38, 25.10, 45.55], title: "Iezer", meteoLink: "mun%C5%A3ii-iezer_romania_675717"}
+  macin:      {bbox: [28.13, 45.07, 28.42, 45.29], title: "Măcin", meteoLink: "muntii-macin_romania_7289808"}
+  nerei:      {bbox: [21.70, 44.80, 22.00, 45.03], title: "Cheile Nerei", meteoLink: "44.946N21.863E"}
+  parang:     {bbox: [23.43, 45.31, 23.82, 45.41], title: "Parâng", meteoLink: "mun%C5%A3ii-par%C3%A2ng_romania_671333"}
+  piatramare: {bbox: [25.59, 45.48, 25.75, 45.60], title: "Piatra Mare", meteoLink: "piatra-mare_romania_7874272"}
+  retezat:    {bbox: [22.72, 45.29, 23.00, 45.42], title: "Retezat", meteoLink: "mun%C5%A3ii-retezatului_romania_668938"}
 }

 SLEEPING_PLACE = {
@@ -38,6 +38,7 @@
 SAVERAW = process.env.SAVERAW
 RETRY = +(process.env.RETRY or 0)

+METEOBLUE_LINK_ROOT = "https://www.meteoblue.com/en/weather/forecast/week/"

 osmid = (o) -> "#{o.type}/#{o.id}"

@@ -201,7 +202,7 @@
     compileOsm(bbox, result, dem)

   .then (result) ->
-    result.meteoLink = data.REGION[region].meteoLink
+    result.meteoLink = METEOBLUE_LINK_ROOT + data.REGION[region].meteoLink
     return result

   .then (db) ->


Show mines when we win

Show mines when we win

--- src/game.coffee
+++ src/game.coffee
@@ -114,7 +114,7 @@
   constructor: (id) ->
     @state = States.Empty
     @entities = Mine: 1
-    @nbMines = 50
+    @nbMines = 2
     @flags = 0
     @safe = 0
     @data = []
@@ -213,6 +213,7 @@

   win: () ->
     @state =States.Win
+    @showMines()
     background = @board.board.rect 0, 0, 42*19, 42*13
     background.attr fill: 'rgba(255, 255, 255, 0.7)'
     winLbl = @board.board.text 42*19/2, 42*13/2, 'Win'


Cleaning up the help file

Cleaning up the help file

--- src/help.coffee
+++ src/help.coffee
@@ -42,11 +42,11 @@
       "Start heating the primary (0th) extruder to 220 degrees celcius": "set e: 220"
       "Start heating the bed to 100 degrees celcius": "set b: 100"
       "Turn off the extruder's heater (unless it's bellow freezing)": "set e: 0"
-      # set temp e0:220 e1:0 b:100
-      # set temp 220
-
-      # set feedrate xy: 100 z: 1
-      # set fan on
+      # Proposed Additions:
+      #   set temp e0:220 e1:0 b:100
+      #   set temp 220
+      #   set feedrate xy: 100 z: 1
+      #   set fan on
   estop:
     description: """
       Emergency stop. Stop all dangerous printer activity


Widgets created with Hull.widget live into the default source

--- src/hull.coffee
+++ src/hull.coffee
@@ -4,7 +4,7 @@
     templates: _.extend {}, window.Hull?.templates
     widget: (widgetName, widgetDef)->
       widgetDef.type ?= "Hull"
-      define("__widget__$#{widgetName}@hull", widgetDef)
+      define("__widget__$#{widgetName}@default", widgetDef)
       return widgetDef

   hull = null


Fixed bug on HTable

Fixed bug on HTable

--- js/tables/table/table.coffee
+++ js/tables/table/table.coffee
@@ -92,7 +92,7 @@
         _width += _halfBorderWidth
       _top = _borderLeftOffset
       for _row, _rowNum in @_rows
-        @_bgCells << ELEM.make(
+        @_bgCells.push ELEM.make(
           _parent, 'div',
           styles:
             backgroundColor: @_cellBgColorOf(_rowNum,_colNum)


fix(composer): better html paste regex

fix(composer): better html paste regex

--- internal_packages/composer/lib/contenteditable-component.cjsx
+++ internal_packages/composer/lib/contenteditable-component.cjsx
@@ -678,7 +678,7 @@

   # This is used primarily when pasting text in
   _sanitizeHtml: (html) ->
-    cleanHtml = sanitizeHtml html.replace(/\n/g, "
"),
+    cleanHTML = sanitizeHtml html.replace(/[\n\r]/g, "
"),
       allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike']
       allowedAttributes:
         a: ['href', 'name']
@@ -698,11 +698,13 @@
     # We sanitized everything and convert all whitespace-inducing elements
     # into
 tags. We want to de-wrap
 tags and replace with two line
     # breaks instead.
-    cleanHTML = cleanHtml.replace(/
/gim, "").replace(/<\/p>/gi, "

")
+    cleanHTML = cleanHTML.replace(/
/gim, "").replace(/<\/p>/gi, "
")

     # We never want more then 2 line breaks in a row.
-    # https://regex101.com/r/gF6bF4/2
-    cleanHTML.replace(/(
\s*){3,}/g, "

")
+    # https://regex101.com/r/gF6bF4/4
+    cleanHTML = cleanHTML.replace(/(
\s*){3,}/g, "

")
+
+    return cleanHTML


fixed a bug where wrong song information was showing

--- jample/app/assets/javascripts/components/jample/index.js.cjsx
+++ jample/app/assets/javascripts/components/jample/index.js.cjsx
@@ -51,8 +51,8 @@
     # ---
     # generated by js2coffee 2.2.0
   render: ->
-    currentPatch = @state.patch_set.patches[@state.currentPatch]
-    currentTrack = @state.track_set[@state.currentPatch]
+    currentPatch = _.find(@state.patch_set.patches, (patch) => return (patch.patch_index == @state.currentPatch ))
+    currentTrack = currentPatch.track

@@ -337,7 +337,9 @@
         console.log(data)

   grid: ->
-    reversed_patches = @state.patch_set.patches
+    console.log("OKOK")
+    reversed_patches = _.sortBy(@state.patch_set.patches, (element) -> return parseInt(element.patch_index))
+    console.log(reversed_patches.slice(0,4))


Add getRoomId to adapter (#168)

Add getRoomId to adapter (#168)

--- src/rocketchat.coffee
+++ src/rocketchat.coffee
@@ -217,6 +217,9 @@
 			strings = strings.map (s) -> "@#{envelope.user.name} #{s}"
 		@send envelope, strings...

+	getRoomId: (room) =>
+		@chatdriver.getRoomId(room)
+
 	callMethod: (method, args...) =>
 		@chatdriver.callMethod(method, args)


fix async prog nesting bug

fix async prog nesting bug

--- src/rocketchat.coffee
+++ src/rocketchat.coffee
@@ -51,34 +51,36 @@
 									for result, idx in res
 										@robot.logger.info "Successfully joined room: #{rooms[idx]}"
 										subs.push @chatdriver.prepMeteorSubscriptions({uid: userid, roomid: rooms[idx]})
+
+									Q.all(subs)
+										.then(
+											(results) =>
+												@robot.logger.info "all subscriptions ready"
+												for result, idx in results
+													@robot.logger.info "Successfully subscribed to room: #{rooms[idx]}"
+
+
+												@chatdriver.setupReactiveMessageList (newmsg) =>
+													if newmsg.u._id isnt userid
+														curts = new Date(newmsg.ts.$date)
+														@robot.logger.info "Message receive callback id " + newmsg._id + " ts " + curts
+														@robot.logger.info "[Incoming] #{newmsg.u.username}: #{newmsg.msg}"
+
+														if curts > @lastts
+															@lastts = curts
+															user = @robot.brain.userForId newmsg.u._id, name: newmsg.u.username, room: newmsg.rid
+															text = new TextMessage(user, newmsg.msg, newmsg._id)
+
+															@robot.receive text
+															@robot.logger.info "Message sent to hubot brain."
+											(err) =>
+												@robot.logger.error "Unable to subscribe: #{err} Reason: #{err.reason}"
+										)
+
 								(err) =>
 									@robot.logger.error "Unable to Join room: #{err} Reason: #{err.reason}"
 							)

-						Q.all(subs)
-							.then(
-								(results) =>
-									@robot.logger.info "all subscriptions ready"
-									for result, idx in results
-										@robot.logger.info "Successfully subscribed to room: #{rooms[idx]}"
-
-
-									@chatdriver.setupReactiveMessageList (newmsg) =>
-										if newmsg.u._id isnt userid
-											curts = new Date(newmsg.ts.$date)
-											@robot.logger.info "Message receive callback id " + newmsg._id + " ts " + curts
-											@robot.logger.info "[Incoming] #{newmsg.u.username}: #{newmsg.msg}"
-
-											if curts > @lastts
-												@lastts = curts
-												user = @robot.brain.userForId newmsg.u._id, name: newmsg.u.username, room: newmsg.rid
-												text = new TextMessage(user, newmsg.msg, newmsg._id)
-
-												@robot.receive text
-												@robot.logger.info "Message sent to hubot brain."
-								(err) =>
-									@robot.logger.error "Unable to subscribe: #{err} Reason: #{err.reason}"
-							)

 					(err) =>
 						@robot.logger.error "Unable to Login: #{err} Reason: #{err.reason}"


Remove dead login checks

Remove dead login checks

--- maproulette/static/js/maproulette.coffee
+++ maproulette/static/js/maproulette.coffee
@@ -21,7 +21,6 @@
 selectedFeature = null

 # User variables
-loggedIn = false
 editor = ""
 difficulty = null
 location = null
@@ -322,9 +321,6 @@
   # This should be harmless if the dialog box is already closed
   dlgClose()

-  if not loggedIn
-    window.location.pathname = '/oauth/authorize'
-
   msg msgMovingOnToTheNextChallenge
   setDelay 1, msgClose()
   payload = {
@@ -342,9 +338,6 @@
   ###
   # Open the currently displayed OSM objects in the selected editor (e)
   ###
-  if not loggedIn
-    window.location.pathname = '/oauth/authorize'
-
   editor = e
   if map.getZoom() < 14
     msg msgZoomInForEdit, 3


Remove unused requires

Remove unused requires

--- src/init.coffee
+++ src/init.coffee
@@ -1,8 +1,6 @@
 path = require 'path'
-url = require 'url'

 optimist = require 'optimist'
-request = require 'request'

 Command = require './command'
 fs = require './fs'


verifying options exist

verifying options exist

--- bokehjs/src/coffee/common/ui_events.coffee
+++ bokehjs/src/coffee/common/ui_events.coffee
@@ -15,7 +15,7 @@

     hit_area = @get('hit_area')

-    unless options.testing
+    unless options and options.testing
       @hammer = new Hammer(hit_area[0])

       # This is to be able to distinguish double taps from single taps


Fix reference to user

Fix reference to user

--- collections/post.coffee
+++ collections/post.coffee
@@ -37,7 +37,7 @@
     ret

   author: ->
-    user = @user()
+    user = User.first(@userId)

     if user.profile and user.profile.firstName and user.profile.lastName
       return "#{user.profile.firstName} #{user.profile.lastName}"


remove debug

remove debug

--- tom.coffee
+++ tom.coffee
@@ -28,7 +28,6 @@
   $('#demo').click() if window.location.hash is "#demo"

   $('.run').click ->
-    console.log 'clicked run'
     template: $('.template').val()
     json: $.parseJSON $('.json').val()
     html: Mustache.to_html(template, json).replace(/^\s*/mg, '')


cleanup transform

cleanup transform


shorter var name

shorter var name

--- src/umobi.init.coffee
+++ src/umobi.init.coffee
@@ -1,10 +1,9 @@
-(->
+( (w,d)->
   # To optimize rendering performance, we append a ui-mobile-rendering class
   # to hide elements before rendering pages and adding classes to every
   # elements.
   uhtml = u('html')
   uhtml.children(0).addClass(['ui-mobile','ui-mobile-rendering'])
-
   u.ready ->
     # defaultHomeScroll = if not $.support.scrollTop or $( window ).scrollTop() is 1 then 0 else 1

@@ -12,17 +11,15 @@
     if umobi.config.enablePage
       umobi.page.init()
       # currently, if the page can not be scrolled, this won't work.
-      if window.navigator.userAgent.match(/iPhone|iPad|Android/)
-        d = document
+      if w.navigator.userAgent.match(/iPhone|iPad|Android/)
         hideAddressBar = ->
-          if d.documentElement.scrollHeight < (window.outerHeight / window.devicePixelRatio)
-            d.documentElement.style.height = (window.outerHeight / window.devicePixelRatio) + 'px'
-          window.scrollTo(0,1)
+          if d.documentElement.scrollHeight < (w.outerHeight / w.devicePixelRatio)
+            d.documentElement.style.height = (w.outerHeight / w.devicePixelRatio) + 'px'
+          w.scrollTo(0,1)
           # window.top.scrollTo(0,1)
-        window.addEventListener "load",hideAddressBar
-        window.addEventListener "orientationchange",hideAddressBar
+        w.addEventListener "load",hideAddressBar
+        w.addEventListener "orientationchange",hideAddressBar
         $(d).on 'pagereveal', hideAddressBar
-
     u.load -> uhtml.removeClass('ui-mobile-rendering')
-    u(document.body).addClass("ui-body-c")
-)()
+    u(d.body).addClass("ui-body-c")
+)(window,document)


Don't symlink /usr/bin/apm on Linux and Mac.

This makes us conflict with the official 'atom' Linux package. No bueno.
Doesn't really make sense for us to install this system-wide on Mac
either.

Fixes T3713.

--- build/tasks/build-task.coffee
+++ build/tasks/build-task.coffee
@@ -164,8 +164,6 @@
     cp 'static', path.join(appDir, 'static')

     cp path.join('apm', 'node_modules', 'atom-package-manager'), path.resolve(appDir, '..', 'new-app', 'apm'), filter: filterNodeModule
-    if process.platform isnt 'win32'
-      fs.symlinkSync(path.join('..', '..', 'bin', 'apm'), path.resolve(appDir, '..', 'new-app', 'apm', 'node_modules', '.bin', 'apm'))

     if process.platform is 'darwin'
       grunt.file.recurse path.join('build', 'resources', 'mac'), (sourcePath, rootDirectory, subDirectory='', filename) ->


Change 'before's to 'it's in new_view

Change 'before's to 'it's in new_view

--- test/integration/new_view.coffee
+++ test/integration/new_view.coffee
@@ -12,11 +12,11 @@
     # tial view. But I'm unsure.
     cleaner.clear_and_set_fixtures done

-  before (done) ->
+  it "goes to ehg's /datasets", (done) ->
     loginAndGo 'ehg', 'testing', "/datasets", done

   context 'when I click on an Apricot dataset', ->
-    before (done) ->
+    it 'clicks the Apricot dataset', (done) ->
         wd40.elementByPartialLinkText 'Apricot', (err, link) ->
           link.click done

@@ -26,15 +26,15 @@
         done()

     context 'when I click on "More tools" in the toolbar', ->
-      before (done) ->
+      it 'waits for the window', (done) ->
         setTimeout done, 500 # wait for window to get big enough!

-      before (done) ->
+      it 'waits for the chooser to appear', (done) ->
         wd40.click '#toolbar .new-view', ->
           browser.waitForElementByCss '#chooser .tool', 4000, done

       context 'when I click on the newview tool', ->
-        before (done) ->
+        it 'clicks the new-view tool and waits for it to appear', (done) ->
           wd40.click '.newview.tool', =>
             wd40.waitForInvisibleByCss '#chooser', (err) =>
               browser.url (err, url) =>
@@ -42,19 +42,19 @@
                 done()
             , 4000

-        before (done) ->
+        it 'waits for the new view page to load', (done) ->
           setTimeout done, 2000

         it 'takes me to the view page', (done) ->
           wd40.waitForMatchingURL new RegExp("#{base_url}/dataset/[^/]+/view/[^/]+"), done

     context 'when I click on "More tools" in the toolbar (again)', ->
-      before (done) ->
+      it 'brings up the chooser', (done) ->
         wd40.click '#toolbar .new-view', ->
           browser.waitForElementByCss '#chooser .tool', 4000, done

       context 'when I click on the newview tool', ->
-        before (done) ->
+        it 'selects the new view tool, waits for it to disappear', (done) ->
           wd40.click '.newview.tool', (err) =>
             wd40.waitForInvisibleByCss '#chooser', (err) =>
               browser.url (err, url) =>


test for hosting static folders

test for hosting static folders

--- test/lib/flirty/host_test.coffee
+++ test/lib/flirty/host_test.coffee
@@ -19,11 +19,5 @@
       request(@app).get('/index.txt')
         .expect "page 1", done

-  describe "When passed a folder and a password", ->
-    beforeEach ->
-      @app = Flirty.host fixtures, "somepass"

-    it "should require a login", (done)->
-      request(@app).get('/index.txt')
-        .expect(401, done)


overview: increase checkbox clickable-area

--- app/assets/javascripts/app/controllers/_application_controller_table.js.coffee
+++ app/assets/javascripts/app/controllers/_application_controller_table.js.coffee
@@ -248,10 +248,9 @@
     # enable checkbox bulk selection
     if data.checkbox

-      # click first tr>td, click checkbox / improve usability
+      # click first tr>td, catch click
       table.delegate('tr > td:nth-child(1)', event, (e) ->
         e.stopPropagation()
-        $(e.target).find('[name="bulk"]').click()
       )

       # bind on full bulk click


Workspace: Fixed modal width.

Workspace: Fixed modal width.

--- client/CollaborativeWorkspace/collaborativeworkspace.coffee
+++ client/CollaborativeWorkspace/collaborativeworkspace.coffee
@@ -217,6 +217,7 @@
       content          : "<p>#{content}</p>"
       cssClass         : "host-disconnected-modal"
       overlay          : no
+      width            : 470
       buttons          :
         Start          :
           title        : "Start New Session"


Add the same ones for Win32.

Add the same ones for Win32.

--- keymaps/merge-conflicts.cson
+++ keymaps/merge-conflicts.cson
@@ -35,3 +35,14 @@
   'ctrl-m d': 'merge-conflicts:detect'

 # Win32
+
+'.platform-win32 .editor.conflicted':
+  'ctrl-m down': 'merge-conflicts:next-unresolved'
+  'ctrl-m up': 'merge-conflicts:previous-unresolved'
+  'ctrl-m enter': 'merge-conflicts:accept-current'
+  'ctrl-m r': 'merge-conflicts:revert-current'
+  'ctrl-m 1': 'merge-conflicts:accept-ours'
+  'ctrl-m 2': 'merge-conflicts:accept-theirs'
+
+'.platform-win32 .workspace':
+  'ctrl-m d': 'merge-conflicts:detect'


Add more relevant (but contrived) example in init.coffee.

Closes #4089

--- dot-atom/init.coffee
+++ dot-atom/init.coffee
@@ -4,11 +4,8 @@
 # after packages are loaded/activated and after the previous editor state
 # has been restored.
 #
-# An example hack to make opened Markdown files always be soft wrapped:
+# An example hack to log to the console when each text editor is saved.
 #
-# path = require 'path'
-#
-# atom.workspaceView.eachEditorView (editorView) ->
-#   editor = editorView.getEditor()
-#   if path.extname(editor.getPath()) is '.md'
-#     editor.setSoftWrapped(true)
+# atom.workspace.observeTextEditors (editor) ->
+#   editor.onDidSave ->
+#     console.log "Saved! #{editor.getPath()}"


Tween: use floats are default values

Tween: use floats are default values

--- components/Tween.coffee
+++ components/Tween.coffee
@@ -71,9 +71,9 @@

   getTween: () ->
     return @tween if @tween
-    @startValue = 0
-    @stopValue = 1
-    @tween = new trident.Timeline(@obj)
+    @startValue = 0.0
+    @stopValue = 1.0
+    @tween = new trident.Timeline()
     @easing = @getEasing('linear')
     @tween.addEventListener('onpulse', (timeline, durationFraction, timelinePosition) =>
       return unless @outPorts.value.isAttached()


:lipstick: Sort requires

:lipstick: Sort requires

--- lib/installed-package-view.coffee
+++ lib/installed-package-view.coffee
@@ -1,9 +1,9 @@
 path = require 'path'

-{View} = require 'atom'
 _ = require 'underscore-plus'
 fs = require 'fs-plus'
 shell = require 'shell'
+{View} = require 'atom'

 PackageKeymapView = require './package-keymap-view'
 SettingsPanel = require './settings-panel'


:gift: ChangeOrder family operator Rotate and RotateBackwards

--- lib/operator-transform-string.coffee
+++ lib/operator-transform-string.coffee
@@ -640,6 +640,20 @@
   getNewList: (rows) ->
     rows.reverse()

+class Rotate extends ChangeOrder
+  @extend()
+  @registerToSelectList()
+  getNewList: (rows) ->
+    rows.unshift(rows.pop())
+    rows
+
+class RotateBackwards extends ChangeOrder
+  @extend()
+  @registerToSelectList()
+  getNewList: (rows) ->
+    rows.push(rows.shift())
+    rows
+
 class Sort extends ChangeOrder
   @extend()
   @registerToSelectList()


Advertise deprecated API

Advertise deprecated API

--- src/sockjs.coffee
+++ src/sockjs.coffee
@@ -180,6 +180,7 @@
         @srv = new Server(options)

     installHandlers: (http_server, handler_options) ->
+        @srv.options.log('info', 'You\'re using deprecated API, shame on you.')
         @srv.on 'connection', (conn) =>
             wrapped_conn = new DeprecatedConnectionWrapper(conn)
             @emit('open', wrapped_conn)


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/sprite.coffee
+++ src/sprite.coffee
@@ -18,6 +18,7 @@
     width: null
     height: null

+  # Hold
   spriteCache = {}

   Sprite = (image, sourceX, sourceY, width, height) ->


Double density of projected points around cluster edges.

--- packages/landsofillusions-assets/engine/mesh/projectclusterpoints.coffee
+++ packages/landsofillusions-assets/engine/mesh/projectclusterpoints.coffee
@@ -19,8 +19,30 @@
     plane = cluster.getPlane()
     horizon = cameraAngle.getHorizon plane.normal

-    # Start with all cluster pixels.
-    for pixelVertex in cameraAngle.projectPoints cluster.pixels, plane
+    # Start with cluster pixels.
+    pixels = []
+
+    for pixel in cluster.pixels
+      pixels.push pixel
+
+      # Also add connections between neighbors at half the distance, but only if it's an edge pixel.
+      allDirectionsAreSameCluster = true
+
+      for direction in pixelDirections
+        unless pixel[direction.property]?.cluster is cluster
+          allDirectionsAreSameCluster = false
+          break
+
+      continue if allDirectionsAreSameCluster
+
+      for direction in pixelDirections
+        continue unless pixel[direction.property]?.cluster is cluster
+
+        pixels.push
+          x: pixel.x + direction.vector.x * 0.5
+          y: pixel.y + direction.vector.y * 0.5
+
+    for pixelVertex in cameraAngle.projectPoints pixels, plane
       cluster.points.push
         vertex: pixelVertex
         type: LOI.Assets.Engine.Mesh.Cluster.PointTypes.Pixel
@@ -43,11 +65,9 @@
         # Use a factor to bring void point 0.5 pixels or more away from the horizon.
         factor = Math.min 1, distance - 0.5

-        voidPixel =
+        voidPixels.push
           x: pixel.x + direction.vector.x * factor
           y: pixel.y + direction.vector.y * factor
-
-        voidPixels.push voidPixel

     voidPointsStart = cluster.points.length

@@ -72,7 +92,20 @@
     for edge in cluster.edges
       line = edge.getLine3()

-      for edgeVertex in cameraAngle.projectPoints edge.vertices, plane, -0.5, -0.5
+      # Create edge vertices at double the density.
+      edgeVertices = []
+
+      for edgeVertex, index in edge.vertices
+        edgeVertices.push edgeVertex
+
+        if index < edge.vertices.length - 1
+          nextVertex = edge.vertices[index + 1]
+
+          edgeVertices.push
+            x: (edgeVertex.x + nextVertex.x) / 2
+            y: (edgeVertex.y + nextVertex.y) / 2
+
+      for edgeVertex in cameraAngle.projectPoints edgeVertices, plane, -0.5, -0.5
         line.closestPointToPoint edgeVertex, false, edgeVertex

         # Make sure this is not a duplicate of another edge point.


profile: wip date

profile: wip date

--- client/app/Applications/Activity.kdapplication/ContentDisplays/blogpost.coffee
+++ client/app/Applications/Activity.kdapplication/ContentDisplays/blogpost.coffee
@@ -76,20 +76,9 @@
 class StaticBlogPostListItem extends KDListItemView
   constructor:(options,data)->
     super options,data
-    # getBlogPosts = (blogPosts=[])->
-    #   posts = ""
-    #   for blog in blogPosts
-    @postDate = @getData().meta.createdAt
+    @postDate = new Date @getData().meta.createdAt
+    @postDate = @postDate.toLocaleString() # "dddd, mmmm dS, yyyy at h:MM:ss TT"
     # postDate = require('dateformat')(blog.meta.createdAt,"dddd, mmmm dS, yyyy at h:MM:ss TT")
-    #     posts+="""
-    #

-    #
#{blog.title}#{postDate}

-    #

-    #           #{blog.html}
-    #

-    #

-    #     """
-    #   posts

     @setClass 'content-item'


Use single quote for className attribute

--- client/app/lib/components/sidebarstacksection/sidebarstackheadersection.coffee
+++ client/app/lib/components/sidebarstacksection/sidebarstackheadersection.coffee
@@ -10,7 +10,7 @@

       <Link className='SidebarSection-headerTitle' href='/Stacks'>
         STACKS
-
+
       </Link>
       {@props.children}


Workspace: Added openUrl method to CollaborativePreviewPane.

--- client/landing/app/CommonViews/workspace/panes/collaborativepreviewpane.coffee
+++ client/landing/app/CommonViews/workspace/panes/collaborativepreviewpane.coffee
@@ -25,6 +25,10 @@
     value = snapshot.val()
     @previewer.openPath value.url  if value?.url

+  openUrl: (url) ->
+    @previewer.openPath url
+    @saveUrl yes
+
   saveUrl: (force) ->
     {path} = @previewer
     url    = unless force then path.replace(/\?.*/, "") else "#{path}?#{Date.now()}"