Fix typing when name clashing

Fix typing when name clashing

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -532,14 +532,6 @@
   isFinallyTyped: (name, scopeIndex) ->
     !!@finalType name, scopeIndex

-  declaredId: (name) ->
-    (@_declaration name)?.id
-
-  # TODO: use scope index instead and remove ids altogether
-  # Only valid for ids from current context, that is without module set
-  typeForId: (id) ->
-    @types[id]
-
   type: (name) ->
     (@_declaration name)?.type

@@ -1238,7 +1230,7 @@
     to = ctx.definitionPattern()
     ctx.setAssignTo (irDefinition expression.tea, translatedExpression,
       {name: ctx.definitionName(), scopeIndex: ctx.currentScopeIndex()})
-    {precs, assigns} = patternCompile ctx, to, expression, polymorphic
+    {precs, assigns, isMalformed} = patternCompile ctx, to, expression, polymorphic
     translationCache = ctx.resetAssignTo()

     #log "ASSIGN #{ctx.definitionName()}", ctx.shouldDefer()
@@ -1246,6 +1238,7 @@
       return deferCurrentDefinition ctx, expression

     return jsNoop() unless expression.tea
+    return jsNoop() if isMalformed

     if assigns.length is 0
       return malformed ctx, to, 'Not an assignable pattern'
@@ -1296,7 +1289,6 @@

   # Properly bind types according to the pattern
   if pattern.tea
-    # log "pattern", matched.tea, pattern.tea
     unify ctx, matched.tea.type, pattern.tea.type

   constraints = matched.tea.constraints
@@ -1307,31 +1299,34 @@
     # Name clash
     if (ctx.isFinallyDeclaredCurrentlyTyped name)
       malformed ctx, pattern, "#{name} is already declared"
-    else if not ctx.isCurrentlyDeclared name
-      # TODO: this is because functions might declare arity before being declared
-      ctx.declare name, id: id
-    if deps.length > 0
-      # log "adding top level lhs to deferred #{name}", deps
-      currentType = type#substitute ctx.substitution, type
-      ctx.addToDeferredBindings
-        name: scopedName name, ctx.currentScopeIndex()
-        id: name
-        scopeIndex: ctx.currentScopeIndex()
-        type: currentType
-        constraints: constraints
-        polymorphic: polymorphic
-        deps: deps#(map (({name}) -> name), deps)
-      # for dep in deps
-      #   ctx.addToDeferredBindings {name: dep.name, type: dep.type, reversed: name}
-      if not ctx.isTyped name
-        ctx.assignType name, (new TempType type)
-    else
-      # Ready for typing since there are no missing dependencies
-      deferredConstraints = inferType ctx, name, type, constraints, polymorphic
-      ctx.addToScopeConstraints deferredConstraints
+      isMalformed = yes
+    else
+      if not ctx.isCurrentlyDeclared name
+        # TODO: this is because functions might declare arity before being declared
+        ctx.declare name, id: id
+      if deps.length > 0
+        # log "adding top level lhs to deferred #{name}", deps
+        currentType = type#substitute ctx.substitution, type
+        ctx.addToDeferredBindings
+          name: scopedName name, ctx.currentScopeIndex()
+          id: name
+          scopeIndex: ctx.currentScopeIndex()
+          type: currentType
+          constraints: constraints
+          polymorphic: polymorphic
+          deps: deps#(map (({name}) -> name), deps)
+        # for dep in deps
+        #   ctx.addToDeferredBindings {name: dep.name, type: dep.type, reversed: name}
+        if not ctx.isTyped name
+          ctx.assignType name, (new TempType type)
+      else
+        # Ready for typing since there are no missing dependencies
+        deferredConstraints = inferType ctx, name, type, constraints, polymorphic
+        ctx.addToScopeConstraints deferredConstraints

   precs: precs ? []
   assigns: assigns ? []
+  isMalformed: isMalformed

 inferType = (ctx, name, type, constraints, polymorphic, scopeIndex) ->
   scopeIndex ?= ctx.currentScopeIndex()
@@ -1610,11 +1605,12 @@
           ctx.assignArity name, paramNames
         else
           documentation = extractDocs docs
-          ctx.preDeclare name,
-            arity: paramNames
-            id: ctx.definitionId()
-            type: ctx.preDeclaredType name # if any
-            docs: documentation
+          if not ctx.isFinallyCurrentlyDeclared name
+            ctx.preDeclare name,
+              arity: paramNames
+              id: ctx.definitionId()
+              type: ctx.preDeclaredType name # if any
+              docs: documentation

       newParamType = (param) ->
         param.scope = ctx.currentScopeIndex()
@@ -3187,7 +3183,9 @@
   # console.log (printType canonicalType)
   # console.log (JSON.stringify inferredType)
   # console.log (JSON.stringify type.type)
-  ctx.extendSubstitution matchType inferredType.type, type.type#(substitute ctx.substitution, type).type
+  #ctx.extendSubstitution
+  # Shouldn't fail
+  matchType inferredType.type, type.type#(substitute ctx.substitution, type).type
   # console.log "after", (printType inferredType.type)

   reduceConstraints ctx, inferredType.constraints
@@ -5674,8 +5672,8 @@
       throw new Error "Macro #{name} already defined"
     else
       addToMap topScope.macros, name, macro
-  for name, {type, arity, docs, isClass, virtual} of values definitions when shouldImport name
-    addToMap topScope, name, {type, arity, docs, isClass, virtual}
+  for name, {type, arity, docs, isClass, virtual, final} of values definitions when shouldImport name
+    addToMap topScope, name, {type, arity, docs, isClass, virtual, final}
   topScope.typeNames = concatMaps topScope.typeNames, typeNames
   topScope.classes = concatMaps topScope.classes, classes
   ctx.scopeIndex += compiledModule.savedScopes.length


Refactor and expand selection core mixin.

Harden select helper and add selectByClass helper.

--- src/js/jquery.extension.hlf.core.coffee
+++ src/js/jquery.extension.hlf.core.coffee
@@ -335,10 +335,16 @@
           @$el.trigger { type, userInfo }
       # `selection`, when given the context has a property `$el` and a property
       # `selectors`, define cached selector results for each name-selector pair.
+      # Also provide selection helpers for common tasks.
       selection: ->
         select: ->
           for own name, selector of @selectors
-            @["$#{name}"] = @$el.find selector
+            if (result = @$el.find selector)?
+              @["$#{name}"] = result
+        selectByClass: (className) ->
+          classNames = @options?.classNames
+          classNames ?= @classNames
+          @$el.find ".#{@classNames[className]}"

   # Export
   # ------


Declaring final for exporting and cloning origin from explicit declaration

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -535,6 +535,9 @@
     declaration.final = no
     @_declare name, declaration

+  declareAsFinal: (name) ->
+    (@_declarationInCurrentScope name).final = yes
+
   _declare: (name, declaration) ->
     declaration.id ?= @freshId()
     replaceOrAddToMap @_scope(), name, declaration
@@ -559,7 +562,7 @@
       undefined

   _declarationInScope: (i, name) ->
-    lookupInMap @scopes[i], name
+    (decl = (lookupInMap @scopes[i], name)) and not decl.isClass and decl

   freshId: ->
     @nameIndex++
@@ -1281,6 +1284,7 @@
           quantifyUnbound ctx, (addConstraints currentType, retainedConstraints)
         else
           toForAll currentType
+  ctx.declareAsFinal name
   if deferredConstraints
     ctx.addToScopeConstraints deferredConstraints

@@ -1439,7 +1443,9 @@
     imports name, names)

 exportAll = (ctx, definitions) ->
-  nonVirtual = filterMap ((name, declaration) -> not declaration.virtual), ctx._scope()
+  shouldBeExported = (name, declaration) ->
+    not declaration.virtual and declaration.final
+  nonVirtual = filterMap shouldBeExported, ctx._scope()
   exported = subtractSets nonVirtual, builtInDefinitions()
   exportList = map validIdentifier, (setToArray exported)
   (iife (concat [
@@ -1469,7 +1475,8 @@
       if name = ctx.isAtSimpleDefinition()
         # Explicit typing
         if type
-          explicitType = preDeclareExplicitlyTyped ctx, typeConstrainedCompile ctx, type
+          compiledType = typeConstrainedCompile ctx, type
+          explicitType = preDeclareExplicitlyTyped ctx, compiledType
           ctx.assignArity name, paramNames
         else
           ctx.preDeclare name,
@@ -1530,7 +1537,7 @@
               new Constrained (join body.tea.constraints, deferredConstraints),
                 markOrigin (typeFn paramTypeVars..., body.tea?.type), call
             else
-              freshInstance ctx, explicitType
+              (cloneOrigin (freshInstance ctx, explicitType), compiledType)
           # """λ#{paramNames.length}(function (#{listOf paramNames}) {
           #   #{compiledWheres}
           #   return #{compiledBody};
@@ -1791,6 +1798,7 @@
         if classConstraint
           ctx.addClass name, classConstraint, superClasses, freshedDeclarations
           declareMethods ctx, classConstraint, freshedDeclarations
+          ctx.declare name, isClass: yes

           translateDict name, (keysOfMap freshedDeclarations), superClassNames
         else
@@ -4392,6 +4400,18 @@
   nameIndex
   # suffix = if nameIndex >= 25 then freshName (Math.floor nameIndex / 25) - 1 else ''
   # (String.fromCharCode 97 + nameIndex % 25) + suffix
+
+cloneOrigin = (to, from) ->
+  if to.TypeApp
+    cloneOrigin to.op, from.op
+    cloneOrigin to.arg, from.arg
+  else if to.Constrained
+    cloneOrigin to.type, from.type
+    for c, i in to.constraints
+      cloneOrigin c, from.constraints[i]
+  else
+    markOrigin to, from.origin
+  to

 mapOrigin = (type, expression) ->
   if type.TypeApp


version

version

--- app/assets/javascripts/pinkman_base/router.coffee
+++ app/assets/javascripts/pinkman_base/router.coffee
@@ -139,17 +139,24 @@
     return(router)

+  @render: (r, callback) ->
+    Pinkman.state.initialize()
+    yieldIn = r.route.yieldIn() || @_config.yield
+    $(yieldIn).html("

") if r.route.blank
+    r.initialize()
+    window.scrollTo(0,0) unless Pinkman.router._config.freeze
+    callback() if typeof callback == 'function'
+    true
+
   # Search path throughout routes. On match, activate respective controllers: clears template and execute main(s) function(s)
   @activate: (path,callback) ->
     r = Pinkman.routes.match(path)
     if r? and r
-      Pinkman.state.initialize()
-      yieldIn = r.route.yieldIn() || @_config.yield
-      $(yieldIn).html("

") if r.route.blank
-      r.initialize()
-      window.scrollTo(0,0) unless Pinkman.router._config.freeze
-      callback() if typeof callback == 'function'
-      true
+      if @_config.transition? and typeof @_config.transition == 'function'
+        @_config.transition =>
+          @render(r,callback)
+      else
+        @render(r,callback)
     else
       false


change twitter url

change twitter url

change What's happening? for Share a link with your followers

--- app/assets/javascripts/social-share-button.coffee
+++ app/assets/javascripts/social-share-button.coffee
@@ -8,6 +8,7 @@
     title = encodeURIComponent($(el).parent().data('title') || '')
     img = encodeURIComponent($(el).parent().data("img") || '')
     url = encodeURIComponent($(el).parent().data("url") || '')
+    via = encodeURIComponent($(el).parent().data("via") || '')
     if url.length == 0
       url = encodeURIComponent(location.href)
     switch site
@@ -16,7 +17,7 @@
       when "weibo"
         SocialShareButton.openUrl("http://service.weibo.com/share/share.php?url=#{url}&type=3&pic=#{img}&title=#{title}")
       when "twitter"
-        SocialShareButton.openUrl("https://twitter.com/home?status=#{title}: #{url}")
+        SocialShareButton.openUrl("https://twitter.com/intent/tweet?url=#{url}&text=#{title}&via=#{via}")
       when "douban"
         SocialShareButton.openUrl("http://shuo.douban.com/!service/share?href=#{url}&name=#{title}&image=#{img}")
       when "facebook"


Map id to internalID

Map id to internalID

--- src/desktop/apps/user/components/auction_registrations/query.coffee
+++ src/desktop/apps/user/components/auction_registrations/query.coffee
@@ -6,7 +6,7 @@
           node {
             is_registered: isRegistered
             sale {
-              id
+              id: internalID
               name
               href
               start_at: startAt(format: "MMMM D, h:mmA")


Reorganize Selection into sections

Reorganize Selection into sections

--- src/selection.coffee
+++ src/selection.coffee
@@ -30,6 +30,9 @@
         @emitter.emit 'did-destroy'
         @emitter.dispose()

+  destroy: ->
+    @marker.destroy()
+
   ###
   Section: Event Subscription
   ###
@@ -60,36 +63,10 @@

     super

-  ###
-  Section: Methods
-  ###
-
-  destroy: ->
-    @marker.destroy()
-
-  finalize: ->
-    @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
-    if @isEmpty()
-      @wordwise = false
-      @linewise = false
-
-  clearAutoscroll: ->
-    @needsAutoscroll = null
-
-  # Public: Determines if the selection contains anything.
-  isEmpty: ->
-    @getBufferRange().isEmpty()
-
-  # Public: Determines if the ending position of a marker is greater than the
-  # starting position.
-  #
-  # This can happen when, for example, you highlight text "up" in a {TextBuffer}.
-  isReversed: ->
-    @marker.isReversed()
-
-  # Public: Returns whether the selection is a single line or not.
-  isSingleScreenLine: ->
-    @getScreenRange().isSingleLine()
+
+  ###
+  Section: Managing the selection range
+  ###

   # Public: Returns the screen {Range} for the selection.
   getScreenRange: ->
@@ -148,54 +125,60 @@
   getHeadBufferPosition: ->
     @marker.getHeadBufferPosition()

-  autoscroll: ->
-    @editor.scrollToScreenRange(@getScreenRange())
+  ###
+  Section: Info about the selection
+  ###
+
+  # Public: Determines if the selection contains anything.
+  isEmpty: ->
+    @getBufferRange().isEmpty()
+
+  # Public: Determines if the ending position of a marker is greater than the
+  # starting position.
+  #
+  # This can happen when, for example, you highlight text "up" in a {TextBuffer}.
+  isReversed: ->
+    @marker.isReversed()
+
+  # Public: Returns whether the selection is a single line or not.
+  isSingleScreenLine: ->
+    @getScreenRange().isSingleLine()

   # Public: Returns the text in the selection.
   getText: ->
     @editor.buffer.getTextInRange(@getBufferRange())
+
+  # Public: Identifies if a selection intersects with a given buffer range.
+  #
+  # * `bufferRange` A {Range} to check against.
+  #
+  # Returns a {Boolean}
+  intersectsBufferRange: (bufferRange) ->
+    @getBufferRange().intersectsWith(bufferRange)
+
+  intersectsScreenRowRange: (startRow, endRow) ->
+    @getScreenRange().intersectsRowRange(startRow, endRow)
+
+  intersectsScreenRow: (screenRow) ->
+    @getScreenRange().intersectsRow(screenRow)
+
+  # Public: Identifies if a selection intersects with another selection.
+  #
+  # * `otherSelection` A {Selection} to check against.
+  #
+  # Returns a {Boolean}
+  intersectsWith: (otherSelection, exclusive) ->
+    @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
+
+  ###
+  Section: Modifying the selected range
+  ###

   # Public: Clears the selection, moving the marker to the head.
   clear: ->
     @marker.setAttributes(goalBufferRange: null)
     @marker.clearTail() unless @retainSelection
     @finalize()
-
-  # Public: Modifies the selection to encompass the current word.
-  #
-  # Returns a {Range}.
-  selectWord: ->
-    options = {}
-    options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
-    if @cursor.isBetweenWordAndNonWord()
-      options.includeNonWordCharacters = false
-
-    @setBufferRange(@cursor.getCurrentWordBufferRange(options))
-    @wordwise = true
-    @initialScreenRange = @getScreenRange()
-
-  # Public: Expands the newest selection to include the entire word on which
-  # the cursors rests.
-  expandOverWord: ->
-    @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()))
-
-  # Public: Selects an entire line in the buffer.
-  #
-  # * `row` The line {Number} to select (default: the row of the cursor).
-  selectLine: (row=@cursor.getBufferPosition().row) ->
-    range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
-    @setBufferRange(@getBufferRange().union(range))
-    @linewise = true
-    @wordwise = false
-    @initialScreenRange = @getScreenRange()
-
-  # Public: Expands the newest selection to include the entire line on which
-  # the cursor currently rests.
-  #
-  # It also includes the newline character.
-  expandOverLine: ->
-    range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
-    @setBufferRange(range)

   # Public: Selects the text from the current cursor position to a given screen
   # position.
@@ -311,46 +294,45 @@
   selectToBeginningOfPreviousParagraph: ->
     @modifySelection => @cursor.moveToBeginningOfPreviousParagraph()

-  # Public: Moves the selection down one row.
-  addSelectionBelow: ->
-    range = (@getGoalBufferRange() ? @getBufferRange()).copy()
-    nextRow = range.end.row + 1
-
-    for row in [nextRow..@editor.getLastBufferRow()]
-      range.start.row = row
-      range.end.row = row
-      clippedRange = @editor.clipBufferRange(range)
-
-      if range.isEmpty()
-        continue if range.end.column > 0 and clippedRange.end.column is 0
-      else
-        continue if clippedRange.isEmpty()
-
-      @editor.addSelectionForBufferRange(range, goalBufferRange: range)
-      break
-
-  # FIXME: I have no idea what this does.
-  getGoalBufferRange: ->
-    if goalBufferRange = @marker.getAttributes().goalBufferRange
-      Range.fromObject(goalBufferRange)
-
-  # Public: Moves the selection up one row.
-  addSelectionAbove: ->
-    range = (@getGoalBufferRange() ? @getBufferRange()).copy()
-    previousRow = range.end.row - 1
-
-    for row in [previousRow..0]
-      range.start.row = row
-      range.end.row = row
-      clippedRange = @editor.clipBufferRange(range)
-
-      if range.isEmpty()
-        continue if range.end.column > 0 and clippedRange.end.column is 0
-      else
-        continue if clippedRange.isEmpty()
-
-      @editor.addSelectionForBufferRange(range, goalBufferRange: range)
-      break
+  # Public: Modifies the selection to encompass the current word.
+  #
+  # Returns a {Range}.
+  selectWord: ->
+    options = {}
+    options.wordRegex = /[\t ]*/ if @cursor.isSurroundedByWhitespace()
+    if @cursor.isBetweenWordAndNonWord()
+      options.includeNonWordCharacters = false
+
+    @setBufferRange(@cursor.getCurrentWordBufferRange(options))
+    @wordwise = true
+    @initialScreenRange = @getScreenRange()
+
+  # Public: Expands the newest selection to include the entire word on which
+  # the cursors rests.
+  expandOverWord: ->
+    @setBufferRange(@getBufferRange().union(@cursor.getCurrentWordBufferRange()))
+
+  # Public: Selects an entire line in the buffer.
+  #
+  # * `row` The line {Number} to select (default: the row of the cursor).
+  selectLine: (row=@cursor.getBufferPosition().row) ->
+    range = @editor.bufferRangeForBufferRow(row, includeNewline: true)
+    @setBufferRange(@getBufferRange().union(range))
+    @linewise = true
+    @wordwise = false
+    @initialScreenRange = @getScreenRange()
+
+  # Public: Expands the newest selection to include the entire line on which
+  # the cursor currently rests.
+  #
+  # It also includes the newline character.
+  expandOverLine: ->
+    range = @getBufferRange().union(@cursor.getCurrentLineBufferRange(includeNewline: true))
+    @setBufferRange(range)
+
+  ###
+  Section: Modifying the selected text
+  ###

   # Public: Replaces text at the current selection.
   #
@@ -390,71 +372,6 @@
       @editor.autoDecreaseIndentForBufferRow(newBufferRange.start.row)

     newBufferRange
-
-  # Public: Indents the given text to the suggested level based on the grammar.
-  #
-  # * `text` The {String} to indent within the selection.
-  # * `indentBasis` The beginning indent level.
-  normalizeIndents: (text, indentBasis) ->
-    textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()]
-    isCursorInsideExistingLine = /\S/.test(textPrecedingCursor)
-
-    lines = text.split('\n')
-    firstLineIndentLevel = @editor.indentLevelForLine(lines[0])
-    if isCursorInsideExistingLine
-      minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow())
-    else
-      minimumIndentLevel = @cursor.getIndentLevel()
-
-    normalizedLines = []
-    for line, i in lines
-      if i == 0
-        indentLevel = 0
-      else if line == '' # remove all indentation from empty lines
-        indentLevel = 0
-      else
-        lineIndentLevel = @editor.indentLevelForLine(lines[i])
-        indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis)
-
-      normalizedLines.push(@setIndentationForLine(line, indentLevel))
-
-    normalizedLines.join('\n')
-
-  # Indent the current line(s).
-  #
-  # If the selection is empty, indents the current line if the cursor precedes
-  # non-whitespace characters, and otherwise inserts a tab. If the selection is
-  # non empty, calls {::indentSelectedRows}.
-  #
-  # * `options` (optional) {Object} with the keys:
-  #   * `autoIndent` If `true`, the line is indented to an automatically-inferred
-  #     level. Otherwise, {Editor::getTabText} is inserted.
-  indent: ({ autoIndent }={}) ->
-    { row, column } = @cursor.getBufferPosition()
-
-    if @isEmpty()
-      @cursor.skipLeadingWhitespace()
-      desiredIndent = @editor.suggestedIndentForBufferRow(row)
-      delta = desiredIndent - @cursor.getIndentLevel()
-
-      if autoIndent and delta > 0
-        @insertText(@editor.buildIndentString(delta))
-      else
-        @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn()))
-    else
-      @indentSelectedRows()
-
-  # Public: If the selection spans multiple rows, indent all of them.
-  indentSelectedRows: ->
-    [start, end] = @getBufferRowRange()
-    for row in [start..end]
-      @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0
-
-  # Public: ?
-  setIndentationForLine: (line, indentLevel) ->
-    desiredIndentLevel = Math.max(0, indentLevel)
-    desiredIndentString = @editor.buildIndentString(desiredIndentLevel)
-    line.replace(/^[\t ]*/, desiredIndentString)

   # Public: Removes the first character before the selection if the selection
   # is empty otherwise it deletes the selection.
@@ -632,41 +549,109 @@
     @editor.createFold(range.start.row, range.end.row)
     @cursor.setBufferPosition([range.end.row + 1, 0])

-  modifySelection: (fn) ->
-    @retainSelection = true
-    @plantTail()
-    fn()
-    @retainSelection = false
-
-  # Sets the marker's tail to the same position as the marker's head.
-  #
-  # This only works if there isn't already a tail position.
-  #
-  # Returns a {Point} representing the new tail position.
-  plantTail: ->
-    @marker.plantTail()
-
-  # Public: Identifies if a selection intersects with a given buffer range.
-  #
-  # * `bufferRange` A {Range} to check against.
-  #
-  # Returns a {Boolean}
-  intersectsBufferRange: (bufferRange) ->
-    @getBufferRange().intersectsWith(bufferRange)
-
-  intersectsScreenRowRange: (startRow, endRow) ->
-    @getScreenRange().intersectsRowRange(startRow, endRow)
-
-  intersectsScreenRow: (screenRow) ->
-    @getScreenRange().intersectsRow(screenRow)
-
-  # Public: Identifies if a selection intersects with another selection.
-  #
-  # * `otherSelection` A {Selection} to check against.
-  #
-  # Returns a {Boolean}
-  intersectsWith: (otherSelection, exclusive) ->
-    @getBufferRange().intersectsWith(otherSelection.getBufferRange(), exclusive)
+  # Public: Indents the given text to the suggested level based on the grammar.
+  #
+  # * `text` The {String} to indent within the selection.
+  # * `indentBasis` The beginning indent level.
+  normalizeIndents: (text, indentBasis) ->
+    textPrecedingCursor = @cursor.getCurrentBufferLine()[0...@cursor.getBufferColumn()]
+    isCursorInsideExistingLine = /\S/.test(textPrecedingCursor)
+
+    lines = text.split('\n')
+    firstLineIndentLevel = @editor.indentLevelForLine(lines[0])
+    if isCursorInsideExistingLine
+      minimumIndentLevel = @editor.indentationForBufferRow(@cursor.getBufferRow())
+    else
+      minimumIndentLevel = @cursor.getIndentLevel()
+
+    normalizedLines = []
+    for line, i in lines
+      if i == 0
+        indentLevel = 0
+      else if line == '' # remove all indentation from empty lines
+        indentLevel = 0
+      else
+        lineIndentLevel = @editor.indentLevelForLine(lines[i])
+        indentLevel = minimumIndentLevel + (lineIndentLevel - indentBasis)
+
+      normalizedLines.push(@setIndentationForLine(line, indentLevel))
+
+    normalizedLines.join('\n')
+
+  # Indent the current line(s).
+  #
+  # If the selection is empty, indents the current line if the cursor precedes
+  # non-whitespace characters, and otherwise inserts a tab. If the selection is
+  # non empty, calls {::indentSelectedRows}.
+  #
+  # * `options` (optional) {Object} with the keys:
+  #   * `autoIndent` If `true`, the line is indented to an automatically-inferred
+  #     level. Otherwise, {Editor::getTabText} is inserted.
+  indent: ({ autoIndent }={}) ->
+    { row, column } = @cursor.getBufferPosition()
+
+    if @isEmpty()
+      @cursor.skipLeadingWhitespace()
+      desiredIndent = @editor.suggestedIndentForBufferRow(row)
+      delta = desiredIndent - @cursor.getIndentLevel()
+
+      if autoIndent and delta > 0
+        @insertText(@editor.buildIndentString(delta))
+      else
+        @insertText(@editor.buildIndentString(1, @cursor.getBufferColumn()))
+    else
+      @indentSelectedRows()
+
+  # Public: If the selection spans multiple rows, indent all of them.
+  indentSelectedRows: ->
+    [start, end] = @getBufferRowRange()
+    for row in [start..end]
+      @editor.buffer.insert([row, 0], @editor.getTabText()) unless @editor.buffer.lineLengthForRow(row) == 0
+
+  setIndentationForLine: (line, indentLevel) ->
+    desiredIndentLevel = Math.max(0, indentLevel)
+    desiredIndentString = @editor.buildIndentString(desiredIndentLevel)
+    line.replace(/^[\t ]*/, desiredIndentString)
+
+  ###
+  Section: Managing multiple selections
+  ###
+
+  # Public: Moves the selection down one row.
+  addSelectionBelow: ->
+    range = (@getGoalBufferRange() ? @getBufferRange()).copy()
+    nextRow = range.end.row + 1
+
+    for row in [nextRow..@editor.getLastBufferRow()]
+      range.start.row = row
+      range.end.row = row
+      clippedRange = @editor.clipBufferRange(range)
+
+      if range.isEmpty()
+        continue if range.end.column > 0 and clippedRange.end.column is 0
+      else
+        continue if clippedRange.isEmpty()
+
+      @editor.addSelectionForBufferRange(range, goalBufferRange: range)
+      break
+
+  # Public: Moves the selection up one row.
+  addSelectionAbove: ->
+    range = (@getGoalBufferRange() ? @getBufferRange()).copy()
+    previousRow = range.end.row - 1
+
+    for row in [previousRow..0]
+      range.start.row = row
+      range.end.row = row
+      clippedRange = @editor.clipBufferRange(range)
+
+      if range.isEmpty()
+        continue if range.end.column > 0 and clippedRange.end.column is 0
+      else
+        continue if clippedRange.isEmpty()
+
+      @editor.addSelectionForBufferRange(range, goalBufferRange: range)
+      break

   # Public: Combines the given selection into this selection and then destroys
   # the given selection.
@@ -683,6 +668,10 @@
     @setBufferRange(@getBufferRange().union(otherSelection.getBufferRange()), options)
     otherSelection.destroy()

+  ###
+  Section: Comparing to other selections
+  ###
+
   # Public: Compare this selection's buffer range to another selection's buffer
   # range.
   #
@@ -691,8 +680,42 @@
   # * `otherSelection` A {Selection} to compare against
   compare: (otherSelection) ->
     @getBufferRange().compare(otherSelection.getBufferRange())
+
+  ###
+  Section: Private Utilities
+  ###

   screenRangeChanged: ->
     @emit 'screen-range-changed', @getScreenRange()
     @emitter.emit 'did-change-range'
     @editor.selectionRangeChanged(this)
+
+  finalize: ->
+    @initialScreenRange = null unless @initialScreenRange?.isEqual(@getScreenRange())
+    if @isEmpty()
+      @wordwise = false
+      @linewise = false
+
+  autoscroll: ->
+    @editor.scrollToScreenRange(@getScreenRange())
+
+  clearAutoscroll: ->
+    @needsAutoscroll = null
+
+  modifySelection: (fn) ->
+    @retainSelection = true
+    @plantTail()
+    fn()
+    @retainSelection = false
+
+  # Sets the marker's tail to the same position as the marker's head.
+  #
+  # This only works if there isn't already a tail position.
+  #
+  # Returns a {Point} representing the new tail position.
+  plantTail: ->
+    @marker.plantTail()
+
+  getGoalBufferRange: ->
+    if goalBufferRange = @marker.getAttributes().goalBufferRange
+      Range.fromObject(goalBufferRange)


fix #270

fix #270

Issue was data_range1d incorrectly computing max and min of the data
when there were some columns that were nested arrays (patches, etc). Use
_.flatten(), also simplifies code.

--- bokehjs/src/coffee/range/data_range1d.coffee
+++ bokehjs/src/coffee/range/data_range1d.coffee
@@ -14,19 +14,10 @@
         sourceobj = @resolve_ref(source['ref'])
         for colname in source['columns']
           columns.push(sourceobj.getcolumn(colname))
-      columns = _.reduce(columns, ((x, y) -> return x.concat(y)), [])
+      columns = _.flatten(columns)
       columns = _.filter(columns, (x) -> typeof(x) != "string")
-      if not _.isArray(columns[0])
-        columns = _.reject(columns, (x) -> isNaN(x))
-        [min, max] = [_.min(columns), _.max(columns)]
-      else
-        maxs = Array(columns.length)
-        mins = Array(columns.length)
-        for i in [0..columns.length-1]
-          columns[i] = _.reject(columns[i], (x) -> isNaN(x))
-          maxs[i] = _.max(columns[i])
-          mins[i] = _.min(columns[i])
-        [min, max] = [_.min(mins), _.max(maxs)]
+      columns = _.reject(columns, (x) -> isNaN(x))
+      [min, max] = [_.min(columns), _.max(columns)]
       if max != min
         span = (max - min) * (1 + @get('rangepadding'))
       else


Unnecessary.

Unnecessary.

--- server/migrations/0039-annotation-body.coffee
+++ server/migrations/0039-annotation-body.coffee
@@ -21,8 +21,6 @@

         count += collection.update {_schema: currentSchema, _id: document._id, body: document.body}, {$set: {body: newBody, _schema: newSchema}}

-        document.body = newBody
-
     counts = super
     counts.migrated += count
     counts.all += count


Remove unused mixin

Remove unused mixin

--- app/assets/javascripts/mixins/file_upload_progress.js.coffee
+++ app/assets/javascripts/mixins/file_upload_progress.js.coffee
@@ -1,23 +0,0 @@
-ETahi.FileUploadProgress = Ember.Mixin.create
-  isUploading: false
-  isProcessing: false
-  uploadProgress: 0
-  showProgress: true
-
-  actions:
-    uploadStarted: ->
-      @set('isUploading', true)
-
-    uploadProgress: (data) ->
-      progress = Math.round(data.loaded * 100 / data.total)
-      @set('uploadProgress', progress)
-      if progress >= 100
-        @setProperties(showProgress: false, isProcessing: true)
-
-    uploadError: (message) ->
-      @set('uploadError', message)
-
-    uploadFinished: (data) ->
-      @set('isUploading', false)
-      @set('completed', true)
-      @super(data)


Adjust user reloading, to ensure password is always empty

--- app/assets/javascripts/sprangular/controllers/account.coffee
+++ app/assets/javascripts/sprangular/controllers/account.coffee
@@ -2,15 +2,15 @@
   Status.pageTitle = 'My Account'

   user = Account.user
-  user.password = ''
-  user.password_confirmation = ''

   $scope.editing = false
   $scope.user = user

   refreshAccount = ->
     Account.init().then ->
-      $scope.user = Account.user
+      user = Account.user
+      user.password = ''
+      user.password_confirmation = ''

   $scope.edit = ->
     $scope.editing = true


clear modal window forms when closing

clear modal window forms when closing

--- app/assets/javascripts/admin/admin_in_place_editor.js.coffee
+++ app/assets/javascripts/admin/admin_in_place_editor.js.coffee
@@ -21,14 +21,17 @@
     $('.modal .modal-footer .save-button').click () ->
       $(@).closest('.modal').find('form').submit()
     $('.modal').on 'hidden', () =>
-      @clearModalForm($(@))
+      $('.modal.hide.fade').each((idx, element) =>
+        @clearModalForm($(element))
+      )
     @initModals()
     @initSearchTypeahead()
     $("[rel='tooltip']").tooltip()

-  clearModalForm: (modal) ->
-    form = modal.find('form')
-    form[0].reset() if form.length > 0
+  clearModalForm: (modalElement) ->
+    modalElement.find('form').each(() ->
+      @reset()
+    )

   initForm: () ->
     $(".datepicker").datepicker


Improved task size calculation.

Improved task size calculation.

--- app/assets/javascripts/app/controllers/task_widget.js.coffee
+++ app/assets/javascripts/app/controllers/task_widget.js.coffee
@@ -215,11 +215,11 @@
     elementsOversizeLeftTotal = 0
     $('#task .task').each(
       (position, element) ->
-        widthTask = $(element).parent().width()
+        widthTask = $(element).width()
         if widthTask > task_size
           elementsOversize++
         else
-          elementsOversizeLeftTotal += ( width / task_count ) - widthTask
+          elementsOversizeLeftTotal += task_size - widthTask
     )

     addOversize = elementsOversizeLeftTotal / elementsOversize


Header update on start, needed for safari.

--- app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
+++ app/assets/javascripts/app/controllers/ticket_zoom.js.coffee
@@ -151,6 +151,9 @@
     @render(force)

   positionPageHeaderStart: =>
+
+    # init header update needed for safari, scroll event is fired
+    @positionPageHeaderUpdate()

     # scroll is also fired on window resize, if element scroll is changed
     @main.bind(


clean-up old waterfall

clean-up old waterfall

--- app/assets/javascripts/lib/views/waterfall_chart_view.coffee
+++ app/assets/javascripts/lib/views/waterfall_chart_view.coffee
@@ -23,12 +23,14 @@
     series = @model.series.map (serie) =>
       present = serie.present_value()
       future = serie.future_value()
-      if serie.get('group') == 'value'
-        # Take only the present value, as group == value queries only future/present
-        # ?! - PZ
-        if @scale_units() then Metric.scale_value(present, scale) else present
+      # There is a conflict between code and documentation. If the serie's
+      # group is set to 'value' then we're interested in the gquery value,
+      # otherwise in the difference future-present
+      val = if serie.get('group') == 'value'
+        future
       else
-        if @scale_units() then Metric.scale_value(future, scale) else future
+        future - present
+      if @scale_units() then Metric.scale_value(val, scale) else val
     [series]

   scale_units: => @model.get('unit') != 'PJ'


Workspace: Refactored @ to this.

Workspace: Refactored @ to this.

--- client/app/CommonViews/workspace/panel/panel.coffee
+++ client/app/CommonViews/workspace/panel/panel.coffee
@@ -57,11 +57,11 @@
     if pane
       newPane = @createPane pane
       @container.addSubView newPane
-      @getDelegate().emit "AllPanesAddedToPanel", @, [newPane]
+      @getDelegate().emit "AllPanesAddedToPanel", this, [newPane]
     else if layout
       @layoutContainer = new WorkspaceLayout
-        delegate      : @
-        layoutOptions : layout
+        delegate       : this
+        layoutOptions  : layout

       @container.addSubView @layoutContainer
     else
@@ -79,7 +79,7 @@

   getPaneClass: (paneOptions) ->
     paneType             = paneOptions.type
-    paneOptions.delegate = @
+    paneOptions.delegate = this

     if paneType is "custom"
       PaneClass = paneOptions.paneClass
@@ -121,7 +121,7 @@

   viewAppended: ->
     super
-    @getDelegate().emit "NewPanelAdded", @
+    @getDelegate().emit "NewPanelAdded", this

   pistachio: ->
     """


ide: correct binding of save error event handler

--- client/ide/lib/workspace/panes/ideeditorpane.coffee
+++ client/ide/lib/workspace/panes/ideeditorpane.coffee
@@ -33,8 +33,7 @@
       kd.getSingleton('appManager').tell 'IDE', 'handleFileDeleted', file

     @errorOnSave = no
-    @saveErrorCallback = @bound 'handleSaveFailed'
-    file.on [ 'fs.save.failed', 'fs.saveAs.failed' ], @saveErrorCallback
+    file.on [ 'fs.save.failed', 'fs.saveAs.failed' ], @bound 'handleSaveFailed'

     @once 'RealtimeManagerSet', @bound 'setContentFromCollaborativeString'
     @once 'RealtimeManagerSet', @bound 'listenCollaborativeStringChanges'
@@ -409,4 +408,4 @@

   destroy: ->

-    @file.off [ 'fs.save.failed', 'fs.saveAs.failed' ], @saveErrorCallback
+    @file.off [ 'fs.save.failed', 'fs.saveAs.failed' ], @bound 'handleSaveFailed'


Refactoring: Return before creating change object.

--- client/ide/lib/workspace/panes/ideeditorpane.coffee
+++ client/ide/lib/workspace/panes/ideeditorpane.coffee
@@ -295,10 +295,9 @@

   handleFileContentChange: ->

+    return  if @dontEmitChangeEvent
+
     change = @getInitialChangeObject()
-
-    return  if @dontEmitChangeEvent
-
     change.type = 'ContentChange'
     change.context.file.content = @getContent()


shortcuts: remove duplicate entry

shortcuts: remove duplicate entry

--- client/ide/views/shortcutsview/shortcutsview.coffee
+++ client/ide/views/shortcutsview/shortcutsview.coffee
@@ -77,7 +77,6 @@
       'Go to previous line'    : 'Ctrl-P'
       'Go to left'             : 'Ctrl-B'
       'Go to right'            : 'Ctrl-F'
-      'Go to end'              : { Others: 'Ctrl-End',       Apple: 'Meta-Down' }
       'Go to line'             : 'Meta-G'
       'Go to line'             : 'Meta-L'
       'Go to line end'         : { Others: 'Alt-Right',      Apple: 'Meta-Right'}


Simplify nodes update attributes methods & update relation direction when changes

--- app/frontend/javascripts/views/visualization-graph.js.coffee
+++ app/frontend/javascripts/views/visualization-graph.js.coffee
@@ -82,9 +82,12 @@
     #!!! We need to arr node_type changes
     #@collection.nodes.bind 'change:node_type',   @onNodeChangeType, @
     #@collection.relations.bind 'add',             @onRelationsChange, @
-    @collection.relations.bind 'change:source_id',  @onRelationsChange, @
-    @collection.relations.bind 'change:target_id',  @onRelationsChange, @
-    @collection.relations.bind 'remove',            @onRelationsRemove, @
+    @collection.relations.bind 'change:source_id',      @onRelationsChangeNode, @
+    @collection.relations.bind 'change:target_id',      @onRelationsChangeNode, @
+    # By now we don't apply any visual style based on relation_type
+    #@collection.relations.bind 'change:relation_type',  @onRelationsChangeAttribute, @
+    @collection.relations.bind 'change:direction',      @onRelationsChangeAttribute, @
+    @collection.relations.bind 'remove',                @onRelationsRemove, @
     # Subscribe Canvas Events
     Backbone.on 'visualization.node.showInfo',    @onNodeShowInfo, @
     Backbone.on 'visualization.node.hideInfo',    @onNodeHideInfo, @
@@ -121,12 +124,14 @@

   onNodeChangeName: (node) ->
     console.log 'onNodeChangeName', node.attributes.name
-    @visualizationGraphCanvas.updateNodeName node.attributes, node.attributes.name
+    # Update nodes labels
+    @visualizationGraphCanvas.updateLabels()
+    # Update Panel Info name
     @updateGraphInfoNode node

   onNodeChangeDescription: (node) ->
     console.log 'onNodeChangeDescription', node.attributes.description
-    @visualizationGraphCanvas.updateNodeDescription node.attributes, node.attributes.description
+    # Update Panel Info description
     @updateGraphInfoNode node

   onNodeChangeVisible: (node) ->
@@ -148,7 +153,7 @@
     if @visualizationGraphInfo.isVisible() and @visualizationGraphInfo.node.id == node.id
       @visualizationGraphInfo.hide()

-  onRelationsChange: (relation) ->
+  onRelationsChangeNode: (relation) ->
     console.log 'onRelationsChange', relation
     # check if we have both source_id and target_id
     if relation.attributes.source_id and relation.attributes.target_id
@@ -157,6 +162,9 @@
       # Add relation
       @visualizationGraphCanvas.addRelation relation.attributes
       @visualizationGraphCanvas.updateLayout()
+
+  onRelationsChangeAttribute: (relation) ->
+    @visualizationGraphCanvas.updateRelations()

   onRelationsRemove: (relation) ->
     @visualizationGraphCanvas.removeRelation relation.attributes


Troubleshoot: terminal is registered for troubleshooting

--- client/Main/troubleshoot/troubleshoot.coffee
+++ client/Main/troubleshoot/troubleshoot.coffee
@@ -29,6 +29,7 @@
         brokerKite     :
           osKite       :
             vm         : 0
+            terminal   : 0
         bongo          :
           broker       :
             liveUpdate : 0
@@ -91,6 +92,9 @@
     @registerItem "vm",
       speedCheck   : no
       troubleshoot : vmChecker.bound 'healthCheck'
+
+    @registerItem "terminal",
+      troubleshoot : vmChecker.bound 'terminalHealthCheck'

   registerConnections: ->
     #register connection


Don't expand border on selection and use more consistent selected color


flux/teams: add action types

flux/teams: add action types

--- client/app/lib/flux/teams/actiontypes.coffee
+++ client/app/lib/flux/teams/actiontypes.coffee
@@ -19,6 +19,8 @@
   'UPDATE_TEAM_MEMBER'
   'DELETE_TEAM_MEMBER'
   'SAVE_DISABLED_TEAM_MEMBER'
+  'LOAD_DISABLED_MEMBERS'
+  'REMOVE_ENABLED_MEMBER'

   'LOAD_PENDING_INVITATION_BEGIN'
   'LOAD_PENDING_INVITATION_SUCCESS'


chat: add a method to get video container

--- client/ide/lib/views/chat/idechatview.coffee
+++ client/ide/lib/views/chat/idechatview.coffee
@@ -93,6 +93,9 @@
     @unsetClass 'loading'

+  getVideoContainer: -> @chatVideoView.getContainer()
+
+
   createChatVideoView: ->

     @chatVideoView = new IDEChatVideoView { cssClass: 'hidden' }


EditorPane: Don't send content on file content changed event.

--- client/ide/workspace/panes/editorpane.coffee
+++ client/ide/workspace/panes/editorpane.coffee
@@ -104,7 +104,6 @@

     ace.on 'FileContentChanged', =>
       change.type = 'ContentChange'
-      change.context.content = @getContent()

       @emit 'ChangeHappened', change

@@ -118,7 +117,6 @@
     {paneType} = @getOptions()
     {machine}  = file

-
     {name, path } = file
     {label, ipAddress, slug, uid} = machine


Spec only needs underscore

Spec only needs underscore

--- test/exercise/collection.spec.coffee
+++ test/exercise/collection.spec.coffee
@@ -1,5 +1,4 @@
-{Testing, expect, sinon, _, ReactTestUtils} = require 'openstax-react-components/test/helpers'
-
+_ = require 'underscore'
 Collection = require 'exercise/collection'
 step = require '../../api/steps/4571/GET'


Removed the activity feed's cog icon. It was useless

--- client/Social/Activity/ContentDisplays/ActivityTicker.coffee
+++ client/Social/Activity/ContentDisplays/ActivityTicker.coffee
@@ -88,7 +88,7 @@
   pistachio:
     """

-
Activity Feed

+
Activity Feed

       {{> @listView}}

     """


ActivityTicker: activity ticker item views are cached

--- client/Social/Activity/ContentDisplays/ActivityTicker.coffee
+++ client/Social/Activity/ContentDisplays/ActivityTicker.coffee
@@ -26,6 +26,10 @@
     group.on "FollowHappened", @bound "addFollow"
     group.on "PostIsCreated", @bound "addActivity"
     group.on "ReplyIsAdded", @bound "addComment"
+    @listController.listView.on 'ItemWasAdded', (view, index) =>
+      if view.getData()?
+        itemId = @getItemId view.data
+        @itemsIndexed[itemId] = view

   getConstructorName :(obj)->
     if obj and obj.bongo_ and obj.bongo_.constructorName
@@ -179,16 +183,15 @@
     """

   addNewItem: (newItem, index) ->
-    {source, target, object, as} = newItem
-
-    itemId = "#{source.getId()}_#{target.getId()}_#{as}_#{object?.getId()}"
+    itemId = @getItemId newItem

     if not @itemsIndexed[itemId]
-      @itemsIndexed[itemId] = newItem
       if index? then @listController.addItem newItem, index
       else @listController.addItem newItem
     else
-      foundItem = @itemsIndexed[itemId]
-      viewItem = @listController.itemForId foundItem._id
+      viewItem = @itemsIndexed[itemId]
       @listController.moveItemToIndex viewItem, 0

+  getItemId: (item) ->
+    {source, target, object, as} = item
+    "#{source.getId()}_#{target.getId()}_#{as}_#{object?.getId()}"


minor changes for hometeamsettingscomponent files

--- client/home/lib/myteam/hometeamsettingscomponent/test.coffee
+++ client/home/lib/myteam/hometeamsettingscomponent/test.coffee
@@ -8,7 +8,7 @@
 Encoder            = require 'htmlencode'

-describe.only 'HomeTeamSettings', ->
+describe 'HomeTeamSettings', ->

   { createRenderer,
   renderIntoDocument,


Hackathon: added judges description

Hackathon: added judges description

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -489,6 +489,7 @@

Judges

+
Because we expect a large number of applicants, we wanted to ensure that we have a good number of awesome judges to help evaluate the incoming projects. These amazing folks have agreed to become part of our event. We cannot thank them enough.


eta reduction

eta reduction

--- code/serv.coffee
+++ code/serv.coffee
@@ -80,5 +80,4 @@
         mkdir /opt/cobalt/etc/sshkeys/#{box_name}
         """
   # insecure - sanitise box_name
-  exec cmd, (err, stdout, stderr) ->
-    callback err, stdout, stderr
+  exec cmd, callback


mocks cleanup in test_pce

mocks cleanup in test_pce

--- test/test_pce.coffee
+++ test/test_pce.coffee
@@ -5,22 +5,23 @@
 kTestFilename = 'test.log'

 describe 'ProcessingChainEntrance', ->
+  setup_mocking()
+
   beforeEach (done) ->
-    @journal = sinon.mock(new Journal(kTestFilename))
-    @replication = sinon.mock({start: (->), send: (->)})
-    @engine = sinon.mock(new TradeEngine())
-    @pce = new ProcessingChainEntrance(@engine.object, @journal.object, @replication.object)
-    done()
+    @journal = new Journal(kTestFilename)
+    @replication = {start: (->), send: (->)}
+    @engine = new TradeEngine()

-  afterEach (done) ->
-    @journal.verify()
-    @replication.verify()
-    @engine.verify()
+    @mockify 'journal'
+    @mockify 'replication'
+    @mockify 'engine'
+
+    @pce = new ProcessingChainEntrance(@engine, @journal, @replication)
     done()

   it 'should intialize the transaction log and replication when starting', (done) ->
-    @journal.expects('start').once().returns(then: ->)
-    @replication.expects('start').once().returns(then: ->)
+    @_journal.expects('start').once().returns(then: ->)
+    @_replication.expects('start').once().returns(then: ->)

     @pce.start()
     done()
@@ -31,9 +32,9 @@

     operation = {kind: "TEST"}
     messageJson = JSON.stringify(operation)
-    @journal.expects('record').once().withArgs(messageJson).returns(deferred.promise)
-    @replication.expects('send').once().withArgs(messageJson).returns(deferred.promise)
-    @engine.expects('execute_operation').once().withArgs(operation).returns("success")
+    @_journal.expects('record').once().withArgs(messageJson).returns(deferred.promise)
+    @_replication.expects('send').once().withArgs(messageJson).returns(deferred.promise)
+    @_engine.expects('execute_operation').once().withArgs(operation).returns("success")

     onComplete = (result) ->
       result.should.equal "success"
@@ -45,9 +46,9 @@
     deferred = Q.defer()
     deferred.resolve(undefined)

-    @journal.expects('record').once().returns(deferred.promise)
-    @replication.expects('send').once().returns(deferred.promise)
-    @engine.expects('execute_operation').once().throws("failure")
+    @_journal.expects('record').once().returns(deferred.promise)
+    @_replication.expects('send').once().returns(deferred.promise)
+    @_engine.expects('execute_operation').once().throws("failure")

     onError = (error) ->
       error.name.should.equal "failure"


add german lang stuff for calendar

add german lang stuff for calendar

--- client/controllers/calendar.coffee
+++ client/controllers/calendar.coffee
@@ -26,6 +26,16 @@
 	$scope.eventSources = [closedEvents, eventEvents, jubiEvents]
 	$scope.calendarConfig =
 		calendar:
+			dayNames: ["Sonntag", "Montag", "Dienstag", "Mittwoch", "Donnerstag", "Freitag", "Samstag"]
+			dayNamesShort: ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"]
+			monthNames: ["Januar","Februar","März","April","Mai","Juni","Juli","August","September","Oktober","November","Dezember"]
+			monthNamesShort: ["Jan","Feb","Mär","Apr","Mai","Jun","Jul","Aug","Sep","Okt","Nov","Dez"]
+			buttonText:
+				today:    'Heute'
+				month:    'Monat'
+				week:     'Woche'
+				day:      'Tag'
+			firstDay: 1
 			height: 500
 			editable: false
 			timeFormat: 'H(:mm)'


Tests: Teams: Fixed broken selector.

Tests: Teams: Fixed broken selector.

--- client/test/lib/teams/teams.coffee
+++ client/test/lib/teams/teams.coffee
@@ -73,7 +73,7 @@
     providerSelector      = "#{modalSelector} .stack-onboarding .provider-selection"
     machineSelector       = "#{providerSelector} .providers"
     stackPreview          = "#{modalSelector} .stack-preview"
-    codeSelector          = "#{stackPreview} .lang-coffee"
+    codeSelector          = "#{stackPreview} .has-markdown"
     footerSelector        = "#{modalSelector} .stacks .footer"
     nextButtonSelector    = "#{footerSelector} button.next"
     awsSelector           = "#{machineSelector} .aws"
@@ -155,4 +155,4 @@
       .click                  cancelButton
       .waitForElementVisible  pendingMemberView, 20000
       .assert.containsText    pendingMemberView, userEmail
-      .end()+      .end()


change menu height on JSMenuView

change menu height on JSMenuView

--- JSKit/04_JSMenuView.coffee
+++ JSKit/04_JSMenuView.coffee
@@ -26,7 +26,7 @@
         @_div = "

"
         menustr = ""
         for disp in @_menuitem
-            menustr += "
"+disp+"
"
+            menustr += "
"+disp+"
"
         @_div = @_div.replace(//, menustr)

         if ($(@_viewSelector+"_menu").length)
@@ -36,7 +36,7 @@
         $(@_viewSelector+"_menu").css("left", "0px")
         $(@_viewSelector+"_menu").css("top", "0px")
         $(@_viewSelector+"_menu").css("width", @_frame.size.width+"px")
-        $(@_viewSelector+"_menu").css("height", (@_frame.size.height * @_menuitem.length)+"px")
+        $(@_viewSelector+"_menu").css("height", ((@_frame.size.height + 4) * @_menuitem.length)+"px")
         $(@_viewSelector+"_menu").css("position", "absolute")
         $(@_viewSelector+"_menu").css("overflow", "visible")
         $(@_viewSelector+"_menu").css("font-size", @_textSize+"pt")


Reverted unrelated change

Reverted unrelated change

--- core/app/backbone/modules/non_confirmed_email_warning.coffee
+++ core/app/backbone/modules/non_confirmed_email_warning.coffee
@@ -3,7 +3,7 @@
   return if currentSession.user().get('confirmed')
   return if currentSession.user().justCreated()

-  Factlink.notificationCenter.notice """
+  Factlink.notificationCenter.error """
     Your email address has not yet been confirmed, please check your email inbox.
     <a href="/users/confirmation/new" target="_blank">
       Resend email.


fixed broken panel specs (the addition of the span in panel title)

--- spec/javascripts/mercury/panel_spec.js.coffee
+++ spec/javascripts/mercury/panel_spec.js.coffee
@@ -22,7 +22,7 @@
       html = $('
').html(@panel.element).html()
       expect(html).toContain('class="mercury-panel loading"')
       expect(html).toContain('style="display:none;"')
-      expect(html).toContain('
foo panel

')
+      expect(html).toContain('
foo panel

')
       expect(html).not.toContain('class="mercury-panel-close"')

     it "appends to any element", ->
@@ -159,16 +159,14 @@
     it "sets the element html to be the data passed to it", ->
       @panel.loadContent('hello world!')
       html = @panel.element.html()
-      expect(html).toContain('
foo panel
')
       expect(html).toContain('class="mercury-panel-pane"')
       expect(html).toContain('style="visibility: hidden;')
       expect(html).toContain('hello world!')

-    it "sets the element html to be the data passed to it", ->
+    it "sets the element html to be the data passed to it -- translated", ->
       Mercury.config.localization.enabled = true
       @panel.loadContent('hello world!')
       html = @panel.element.html()
-      expect(html).toContain('
foo panel
')
       expect(html).toContain('class="mercury-panel-pane"')
       expect(html).toContain('style="visibility: hidden;')
       expect(html).toContain('bork! bork!')


enable choose activity automatically

enable choose activity automatically

--- coffee/services/dataAdapter.coffee
+++ coffee/services/dataAdapter.coffee
@@ -319,8 +319,9 @@
     setActivities: (url, activities) ->
       if not url? or not activities? then return
       @_data[url].activities = activities
-      if @selectedAccount and @selectedAccount.url is url
+      if @selectedTicket and @selectedTicket.url is url
         @_activities.set activities
+        @_selectedActivity = activities[0]
       Log.debug("setActivities: #{url}")

     ###*


Undo a formatting change that was causing unnecessary merge conflicts

--- SingularityUI/app/controllers/DeployDetail.coffee
+++ SingularityUI/app/controllers/DeployDetail.coffee
@@ -2,7 +2,7 @@

 DeployDetails           = require '../models/DeployDetails'
 DeployHistoricalTasks   = require '../collections/DeployHistoricalTasks'
-DeployActiveTasks       = require '../collections/DeployActiveTasks'
+DeployActiveTasks   = require '../collections/DeployActiveTasks'
 HealthCheckResult       = require '../models/HealthCheckResult'
 DeployTasksHealthChecks = require '../collections/DeployTasksHealthChecks'


Integration tests for initial git commit and readme url

--- integration_test/cobalt.coffee
+++ integration_test/cobalt.coffee
@@ -84,17 +84,21 @@
         settings.publish_token.should.match /[0-9a-z]{15}/
         done()

-    it 'I can see my README.md', (done) ->
-      ssh_cmd "ls ~/README.md", (err, stdout, stderr) ->
+    it 'I have a README.md with the box URL in', (done) ->
+      ssh_cmd "cat ~/README.md", (err, stdout, stderr) ->
         should.not.exist err
         stderr.should.be.empty
-        done()
-
-    it 'I can see my git repo', (done) ->
-      ssh_cmd "git status", (err, stdout, stderr) ->
+        stdout.should.include "https://box.scraperwiki.com/#{boxname}"
+        done()
+
+    it 'I have a git repo with README.md committed', (done) ->
+      ssh_cmd "git log", (err, stdout, stderr) ->
         should.not.exist err
         stderr.should.be.empty
-        done()
+        ssh_cmd "git show README.md", (err, stdout, stderr) ->
+          should.not.exist err
+          stderr.should.be.empty
+          done()

     it "I cannot see any other box (i.e. I'm chrooted)", (done) ->
       ssh_cmd "ls -di", (err, stdout, stderr) ->
@@ -155,7 +159,7 @@
             qs:
               apikey: cobalt_api_key
         request.get options, (err, resp, body) ->
-          body.should.match /This is the README\.md file/
+          body.should.include "# ScraperWiki Box: #{boxname} #"
           resp.should.have.status 200
           done()


Restore HoverTool.ttmodels as a computed property

--- bokehjs/src/coffee/models/tools/inspectors/hover_tool.coffee
+++ bokehjs/src/coffee/models/tools/inspectors/hover_tool.coffee
@@ -331,9 +331,7 @@
     @add_dependencies('computed_renderers', this, ['renderers', 'names', 'plot'])
     @add_dependencies('computed_renderers', @plot, ['renderers'])

-  @getters {
-    computed_renderers: () -> @_get_computed('computed_renderers')
-    ttmodels: () ->
+    @define_computed_property 'ttmodels', () ->
       ttmodels = {}
       tooltips = @tooltips

@@ -347,6 +345,11 @@
           ttmodels[r.id] = tooltip

       return ttmodels
+    @add_dependencies('ttmodels', this, ['computed_renderers', 'tooltips'])
+
+  @getters {
+    computed_renderers: () -> @_get_computed('computed_renderers')
+    ttmodels: () -> @_get_computed('ttmodels')
     synthetic_renderers: () -> _.values(@ttmodels)
   }


rm unused follow button on feed view

rm unused follow button on feed view

--- components/feed/client/feed.coffee
+++ components/feed/client/feed.coffee
@@ -11,7 +11,6 @@
 FeedItem = require '../models/feed_item.coffee'
 FeedItemView = require('./feed_item.coffee').FeedItemView
 feedItemsTemplate = -> require('../templates/feed_items.jade') arguments...
-FollowProfileButton = require '../../../apps/partners/client/follow_profiles_button.coffee'
 feedItemsContainerTemplate = -> require('../templates/feed_items_container.jade') arguments...

 module.exports = class FeedView extends Backbone.View
@@ -152,14 +151,6 @@
     @lastItem = @$('.feed-item:last')
     @handleDoneFetching?()

-  initFollowButton: (profile, el) ->
-    new FollowProfileButton
-      el: el
-      model: profile
-      collection: @followProfiles
-      analyticsFollowMessage: @analyticsFollowMessage
-      analyticsUnfollowMessage: @analyticsUnfollowMessage
-
   infiniteScroll: (scrollTop) ->
     @scrollTop = scrollTop || @$window.scrollTop()


Temporarily disable io.balena tag reservation tests

--- tests/integration/models/tags.coffee
+++ tests/integration/models/tags.coffee
@@ -135,7 +135,10 @@
 		promise = model.set(@resource.id, 'io.resin.test', 'secret')
 		m.chai.expect(promise).to.be.rejectedWith('Tag keys beginning with io.resin. are reserved.')

-	it 'should not allow creating a balena tag', ->
+	# This should be implemented in time for the balena API release, but
+	# we want to be able to release the SDK before that happens
+	# After the rename is complete, we should un-skip this
+	it.skip 'should not allow creating a balena tag', ->
 		promise = model.set(@resource.id, 'io.balena.test', 'secret')
 		m.chai.expect(promise).to.be.rejectedWith('Tag keys beginning with io.balena. are reserved.')


Prefer relative paths with ghc-modi<5.5.0.0


CUI.ListViewTreeNode: Add method setChildren(children).

--- src/elements/ListView/ListViewTreeNode.coffee
+++ src/elements/ListView/ListViewTreeNode.coffee
@@ -446,6 +446,10 @@
 		else
 			@father.addNode(node, idx)

+	setChildren: (@children) ->
+		@initChildren()
+		return
+
 	initChildren: ->
 		for node, idx in @children
 			for _node, _idx in @children


WIP: select links in the sidebar for reading using keyboard shortcuts.

Currently implemented for the start link and feed links. Still unimplemented for folder links.

--- app/assets/javascripts/ng-services/ng-KeyboardShortcutsSvc.js.coffee
+++ app/assets/javascripts/ng-services/ng-KeyboardShortcutsSvc.js.coffee
@@ -3,16 +3,18 @@
 ########################################################

 angular.module('feedbunch').service 'keyboardShortcutsSvc',
-['$rootScope', 'highlightedEntrySvc', 'highlightedSidebarLinkSvc','entrySvc',
-($rootScope, highlightedEntrySvc, highlightedSidebarLinkSvc, entrySvc)->
+['$rootScope', 'highlightedEntrySvc', 'highlightedSidebarLinkSvc','entrySvc', 'startPageSvc', 'currentFeedSvc',
+'findSvc',
+($rootScope, highlightedEntrySvc, highlightedSidebarLinkSvc, entrySvc, startPageSvc, currentFeedSvc,
+findSvc)->

   #---------------------------------------------
   # Start responding to keyboard shortcuts
   #---------------------------------------------
   start: ->
     $(document).keypress (event)->
-      # spacebar=32, j=106, k=107, h=104, l=108
-      if event.which in [32, 104, 108, 106, 107]
+      # enter=13, spacebar=32, j=106, k=107, h=104, l=108
+      if event.which in [13, 32, 104, 108, 106, 107]
         # If a keyboard shortcut is used, stop highlighting entries on mousover until user actually moves the mouse.
         # This prevents accidental mouseover events when autoscrolling.
         $rootScope.mouseover_highlight_disabled = true
@@ -23,12 +25,32 @@

         # shortcuts related to entries
         if $rootScope.entries
+          # next entry
           highlightedEntrySvc.next() if event.which == 106
+
+          # previous entry
           highlightedEntrySvc.previous() if event.which == 107
+
+          # toggle open/close state of entry
           entrySvc.toggle_open_entry highlightedEntrySvc.get() if event.which == 32

+        # next link in sidebar
         highlightedSidebarLinkSvc.next() if event.which == 108
+
+        # previous link in sidebar
         highlightedSidebarLinkSvc.previous() if event.which == 104
+
+        # select the currently highlighted link (feed or folder) in the sidebar for reading
+        if event.which == 13
+          highlighted_link = highlightedSidebarLinkSvc.get()
+          if highlighted_link.id == 'start'
+            startPageSvc.show_start_page()
+          else if highlighted_link.type == 'feed'
+            feed = findSvc.find_feed highlighted_link.id
+            currentFeedSvc.set feed
+          else if highlighted_link.type == 'folder'
+            # TODO
+            alert 'todo'

         event.preventDefault()


Social/Client: if unread count is not set check for unreadRepliesCount

--- client/Social/Activity/sidebar/sidebaritem.coffee
+++ client/Social/Activity/sidebar/sidebaritem.coffee
@@ -27,7 +27,12 @@
       @getDelegate().emit 'ItemShouldBeSelected', this
       @lastClickedTimestamp = Date.now()

-    @once 'viewAppended', => @setUnreadCount @getData().unreadCount
+
+    # `unreadRepliesCount` is used for pinned messages, pinned message list is the
+    # only place where we are displaying message items, for other places (my
+    # feeds and private messages) they are channels and channels have their
+    # unread count in the data as `unreadCount`
+    @once 'viewAppended', => @setUnreadCount @getData().unreadCount or @getData().unreadRepliesCount

   setUnreadCount: (unreadCount = 0) ->


reset invitation form after clicking away

--- client/landing/app/MainApp/login/loginview.coffee
+++ client/landing/app/MainApp/login/loginview.coffee
@@ -274,6 +274,10 @@
     @setY -@getSingleton('windowController').winHeight

     cb = =>
+      @requestForm.email.show()
+      @requestForm.button.show()
+      @$('.flex-wrapper').removeClass 'expanded'
+
       @emit "LoginViewHidden"
       @hidden = yes
       callback?()


fix multiplication logic

fix multiplication logic

--- coffeescript/004-webworkers/multiplication.coffee
+++ coffeescript/004-webworkers/multiplication.coffee
@@ -9,5 +9,5 @@
 ], (_, multi) ->

     multi.onmessage = (e) ->
-        result = _.reduce(e.data, (memo, elem) -> memo * elem, 1)
+        result = _.reduce(_.values(e.data), (memo, elem) -> memo * elem, 1)
         postMessage(result)


social: destroy add participant and settings buttons for bot message pane

--- client/activity/lib/views/privatemessage/kodingbotmessagepane.coffee
+++ client/activity/lib/views/privatemessage/kodingbotmessagepane.coffee
@@ -12,7 +12,8 @@
     super options, data

     @listController.getListView().setClass 'kdlistview-privatemessage'
-
+    @actionsMenu?.destroy()
+    @participantHeads?.newParticipantButton?.destroy()

   fetch: (options = {}, callback) ->


Remove MemberFollowToggleButton from Members.AppView

--- client/landing/app/Applications/Members.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Members.kdapplication/AppView.coffee
@@ -161,66 +161,3 @@
         KD.getSingleton('contentDisplayController').emit "ContentDisplayWantsToBeHidden", @

     @listenWindowResize()
-
-
-class MemberFollowToggleButton extends KDToggleButton
-
-  constructor:(options = {}, data)->
-    options = $.extend
-      title           : "Follow"
-      dataPath        : "followee"
-      defaultState    : "Follow"
-      loader          :
-        color         : "#333333"
-        diameter      : 18
-      states          : [
-        title         : "Follow"
-        callback      : (callback)->
-          KD.requireMembership
-            callback  : =>
-              @getData().follow (err, response)=>
-                @hideLoader()
-                unless err
-                  @getData().followee = yes
-                  @setClass 'following-btn'
-                  KD.track "Members", "Follow", @getData().profile.nickname
-                  callback? null
-            tryAgain  : yes
-            onFailMsg : 'Login required to follow members'
-            onFail    : => @hideLoader()
-      ,
-        title         : "Unfollow"
-        callback      : (callback)->
-          @getData().unfollow (err, response)=>
-            @hideLoader()
-            unless err
-              @getData().followee = no
-              @unsetClass 'following-btn'
-              KD.track "Members", "Unfollow", @getData().profile.nickname
-              callback? null
-      ]
-    , options
-
-    super options, data
-
-    decorator = (following)=>
-      if @getData().followee
-        @setClass 'following-btn'
-        @setState "Unfollow"
-      else
-        @setState "Follow"
-        @unsetClass 'following-btn'
-
-    if @getData().followee?
-      decorator @getData().followee
-    else
-      KD.whoami().isFollowing? @getData().getId(), "JAccount", \
-        (err, following) =>
-          @getData().followee = following
-          warn err  if err and KD.isLoggedIn()
-          decorator following
-
-  decorateState:(name)->
-
-    @setClass 'following-btn' if name is 'Unfollow'
-    super


Code cleanup.

Code cleanup.

--- client/landing/app/Applications/WebTerm.kdapplication/AppView.coffee
+++ client/landing/app/Applications/WebTerm.kdapplication/AppView.coffee
@@ -23,39 +23,6 @@
       @container.$().scrollLeft 0
     @addSubView @container

-    @sessionBox = new KDView
-      cssClass: "kddialogview"
-      position:
-        top: 10
-        left: 10
-    @sessionBox.hide()
-    @addSubView @sessionBox
-
-    label = new KDLabelView
-      title: "Select session:"
-    @sessionBox.addSubView label
-
-    @sessionList = new KDListView
-      itemClass: WebTermSessionItem
-    @sessionBox.addSubView @sessionList
-
-    label = new KDLabelView
-      title: "Create session with name:"
-    @sessionBox.addSubView label
-
-    sessionNameInput = new KDInputView
-      label: label
-      defaultValue: (new Date).format "yyyy-mm-dd HH:MM:ss"
-    @sessionBox.addSubView sessionNameInput
-
-    createSessionButton = new KDButtonView
-      title: "Create"
-      callback: =>
-        @sessionBox.hide()
-        @terminal.createSession sessionNameInput.getValue()
-        @setKeyView()
-    @sessionBox.addSubView createSessionButton
-
     @terminal = new WebTerm.Terminal @container.$()

     if @options.advancedSettings
@@ -74,24 +41,7 @@

     @terminal.sessionEndedCallback = (sessions) =>
       @emit "WebTerm.terminated"
-      return

-      @server.getSessions (sessions) =>
-        keys = Object.keys sessions
-        keys.sort (a, b) ->
-          if sessions[a] < sessions[b]
-            -1
-          else if sessions[a] > sessions[b]
-            1
-          else
-            0
-        @sessionList.empty()
-        for key in keys
-          @sessionList.addItem
-            id: parseInt(key)
-            name: sessions[key]
-            mainView: this
-        @sessionBox.show()
     @terminal.setTitleCallback = (title) =>
       #@tabPane.setTitle title

@@ -228,23 +178,7 @@
     @terminal.windowDidResize()

   getAdvancedSettingsMenuItems:->
-
     settings      :
       type        : 'customView'
       view        : new WebtermSettingsView
         delegate  : @
-
-
-class WebTermSessionItem extends KDListItemView
-  constructor: (options = {},data) ->
-    super options, data
-
-  partial: (data) ->
-    link = $(document.createElement("a"))
-    link.text data.name
-    link.attr "href", "#"
-    link.bind "click", (event) =>
-      data.mainView.sessionBox.hide()
-      data.mainView.terminal.joinSession data.id
-      event.preventDefault()
-    link


Update show inquiry modal test

Update show inquiry modal test

--- components/contact/test/show_inquiry_modal.coffee
+++ components/contact/test/show_inquiry_modal.coffee
@@ -17,7 +17,7 @@
       @ShowInquiryView = benv.require resolve __dirname, '../show_inquiry_modal'
       @ContactView = @ShowInquiryView.__get__ 'ContactView'
       sinon.stub @ContactView::, 'onSubmit'
-      sinon.stub @ContactView::, 'submit'
+      sinon.stub @ShowInquiryView::, 'openInquiryQuestionnaire'
       sinon.stub @ShowInquiryView::, 'initialize'
       @view = new @ShowInquiryView
       @view.show = new Backbone.Model fabricate 'show'
@@ -29,19 +29,19 @@

   afterEach ->
     benv.teardown()
-    @ContactView::submit.restore()
+    @ShowInquiryView::openInquiryQuestionnaire.restore()
     @ContactView::onSubmit.restore()
     @ShowInquiryView::initialize.restore()

   describe '#submit', ->

-    it 'submits an inquiry about the show', ->
+    it 'opens the inquiry questionnaire an inquiry about the show', ->
       @view.show.set id: 'foo-gallery-show'
       @view.onSubmit { preventDefault: -> }
       @view.model.toJSON().inquireable_id.should.equal 'foo-gallery-show'
       @view.model.toJSON().inquireable_type.should.equal 'partner_show'
       @view.model.toJSON().contact_gallery.should.equal true
-      @ContactView::submit.called.should.be.ok()
+      @ShowInquiryView::openInquiryQuestionnaire.called.should.be.ok()

   describe '#renderLocation', ->


Made naming more consistent: showController > openController

To be consisten with closeController

--- core/app/backbone/factlink/base_controller.coffee
+++ core/app/backbone/factlink/base_controller.coffee
@@ -16,11 +16,11 @@

   getRouteFunction: (name) ->
     (args...) ->
-      @showController()
+      @openController()
       @onAction() if @onAction?
       @[name](args...)

-  showController: ->
+  openController: ->
     return if @started
     FactlinkApp.vent.trigger 'controller:switch'
     @bindTo FactlinkApp.vent, 'controller:switch', @closeController, this


Parse custom tags on item fields to span tags

--- api/routes/items.coffee
+++ api/routes/items.coffee
@@ -4,6 +4,31 @@
 _ = require 'lodash'
 # Not in 2.x, which is required by dependencies for now
 _mapKeys = require 'lodash.mapkeys'
+
+parseDescriptionTags = (item) ->
+    # Pre-parse the color markup tags in descriptions. (Simple regex replace, assumes markup is valid and not nested.)
+
+    tagExpr = /<c=@([a-zA-Z]+)>(.+?)<\/c>/g
+    tagRepl = '$2'
+    brExpr = /\r?\n/g
+    brRepl = '
'
+
+    replacer = (str) ->
+        str.replace(tagExpr, tagRepl).replace(brExpr, brRepl)
+
+    if item.description?
+        item.description = replacer item.description
+
+    if item.detailsObject?.description?
+        item.detailsObject.description = replacer item.detailsObject.description
+
+    if item.detailsObject?.infix_upgrade?.buff?.description?
+        item.detailsObject.infix_upgrade.buff.description = replacer item.detailsObject.infix_upgrade.buff.description
+
+    if item.detailsObject?.bonuses?
+        item.detailsObject.bonuses = _.map item.detailsObject.bonuses, replacer
+
+    item

 sendFinalizedItemList = (res, ids, fromDB, fromAPI) ->
     # Unescape escaped data from own database.
@@ -33,6 +58,7 @@
         items[idx].missing = false
         items[idx] = _.pick item, ['id', 'missing', 'name', 'description', 'icon', 'type', 'rarity', 'description',
                                    'level', 'vendorValue', 'flags', 'restrictedTo', 'chatLink', 'detailsObject']
+        items[idx] = parseDescriptionTags items[idx]

     idsToStub = _.difference ids, _.pluck items, 'id'
     _.each idsToStub, (id) ->


extend a fresh dict when chaining props

extend a fresh dict when chaining props

--- bokehjs/src/coffee/model.coffee
+++ bokehjs/src/coffee/model.coffee
@@ -5,7 +5,7 @@
 class Model extends HasProps
   type: "Model"

-  props: () -> _.extend super(), {
+  props: () -> _.extend {}, super(), {
     tags: [ p.Array, [] ]
     name: [ p.String    ]
   }


Fix logout URL

Fix logout URL

--- generators/app/templates/gulpfile.coffee
+++ generators/app/templates/gulpfile.coffee
@@ -370,7 +370,7 @@
     gutil.log('status code: ', httpResponse.statusCode)

 LOGIN_URL = 'https://igem.org/Login2'
-LOGOUT_URL = 'http://igem.org/cgi/Logout.cgi'
+LOGOUT_URL = 'http://igem.org/Logout'
 # Login and call the callback with the cookie jar
 login = (cb) ->
     username = readlineSync.question('Username: ')


Update pt-BR.coffee (#4759)

Update pt-BR.coffee (#4759)

--- app/locale/pt-BR.coffee
+++ app/locale/pt-BR.coffee
@@ -1266,7 +1266,7 @@
     stats: "Estatísticas"
     student_email_invite_blurb: "Seu alunos podem também usar um código da turma <strong>__classCode__</strong> quando criar uma Conta de Aluno, sem necessidade de email."
     total_students: "Total de Alunos:"
-    average_time: "Average level play time:"
+    average_time: "Tempo médio de jogo:"
     total_time: "Total play time:"
     average_levels: "Média de níveis completados:"
     total_levels: "Total de níveis completados:"
@@ -1450,7 +1450,7 @@
     level_overview_solutions: "Análise do Nível e Soluções"
     no_student_assigned: "Nenhum aluno foi atribuído a este curso."
     paren_new: "(novo)"
-    student_code: "__name__'s Código do Aluno"
+    student_code: "__name__ Código do Aluno"
     teacher_dashboard: "Painel do Professor" # Navbar
     my_classes: "Minhas Turmas"
     courses: "Guias dos Cursos"
@@ -1987,7 +1987,7 @@
     my_matches: "Minhas Partidas"
     simulate: "Simular"
     simulation_explanation: "Por simular partidas você pode classificar seu jogo mais rápido!"
-#    simulation_explanation_leagues: "You will mainly help simulate games for allied players in your clans and courses."
+    simulation_explanation_leagues: "Você ajudará principalmente a simular jogos para jogadores aliados em seus clãs e cursos."
     simulate_games: "Simular Partidas!"
     games_simulated_by: "Partidas simuladas por você:"
     games_simulated_for: "Partidas simuladas para você:"


Reverted to last working state

Reverted to last working state

--- vendor/assets/javascripts/nested-form.js.coffee
+++ vendor/assets/javascripts/nested-form.js.coffee
@@ -1,13 +1,14 @@
 class NestedForm
   constructor: (@selector) ->
     # the jquery element should be the field_wrapper
-    this.j_el = $(selector)
+    if ($(selector).length)
+      this.j_el = $(selector)
     if (this.j_el).length > 0
       this.bind_directional_buttons()
       this.set_directional_button_ability()
       this.bind_add_button()
       this.bind_remove_buttons()
-
+

   set_directional_button_ability: () ->
     num_fields = this.j_el.find('.fields:visible').length
@@ -73,7 +74,7 @@
       num_fields = fields_wrapper.find('.fields:visible').length
       nested_form.set_directional_button_ability(fields_wrapper)
       nested_form.set_order_num(fields_wrapper)
-
+
       if num_fields < 1
         fields_wrapper.find('.empty-list').show()
       event.preventDefault()
@@ -101,4 +102,4 @@

 $(document).ready(
   nested_form = new NestedForm('.fields-wrapper')
-)+)


Update area_results_view.coffee

Update area_results_view.coffee

Changed km2 to ha in the tip

--- app/assets/javascripts/analysis/views/area_results_view.coffee
+++ app/assets/javascripts/analysis/views/area_results_view.coffee
@@ -11,7 +11,7 @@
     tot_area_tip: "The total size of your Area of Interest in km2."
     ca_stock_tip: "The total above and below ground carbon stock of all project habitats with your Area of Interest."
     co2_pc_emis_tip: "A conversion of the Total Carbon Stock value of your Area of Interest to CO2, which is then represented as days/years CO2 emissions of an average UAE citizen." #,  based on (XXXXX Global Earth Initiative ?)
-    habitat_tip: "Size of each habitat within your Area of Interest listed out in km2 and as a percentage of the overall habitat. Carbon stock per habitat in metric tonnes."
+    habitat_tip: "Size of each habitat within your Area of Interest listed out in hectares and as a percentage of the overall habitat. Carbon stock per habitat in metric tonnes."

   initialize: (options) ->


Fixed better buttons snippets

Fixed better buttons snippets

--- completions.cson
+++ completions.cson
@@ -2302,7 +2302,7 @@
   'prefix': 'gridfieldorderablerows'
   'module': 'silverstripe-australia/gridfieldextensions'
   'scope': '.text.html.php'
-  'body': 'new GridFieldOrderableRows(${1:\'Sort\'})'
+  'body': 'new GridFieldOrderableRows(${1:\'Sort\'})${0}'

 ######
 #
@@ -2313,7 +2313,7 @@
 'GridFieldSortableRows':
   'prefix': 'gridfieldsortablerows'
   'module': 'undefinedoffset/sortablegridfield'
-  'body': 'new GridFieldSortableRows(${1:\'Sort\'})'
+  'body': 'new GridFieldSortableRows(${1:\'Sort\'})${0}'

 ######
 #
@@ -2324,6 +2324,7 @@
 'getBetterButtonsActions':
   'prefix': 'getbetterbuttonsactions'
   'module': 'unclecheese/betterbuttons'
+  'scope': '.text.html.php'
   'body': """
   /**
    * Create better button custom actions
@@ -2340,6 +2341,7 @@
 'BetterButtonCustomAction':
   'prefix': 'betterbuttoncustomaction'
   'module': 'unclecheese/betterbuttons'
+  'scope': '.text.html.php'
   'body': """
   BetterButtonCustomAction::create(
   \t\'${1:name}\',
@@ -2350,6 +2352,7 @@
 'BetterButtonNestedForm':
   'prefix': 'betterbuttonnestedform'
   'module': 'unclecheese/betterbuttons'
+  'scope': '.text.html.php'
   'body': """
   BetterButtonNestedForm::create(
   \t\'${1:name}\',


Redirect directly to /not-found when page missing

--- app/assets/javascripts/sprangular/static_content/routes.coffee
+++ app/assets/javascripts/sprangular/static_content/routes.coffee
@@ -14,8 +14,8 @@
       controller: 'PageShowCtrl'
       templateUrl: 'pages/show.html'
       resolve:
-        page: (StaticContent, $route, $location)->
+        page: (StaticContent, $route, $window)->
           slug = $route.current.params.slug

           StaticContent.find(slug).then (->), ->
-            $location.path('/404')
+            $window.location.href = "/not-found"


Fix Find button

Fix Find button

--- app/assets/javascripts/views/events/find_button_view.js.coffee
+++ app/assets/javascripts/views/events/find_button_view.js.coffee
@@ -1,3 +1,4 @@
 Yossarian.EventFindButtonView = Ember.View.extend
   click: ->
     $('html, body').animate(scrollTop: $('#events').offset().top - 120, 500)
+    $('[data-target="#tags"]').click()


Move install file var under if block

Move install file var under if block

--- build/tasks/install-task.coffee
+++ build/tasks/install-task.coffee
@@ -36,13 +36,14 @@

       # Create Atom.desktop if installation not in temporary folder
       tmpDir = if process.env.TMPDIR? then process.env.TMPDIR else '/tmp'
-      desktopInstallFile = path.join(installDir, 'share', 'applications', 'Atom.desktop')
       if installDir.indexOf(tmpDir) isnt 0
         mkdir path.dirname(desktopInstallFile)
         {description} = grunt.file.readJSON('package.json')
         installDir = path.join(installDir,'.') # To prevent "Exec=/usr/local//share/atom/atom"

+        desktopInstallFile = path.join(installDir, 'share', 'applications', 'Atom.desktop')
         desktopFile = path.join('resources', 'linux', 'Atom.desktop.in')
+
         template = _.template(String(fs.readFileSync(desktopFile)))
         filled = template({description, installDir, iconName})


comment text back, not secure!

comment text back, not secure!

--- app/assets/javascripts/api/crud_comment.js.coffee
+++ app/assets/javascripts/api/crud_comment.js.coffee
@@ -121,8 +121,7 @@
   else
     abaa.className = "juvia-comment-pure-content"

-  comment_content = $(comment_text).clone().find("script,noscript").remove().end().html();
-  $(abaa).html comment_content
+  $(abaa).html comment_text

   aba.appendChild abaa
   abab = document.createElement("div")


Make clearer social buttons = login, not share

--- app/assets/javascripts/components/yale-login.cjsx
+++ app/assets/javascripts/components/yale-login.cjsx
@@ -7,8 +7,9 @@
     return

Welcome

-
To begin marking and transcribing programs, please log in to your prefered account

+
To begin marking and transcribing programs, please log in with either a Yale NetID...

         Yale Login
+
...or with a Facebook, Google+ or Twitter account:


scenarios: resetting a scenario brings to a blank scenario

Rather than reloading the current preset scenario. See #916

After this deploy we must decide what to do.

--- app/assets/javascripts/lib/models/scenario.coffee
+++ app/assets/javascripts/lib/models/scenario.coffee
@@ -40,6 +40,7 @@
     base_url + "/api_scenarios/" + @api_session_id()

   reset: =>
+    App.settings.set({scenario_id: null}, {silent: true})
     @new_session()
     # Take it easy. TODO: use deferred objects and refactor the js app events
-    setTimeout(App.load_user_values, 1000)
+    setTimeout(App.load_user_values, 1200)


minor changes and cleanup

minor changes and cleanup

--- app/assets/javascripts/search/queries/base.coffee
+++ app/assets/javascripts/search/queries/base.coffee
@@ -2,17 +2,18 @@
 class Clarat.Search.Query.Base
   PER_PAGE: 20
   BASE_PRECISION: 500
-  VALUES_PER_FACET: 300 # must be >= Category.Count
+  VALUES_PER_FACET: 300 # must be >= Category.Count to avoid missing categories

   constructor:
     (@query = '', @category = null, @facet_filters = [], @page = null) ->

   query_hash: ->
-    @categoryHelper = if @category && @category != "" then [@category] else []
+    # Algolia seems to want this string in an array for some cases
+    @categoryArray = if @category && @category != "" then [@category] else []
     _.merge @page_query(),
       query: @query
       params:
-        tagFilters: @categoryHelper
+        tagFilters: @categoryArray
         facets: '_age_filters,_audience_filters,_language_filters'
         facetFilters: @facet_filters
         aroundPrecision: @BASE_PRECISION


fix jsonp errors text

fix jsonp errors text

--- app/assets/javascripts/uploadcare/utils.js.coffee
+++ app/assets/javascripts/uploadcare/utils.js.coffee
@@ -129,10 +129,10 @@
     $.ajax(url, {data, dataType: 'jsonp'}).then (data) ->
       if data.error
         text = data.error.content or data.error
-        ns.warn(text)
+        ns.warn("JSONP error: #{text}")
         $.Deferred().reject(text)
       else
         data
     , (_, textStatus, errorThrown) ->
-      "Network error: #{textStatus} (#{errorThrown})"
+      "JSONP unexpected error: #{textStatus} (#{errorThrown})"


remove console.log

remove console.log

--- app/js/jail_iframe/util/postFactlinkObject.coffee
+++ app/js/jail_iframe/util/postFactlinkObject.coffee
@@ -15,7 +15,6 @@
   handler = (e) ->
     if typeof e.data == 'string' && startsWith(e.data, msgPrefix)
       data_obj = JSON.parse(e.data.substring(msgPrefix.length))
-      console.log data_obj[0], data_obj[1]
       receiver[data_obj[0]].apply receiver, data_obj[1]
     return
   window.addEventListener 'message', handler


add tortoise

add tortoise

--- app/lib/field-guide-content/bells-tortoise.coffee
+++ app/lib/field-guide-content/bells-tortoise.coffee
@@ -43,13 +43,4 @@
     content: '''
         Breeding is thought to occur during the wetter months. Females will dig a hole to lay up to 10 elongated, brittle-shelled eggs. Laying can occur at 40-day intervals. After an incubation period that generally lasts between 90 and 110 days, but can be up to a year, the hatchlings emerge.
     '''
-  }, {
-    title: 'Fun Facts'
-    style: 'focus-box'
-    content: '''
-

-

-

-

-    '''
   }]


Escape userAppPath too

Escape userAppPath too

--- kites/sharedHosting/api.coffee
+++ kites/sharedHosting/api.coffee
@@ -161,6 +161,7 @@
     appRootPath   = escapePath "/opt/Apps/#{username}/#{appName}"
     latestPath    = escapePath "/opt/Apps/#{username}/#{appName}/latest"
     versionedPath = escapePath "/opt/Apps/#{username}/#{appName}/#{version}"
+    userAppPath   = escapePath userAppPath

     cb = (err)->
       console.error err if err


Hackathon: reorganized the page flow

Hackathon: reorganized the page flow

--- client/landing/site.hackathon/coffee/home/AppViewFull.coffee
+++ client/landing/site.hackathon/coffee/home/AppViewFull.coffee
@@ -369,19 +369,25 @@

How does it work?

-
 First, sign up using the simple form above. We will send you a short questionnaire
-        that will help us understand a little bit more about you and your team’s background.
-        We promise to keep it short and sweet.
-

-
-
Applications can come from individuals or teams of up to 5, however each team member
-        needs to be a contributing team member (writing code or designing). If you are an
-        individual who is selected and are looking for team members, post a message on the
-        #hackathon channel on Koding.
-        Your post should clearly articulate what type of skill set you are looking for and what
-        type of skill set you have. e.g.: I am a nodeJS developer looking for a backend
-        database team member. Once you get a response, jump into private chats on Koding
-        itself to discuss your ideas and start recruiting!
+
First, sign up using the simple form above. Every day (up until we stop accepting
+        applications) between 4-5pm PST, we send you a short questionnaire that will help us
+        understand a little bit more about you and your team’s background. We promise to keep
+        it short and sweet.
+

+
+
Applications can come from individuals or teams of up to 5. Only one member per team
+        needs to apply for the entire team to be accepted but all members need to have Koding
+        accounts. Additionally, each team member needs to be a contributing team member
+        (writing code or designing).
+

+
+
If you are an individual who is selected and are looking for team members, post a
+        message on the
+        #hackathon channel on Koding. Your post should clearly articulate what type
+        of skill set you are looking for and what type of skill set you have. e.g.: I am a nodeJS
+        developer looking for a backend database team member. Once you get a response, jump into
+        private chats on Koding itself to discuss your ideas and start recruiting!
+

How do I get accepted?

@@ -406,23 +412,6 @@
         accepted into the event.

-
 What are the rules?

-
Our goal is to ensure that all teams have a level playing field; therefore it is
-        imperative that all code, design, assets, etc… must be created during the duration
-        of the event. No exceptions. You can brainstorm ideas prior to the event, however
-        any assets or code used as part of your submission must have been created during the event.
-        The only exception to this rule is the usage of publicly available material. This includes:
-        public code snippets, images, open source libraries and projects, public APIs, etc. You
-        get the picture.
-

-
-
We will request a review of all the code you submit as part of your hackathon project
-        to ensure that it has been created during the hackathon period (Dec 6th - Dec 7th).
-        Being engineers ourselves we are savvy to what can and can't be created in 48 hours. :)

-
-
Play fair!

-

-

What are the prizes?

$10,000 cash prize from Koding, split amongst the top 3 teams ($5k, $3k, $2k each).

@@ -430,12 +419,6 @@

Winners will be offered interviews with investors on our judging panel.

-

-
Who will eventually own the projects created?

-
-
You will retain full ownership and rights to your creation. Be super awesome and have fun! Get
-        creative, meet new people, make new friends and problem solve with them.
-

Additional Prizes

@@ -469,6 +452,11 @@

#{emailDay} - Winners are notified via email and winning projects are listed for public viewing.

+

+
Judges

+
Because we expect a large number of applicants, we wanted to ensure that we have a good number of awesome judges to help evaluate the incoming projects. These amazing folks have agreed to become part of our event. We cannot thank them enough.

+

+

What is the judging process?

Due to the large number of teams and potential hosting pitfalls that can come with, we request that
@@ -488,7 +476,31 @@

Round 3: Judges meet (virtually of course) for a live call to discuss and pick the final 3.

-

+

+

+
 What are the rules?

+
Our goal is to ensure that all teams have a level playing field; therefore it is
+        imperative that all code, design, assets, etc… must be created during the duration
+        of the event. No exceptions. You can brainstorm ideas prior to the event, however
+        any assets or code used as part of your submission must have been created during the event.
+        The only exception to this rule is the usage of publicly available material. This includes:
+        public code snippets, images, open source libraries and projects, public APIs, etc. You
+        get the picture.
+

+
+
We will request a review of all the code you submit as part of your hackathon project
+        to ensure that it has been created during the hackathon period (Dec 6th - Dec 7th).
+        Being engineers ourselves we are savvy to what can and can't be created in 48 hours. :)

+
+
Play fair!

+

+

+
Who will eventually own the projects created?

+
+
You will retain full ownership and rights to your creation. Be super awesome and have fun! Get
+        creative, meet new people, make new friends and problem solve with them.
+

+

How to submit your project

Send the following details to hackathon@koding.com
@@ -502,11 +514,6 @@

Your submission should have a time label less than #{secondDay} 2359 PST.
           Submissions received after the deadline will not be considered. No exceptions.

-

-

-
Judges

-
Because we expect a large number of applicants, we wanted to ensure that we have a good number of awesome judges to help evaluate the incoming projects. These amazing folks have agreed to become part of our event. We cannot thank them enough.

-

Awesome API's for you to check out


signup: enter handling for 1Password

signup: enter handling for 1Password

--- client/landing/site.landing/coffee/login/registerform.coffee
+++ client/landing/site.landing/coffee/login/registerform.coffee
@@ -21,7 +21,7 @@
         validate          : @getEmailValidator()
         decorateValidation: no
         focus             : => @email.icon.unsetTooltip()
-        keyup             : (event) => @submitForm event  if event.which is ENTER
+        keydown           : (event) => @submitForm event  if event.which is ENTER
         blur              : => @fetchGravatarInfo @email.input.getValue()
         change            : => @emailIsAvailable = no

@@ -33,7 +33,7 @@
         testPath         : "recover-password"
         placeholder      : "Password"
         focus            : => @password.icon.unsetTooltip()
-        keyup            : (event) =>
+        keydown          : (event) =>
           if event.which is ENTER
             @password.input.validate()
             @button.click event


Sync Queue

Sync Queue

--- app/controllers/sync.coffee
+++ app/controllers/sync.coffee
@@ -6,8 +6,9 @@
 IN_PROGRESS = 1
 ONLINE = 2

+Sync =

-Sync =
+  queue: JSON.parse localStorage.Queue or '{"Note": {}, "Notebook": {}}'

   # Hold pending actions
   pending: []
@@ -51,6 +52,24 @@
       fn() if typeof fn is 'function'
       @_clearPending()

+  # Check an event, and if it is a model update add it to the queue
+  addToQueue: (event, args) ->
+    if @queue[args.constructor.name][args.id] and @queue[args.constructor.name][args.id][0] is "create"
+      # create + destroy = nothing
+      if event is "destroy"
+        delete @queue[args.constructor.name][args.id]
+    else
+      # Otherwiswe, add it to the queue
+      @queue[args.constructor.name][args.id] = [event, new Date().getTime()]
+    @saveQueue()
+
+  saveQueue: ->
+    # Save queue to localstorage
+    localStorage.Queue = JSON.stringify @queue
+
+# Just in case you need any default values
+class Base
+
 Model.Sync =

   extended: ->
@@ -61,8 +80,9 @@
       console.log '%c> Calling fetch', 'background: #eee'
       Sync.defer(this, @loadLocal)

-    @change ->
-      console.log '%c> Calling change', 'background: #eee'
+    @change (record, event) ->
+      Sync.addToQueue(event, record)
+      console.log '%c> Calling change: ' + event, 'background: #eee'
       Sync.defer(this, @saveLocal)

     Sync.connect()


Colorize parameters in Tree-Sitter grammar

--- grammars/tree-sitter/tree-sitter-kotlin.cson
+++ grammars/tree-sitter/tree-sitter-kotlin.cson
@@ -119,6 +119,7 @@
 	'infix_expression > simple_identifier': 'entity.name.function.kotlin'
 	'call_expression > navigation_expression > navigation_suffix > simple_identifier': 'entity.name.function.kotlin'
 	'variable_declaration > simple_identifier': 'variable.kotlin'
+	'parameter > simple_identifier': 'variable.parameter.kotlin'
 	'comment': 'comment.kotlin'
 	'integer_literal': 'constant.numeric.integer.kotlin'
 	'hex_literal': 'constant.numeric.integer.hexadecimal.kotlin'


highlight image on input field change

highlight image on input field change

--- app/assets/javascripts/purchases.js.coffee
+++ app/assets/javascripts/purchases.js.coffee
@@ -8,9 +8,11 @@

     $("input.quantity").change ->
       self = $(this)
+      img = self.parents(".item-container").find("img")
       if parseInt(self.val()) < 1
-        img = self.parents(".item-container").find("img")
         img.removeClass "bg-primary"
+      else if !img.hasClass "bg-primary"
+        img.addClass "bg-primary"

     $(".item-thumb").click ->
       self = $(this)


Add "click 'more choices" box when more than 8

Supports opengovernment/product#85

--- app/assets/javascripts/questions.js.coffee
+++ app/assets/javascripts/questions.js.coffee
@@ -164,6 +164,16 @@
     personLi = e.delegateTarget

     name = $(personLi).children('h2').text()
+
+    # if click is for "more choices"
+    # show remaining results rather than selecting person
+    # and return early
+    # @todo, replace hardcoding
+    if name == "Click for More Choices"
+      $(personLi).hide()
+      $(personLi).parent().children('li.more-people').fadeTo(300, 1)
+      return
+
     avatarHtml = $(personLi).children('.avatar').html()
     jurisdiction = $(personLi).children('.person-info').children('.jurisdiction').text()

@@ -220,6 +230,9 @@
     personList.html('')
     $('#question_person_id').remove() # our radio buttons replace this below
     if $(data).length > 0
+      morePeopleThreshold = 8
+      totalPeople = $(data).length
+      personCount = 1
       $(data).each ->
         liVal = '
'
         liVal += '
'
@@ -249,7 +262,13 @@

         personList.append liVal

-        personList.children('li:last').fadeTo(300, 1)
+        if (totalPeople <= morePeopleThreshold) || (personCount < morePeopleThreshold)
+          personList.children('li:last').fadeTo(300, 1)
+        else
+          personList.children('li:last').addClass('more-people')
+          addMoreChoicesBox(personList) if personCount == morePeopleThreshold
+
+        personCount++
         $('.click-recipient-for-next-step').show() if lookupDiv != 'div.address_lookup'

       loading.hide()
@@ -262,6 +281,23 @@
     else
       loading.hide()
       $('.nothing-matching').show()
+
+  addMoreChoicesBox = (list)->
+        liVal = '
'
+        liVal += '
'
+        liVal += "<input type=\"radio\" name=\"more-choices\" id=\"more-choices\" />
"
+
+        liVal += '
'
+        placeholderUrl = "http://" + $(location).attr('host') + "/assets/placeholder.png"
+        liVal += ""
+        liVal += '
'
+
+        liVal += "
Click for More Choices
"
+
+        liVal += "
"
+
+        list.append liVal
+        list.children('li:last').fadeTo(300, 1)

   hideLaterSteps = (->
     $('article.not-first-step').each ->


Color scheme listener.

Color scheme listener.

--- app/assets/javascripts/web/_site.js.coffee
+++ app/assets/javascripts/web/_site.js.coffee
@@ -2609,12 +2609,15 @@
       $(document).on 'click', '[data-toggle="tooltip"]', (event) ->
         $(@).tooltip('hide')

+    colorSchemePreference: ->
+      darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
+      darkModeMediaQuery.addListener (event) ->
+          setTimeout feedbin.setNativeTheme, 300
+
     visibilitychange: ->
       $(document).on 'visibilitychange', (event) ->
         if feedbin.native && document.hidden == false
-          setTimeout ( ->
-            feedbin.setNativeTheme()
-          ), 300
+          setTimeout feedbin.setNativeTheme, 300
           feedbin.refresh()

     copy: ->


only collection requests should show activity

--- app/js/views/subject.coffee
+++ app/js/views/subject.coffee
@@ -56,10 +56,16 @@
     else
       @model.get('html_url')

-  startFetching: ->
+  startFetching: (object) ->
+    # We only care about requests for the collection
+    return unless object instanceof Backbone.Collection
+
     @fetchCount += 1
     @$el.addClass('paginating')

-  doneFetching: ->
+  doneFetching: (object) ->
+    # We only care about requests for the collection
+    return unless object instanceof Backbone.Collection
+
     @fetchCount -= 1
     @$el.removeClass('paginating') if @fetchCount == 0


fix inifinite loading indicator

fix inifinite loading indicator

--- app/js/views/threads.coffee
+++ app/js/views/threads.coffee
@@ -9,8 +9,6 @@
   initialize: ->
     @listenTo @collection, 'add', @add
     @listenTo @collection, 'reset', @addAll
-    @listenTo @collection, 'request', @startPaginating
-    @listenTo @collection, 'sync', @donePaginating

     @listenToOnce @collection, 'sync', =>
       # FIXME: this should be bound in #render, but for some reason the scroll
@@ -19,6 +17,8 @@

       # FIXME: this belongs somewhere else
       $('#toggle-lists').attr('checked', false) # Collapse the menu on mobile
+
+      @$el.removeClass('loading')

     @stateChange()

@@ -51,7 +51,8 @@

   paginate: =>
     return if @isPaginating || @collection.donePaginating || !@shouldPaginate()
-    @collection.paginate().done(@paginate)
+    @startPaginating
+    @collection.paginate().then(@donePaginating).done(@paginate)

   shouldPaginate: ->
     @$content.children().height() - @$content.scrollTop() < @$content.height() + 300
@@ -65,6 +66,6 @@
     @isPaginating = true
     @$el.addClass('paginating')

-  donePaginating: ->
+  donePaginating: =>
     @isPaginating = false
-    @$el.removeClass('loading paginating')
+    @$el.removeClass('paginating')