if search string starts with ~ or @ search when at least 4 chars are present

--- autocomplete-client.coffee
+++ autocomplete-client.coffee
@@ -75,7 +75,7 @@
     @ruleDep = new Deps.Dependency
     @filterDep = new Deps.Dependency
     @loadingDep = new Deps.Dependency
-
+
     # autosubscribe to the record set published by the server based on the filter
     # This will tear down server subscriptions when they are no longer being used.
     @sub = null
@@ -139,6 +139,9 @@
       @hideList()
       return

+    if (val.substring(0,1) in ["~", "@"]) and val.length < 4
+      @hideList()
+      return
     ###
       Matching on multiple expressions.
       We always go from a matched state to an unmatched one
@@ -202,7 +205,7 @@

   onItemClick: (doc, e) => @processSelection(doc, @rules[@matched])

-  onItemHover: (doc, e) ->
+  onItemHover: (doc, e) ->
     @markSelected($(e.target).closest(".-autocomplete-item"))

   filteredList: ->
@@ -363,12 +366,12 @@
   markSelected: ($item) ->
     $items = @tmplInst.$(".-autocomplete-item")

-    if $items.length == 0 and !Blaze.currentView
+    if $items.length == 0 and !Blaze.currentView
       return

     $items.removeClass("selected")
     $item.addClass("selected")
-
+
     doc = Blaze.getData($item[0])
     # console.log("Marked as selected", doc)
     $item.trigger("selected", doc)


More added to glyphs demo.

More added to glyphs demo.


allow Spawn only after user specifies an interpreter

--- client/connect.coffee
+++ client/connect.coffee
@@ -81,7 +81,7 @@

   enableSpawnAndListen = (b) ->
     $('#spawn,#listen').button if b then 'enable' else 'disable'
-    $('#listen-host,#listen-port').attr 'disabled', !b
+    $('#spawn-select,#spawn-exe,#listen-host,#listen-port').attr 'disabled', !b
     return

   $connect.add($about).add($new).add($delete).add($save).add($cancel).add($spawn).add($listen).button()
@@ -134,8 +134,13 @@
     false
   $('#spawn-select').change ->
     v = $(@).val(); $('#spawn-exe').val(v || prefs.otherExe()).prop 'readonly', !!v; v || $('#spawn-exe').focus()
-    prefs.selectedExe v; return
-  $('#spawn-exe').on 'change keyup', -> $('#spawn-select').val() || prefs.otherExe $(@).val(); return
+    prefs.selectedExe v
+    $('#spawn').button if v || prefs.otherExe() then 'enable' else 'disable'
+    return
+  $('#spawn-exe').on 'change keyup', ->
+    $('#spawn-select').val() || prefs.otherExe $(@).val()
+    $('#spawn').button if $('#spawn-select').val() || prefs.otherExe() then 'enable' else 'disable'
+    return
   $listen.click ->
     host = $listenHost.val()
     port = +$listenPort.val()


enable and disable Save and Cancel as appropriate

--- client/connect.coffee
+++ client/connect.coffee
@@ -130,17 +130,27 @@
       $list.append('127.0.0.1').trigger 'sel'
       $('.fav.sel').focus(); $host.select(); storeFavs(); false
     $delete.click -> $('.fav.sel').remove(); $list.trigger 'sel'; storeFavs(); false
-    $list.on 'sel', -> # triggered whenever selection changes
+    $list.on 'sel', -> # triggered after selection changes
       storeFavs(); $s = $ '.fav.sel'; n = $s.length
       $name.add($host).add($port).attr 'disabled', n != 1
       $connect.add($save).add($cancel).button 'option', 'disabled', n != 1
       $delete.button 'option', 'disabled', !n
+      $save.add($cancel).button if n == 1 then 'enable' else 'disable'
       if n == 1 then x = parseFav $s.text(); $name.val x.name; $host.val x.host; $port.val x.port
       else $name.add($host).add($port).val ''
       return
-    $host.add($port).add($name).on 'keydown', null, 'return', -> $save.click(); false
-    $save.click -> $('.fav.sel').focus().text fmtFav host: $host.val(), port: $port.val(), name: $name.val(); storeFavs(); false
-    $cancel.click -> x = parseFav $('.fav.sel').text(); $host.val x.host; $port.val x.port; $name.val x.name; false
+    $host.add($port).add($name)
+      .on 'keydown', null, 'return', -> $save.click(); false
+      .on 'keyup change', ->
+        s0 = $('.fav.sel').text(); s1 = fmtFav host: $host.val(), port: $port.val(), name: $name.val()
+        $save.add($cancel).button if s0 == s1 then 'disable' else 'enable'
+        return
+    $save.click ->
+      $('.fav.sel').focus().text fmtFav host: $host.val(), port: $port.val(), name: $name.val(); storeFavs()
+      $save.add($cancel).button 'disable'; false
+    $cancel.click ->
+      x = parseFav $('.fav.sel').text(); $host.val x.host; $port.val x.port; $name.val x.name
+      $save.add($cancel).button 'disable'; false
     $spawn.click ->
       port = +$spawnPort.val()
       if 0 < port < 0x10000


Support hearing multiple issues in one message (eg, saying "look at PROJ-324 or PROJ-355" would have previously only posted the link to 324, now it will show both)

--- src/scripts/jira.coffee
+++ src/scripts/jira.coffee
@@ -128,12 +128,13 @@
     search msg, msg.match[2], (text) ->
       msg.send "#{msg.message.user.id}: #{text}"

-  robot.hear /(\w+-[0-9]+)/i, (msg) ->
+  robot.hear /(\w+-[0-9]+)/ig, (msg) ->
     if msg.message.user.id is robot.name
       return

-    info msg, msg.match[0], (text) ->
-      msg.send text
+    for matched in msg.match
+      info msg, matched, (text) ->
+        msg.send text

   robot.respond /save filter (.*) as (.*)/i, (msg) ->
     filter = filters.get msg.match[2]


LO-1006 hubot pager services

LO-1006 hubot pager services

--- src/scripts/part.coffee
+++ src/scripts/part.coffee
@@ -90,7 +90,7 @@
       services = json.services
       if services.length > 0
         for service in services
-          buffer += "* #{service.id}: #{service.name} (#{service.status}) - https://#{pagerduty.subdomain}.pagerduty.com/services/#{service.id}\n"
+          buffer += "* #{service.id}: #{service.name} (#{service.status}) - #{service.html_url}\n"
         msg.send buffer
       else
         msg.send 'No services found!'


Removed extraneous console output meant for debugging

--- src/scripts/time.coffee
+++ src/scripts/time.coffee
@@ -361,7 +361,6 @@
     earlyToday = moment({hour: 0, minute: 0, second: 0})
     now = moment({hour: 0, minute: 0, second: 0}).add(1, 'days')
     report = user.toRawPayroll(earlyToday, now)
-    console.log report
     headers = HEADERS.payrollreports

     loggedAny = false


server: lower the limit

server: lower the limit

--- servers/lib/server/handlers/invitetoteambycsv.coffee
+++ servers/lib/server/handlers/invitetoteambycsv.coffee
@@ -27,7 +27,7 @@

     busboy = new Busboy {
       headers: req.headers
-      fileSize: 10 * 1024 * 1024 # 10 mb
+      fileSize: 1024 * 1024 # 10 mb
     }

     busboy.on 'file', (fieldname, file, filename, encoding, mimetype) ->


respect mongo 3 limit of 1000 bulk operations

--- services/track-changes/app/coffee/PackManager.coffee
+++ services/track-changes/app/coffee/PackManager.coffee
@@ -268,7 +268,7 @@
 		return newResults

 	MAX_SIZE:  1024*1024 # make these configurable parameters
-	MAX_COUNT: 1024
+	MAX_COUNT: 512

 	convertDocsToPacks: (docs, callback) ->
 		packs = []


globals: do not set any field that is going to be written by webserver in this file. (globals.coffee overrides window.globals)

--- client/globals.coffee
+++ client/globals.coffee
@@ -1,5 +1,4 @@
 g =
-  config                    : {}
   appClasses                : {}
   appScripts                : {}
   appLabels                 : {}
@@ -8,7 +7,6 @@
   __logs                    : []
   useWebSockets             : true
   logsEnabled               : false
-  isLoggedInOnLoad          : false
   isTesting                 : false
   REMOTE_API                : {}
   COMPUTECONTROLLER_TIMEOUT : 30000


Add `paper-scroll-header-panel` snippet.

--- snippets/paper-elements.cson
+++ snippets/paper-elements.cson
@@ -8,6 +8,14 @@

${1:Drawer}

${2:Main Panel}

     </paper-drawer-panel>
+    """
+  "paper scroll header panel":
+    "prefix": "paper-scroll-header-panel"
+    "body": """
+    <paper-scroll-header-panel>
+      <paper-toolbar>${1:Header}</paper-toolbar>
+
${2:Content goes here...}

+    </paper-scroll-header-panel>
     """
   "paper toolbar":
     "prefix": "paper-toolbar"


Add `paper-material` snippet.

Add `paper-material` snippet.

--- snippets/paper-elements.cson
+++ snippets/paper-elements.cson
@@ -51,6 +51,13 @@
     "body": """
     <paper-icon-button icon="${1:menu}""></paper-icon-button>
     """
+  "paper material":
+    "prefix": "paper-material"
+    "body": """
+    <paper-material elevation="${1:1}">
+      $2
+    </paper-material>
+    """
   "paper scroll header panel":
     "prefix": "paper-scroll-header-panel"
     "body": """


Increased min size of network for health condition report, closes #536

--- app/assets/javascripts/reports/my_health_conditions.coffee
+++ app/assets/javascripts/reports/my_health_conditions.coffee
@@ -73,7 +73,7 @@
   setupData = (data) ->
     # initialize circle radius scale
     countExtent = d3.extent(data.nodes, (d) -> d.frequency)
-    circleRadius = d3.scale.sqrt().range([3, 50]).domain(countExtent)
+    circleRadius = d3.scale.sqrt().range([10, 50]).domain(countExtent)

     # Initialize x/y coordinate and radius of each node
     data.nodes.forEach (n) ->


Fix normal replies (not public or private)

--- client/message.coffee
+++ client/message.coffee
@@ -1320,11 +1320,12 @@
       Files.resumable.addFile file, e
   {buttonProps, inputProps} = uploaderProps attachFiles, attachInput

+  privateReply = canPrivateReply message
   <div className="btn-group pull-right message-reply-buttons">
     {if canPublicReply message
-      <button className="btn btn-default replyButton" data-privacy="public" onClick={onReply}>{prefix}{if canPrivateReply message then 'Public '}Reply</button>
+      <button className="btn btn-default replyButton" data-privacy={if privateReply then "public"} onClick={onReply}>{prefix}{if privateReply then 'Public '}Reply</button>
     }
-    {if canPrivateReply message
+    {if privateReply
       <button className="btn btn-default replyButton" data-privacy="private" onClick={onReply}>{prefix}Private Reply</button>
     }
     <input className="attachInput" type="file" multiple ref={attachInput} {...inputProps}/>


Bugfix: make automatic search list expansion work.

--- src/views/search-results.coffee
+++ src/views/search-results.coffee
@@ -110,7 +110,12 @@
             fields = @getDetailedFieldset()
             @ready = false
             @onlyResultType = opts.onlyResultType
-            unless opts.onlyResultType
+            if opts.onlyResultType
+                @expansion = 2 * PAGE_SIZE
+                @fullCollection.fetchFields(0, @expansion, fields).done =>
+                    @ready = true
+                    @render()
+            else
                 @fullCollection.fetchFields(0, EXPAND_CUTOFF, fields).done =>
                     @ready = true
                     @render()
@@ -118,9 +123,6 @@
                 @hidden = true
                 @render()
             @listenTo @fullCollection, 'show-all', @nextPage
-            if opts.onlyResultType
-                @ready = false
-                @nextPage()
         serializeData: ->
             if @hidden
                 return hidden: true


修复 Editor无法刷新值的bug

修复 Editor无法刷新值的bug

--- src/widget/edit/abstract-editor.coffee
+++ src/widget/edit/abstract-editor.coffee
@@ -78,7 +78,8 @@
 				form = cola.widget(@_formDom)
 				if form and form instanceof cola.Form
 					form.setFieldMessages(@, keyMessage)
-		else
+
+		unless type == cola.constants.MESSAGE_VALIDATION_STATE_CHANGE
 			value = @readBindingValue()
 			if value? and @_dataType
 				value = @_dataType.parse(value)


Output name and version of module being published

--- src/publisher.coffee
+++ src/publisher.coffee
@@ -1,4 +1,6 @@
 path = require 'path'
+colors = require 'colors'
+CSON = require 'season'
 config = require './config'
 Command = require './command'

@@ -12,9 +14,16 @@
     @atomNpmPath = require.resolve('npm/bin/npm-cli')

   run: (options) ->
+    process.stdout.write 'Publishing '
+    try
+      {name, version} = CSON.readFileSync(CSON.resolve('package')) ? {}
+      process.stdout.write "#{name}@#{version} "
+    catch e
     publishArgs = ['--userconfig', @userConfigPath, 'publish']
-    @fork @atomNpmPath, publishArgs, (code) =>
+    @fork @atomNpmPath, publishArgs, (code, stderr='') =>
       if code is 0
+        process.stdout.write '\u2713\n'.green
         options.callback()
       else
-        options.callback("Publishing module failed with code: #{code}")
+        process.stdout.write '\u2717\n'.red
+        options.callback(stderr.red)


bumping version to 2.10.3

bumping version to 2.10.3

--- client/version.coffee
+++ client/version.coffee
@@ -1 +1 @@
-@version = '2.10.1'
+@version = '2.10.3'


.css('opacity', 0) fix

.css('opacity', 0) fix

You can't set i.e. opacity to 0 due to `if value` will return false for 0.

Fixed by chaining `if value` to `if typeof value != "undefined"`.

--- src/quo.style.coffee
+++ src/quo.style.coffee
@@ -27,7 +27,7 @@
         _existsClass name, this[0].className

     $$.fn.style = (property, value) ->
-        if value
+        if typeof value != "undefined"
             @each -> @style[property] = value
         else
             this[0].style[property] or _computedStyle(this[0], property)


Kill dead tab view code

Kill dead tab view code

--- src/packages/tabs/lib/tab-view.coffee
+++ src/packages/tabs/lib/tab-view.coffee
@@ -12,15 +12,6 @@
   initialize: (@item, @pane) ->
     @item.on? 'title-changed', => @updateTitle()
     @updateTitle()
-#     @buffer = @editSession.buffer
-#     @subscribe @buffer, 'path-changed', => @updateFileName()
-#     @subscribe @buffer, 'contents-modified', => @updateModifiedStatus()
-#     @subscribe @buffer, 'saved', => @updateModifiedStatus()
-#     @subscribe @buffer, 'git-status-changed', => @updateModifiedStatus()
-#     @subscribe @editor, 'editor:edit-session-added', => @updateFileName()
-#     @subscribe @editor, 'editor:edit-session-removed', => @updateFileName()
-#     @updateFileName()
-#     @updateModifiedStatus()

   updateTitle: ->
     return if @updatingTitle


do not clear config

do not clear config

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -1418,9 +1418,6 @@
       # save whatever ruleset type they are setting us to
       ruleset.ruleset_type = rulesetConfig.type

-      # clear previous config
-      ruleset.config = {}
-
       if rulesetConfig.type == 'subflow'
         flow = splitEditor.flow.selected[0]
         ruleset.config =


Add method to show additional options

Add method to show additional options

--- static/coffee/flows/controllers.coffee
+++ static/coffee/flows/controllers.coffee
@@ -836,7 +836,7 @@
 TranslateRulesController = ($scope, $modalInstance, Flow, utils, languages, ruleset, translation) ->

   $scope.translation = translation
-
+
   # clone our ruleset
   ruleset = utils.clone(ruleset)

@@ -1089,6 +1089,14 @@

   formData.flowField = Flow.getFieldSelection($scope.flowFields, $scope.ruleset.operand, true)
   formData.contactField = Flow.getFieldSelection($scope.contactFields, $scope.ruleset.operand, false)
+
+  formData.isAdditionalOptionsVisible = false
+
+  $scope.showAdditionalOptions = () ->
+    if formData.isAdditionalOptionsVisible
+      formData.isAdditionalOptionsVisible = false
+    else
+      formData.isAdditionalOptionsVisible = true

   config = $scope.ruleset.config
   if not config
@@ -1918,7 +1926,7 @@
       $scope.action.type = 'flow'

     flow = flow[0]
-    $scope.action.flow =
+    $scope.action.flow =
       uuid: flow.id
       name: flow.text


fix for dropdown #478

fix for dropdown #478

--- app/assets/javascripts/admin/views/selection_view.coffee
+++ app/assets/javascripts/admin/views/selection_view.coffee
@@ -7,7 +7,7 @@
   tagName : "select"
   className : "form-control"
   attributes : ->
-    name = @options.name
+    return {name : @options.name}

   childView : SelectionItemView


Fix REPL when env.HOME isn't available - #2945

--- repl.coffee
+++ repl.coffee
@@ -7,7 +7,7 @@

 replDefaults =
   prompt: 'coffee> ',
-  historyFile: path.join(process.env.HOME, '.coffee_history')
+  historyFile: path.join process.env.HOME, '.coffee_history' if process.env.HOME
   historyMaxInputSize: 10240
   eval: (input, context, filename, cb) ->
     # XXX: multiline hack.


Socialapi: created client api endpoints for init and send private messages from koding bot

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -366,6 +366,16 @@
       validateOptionsWith: ['body', 'channelId']
       mapperFn           : mapPrivateMessages

+    initPrivateMessageFromBot : messageRequesterFn
+      fnName                  : 'initPrivateMessage'
+      validateOptionsWith     : ['body', 'recipients']
+      mapperFn                : mapPrivateMessages
+
+    sendPrivateMessageFromBot : messageRequesterFn
+      fnName                  : 'sendPrivateMessageFromBot'
+      validateOptionsWith     : ['body', 'channelId']
+      mapperFn                : mapPrivateMessages
+
     fetchPrivateMessages : messageRequesterFn
       fnName             : 'fetchPrivateMessages'
       mapperFn           : mapPrivateMessages


realtime: fix some private messages are not shown bug

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -227,7 +227,7 @@
   # this method will prevent the arrival of
   # realtime messages to the individual messages
   # if the message is mine and current window has focus.
-  isFromOtherBrowser = (message, event) ->
+  isFromOtherBrowser = (message) ->

     # selenium doesn't put focus into the
     # spawned browser, it's causing problems.
@@ -237,11 +237,7 @@
     return no  if KD.isTesting

     {message} = message  unless message.typeConstant?
-    {_inScreenMap, _cache}  = KD.singletons.socialapi
-
-    # While making retrospective realtime message query, it is possible to fetch an already
-    # existing message. This is for preventing the case.
-    return no  if event is 'MessageAdded' and _cache?[message.typeConstant]?[message.id]
+    {_inScreenMap}  = KD.singletons.socialapi

     # when I am not the message owner, it is obviously from another browser
     return yes  unless message.accountId is KD.whoami().socialApiId
@@ -259,18 +255,34 @@
   forwardMessageEvents : forwardMessageEvents
   forwardMessageEvents = (source, target, events) ->

-    events.forEach ({event, mapperFn, validatorFn}) ->
+    events.forEach ({event, mapperFn, validatorFn, filterFn}) ->
       source.on event, (data, rest...) ->

         if validatorFn
           if typeof validatorFn isnt "function"
             return warn "validator function is not valid"

-          return  unless validatorFn(data, event)
+          return  unless validatorFn(data)
+
+        if filterFn
+          if typeof filterFn isnt "function"
+            return warn "filter function is not valid"
+
+          return  unless filterFn(data)

         data = mapperFn data

         target.emit event, data, rest...
+
+
+  # While making retrospective realtime message query, it is possible to fetch an already
+  # existing message. This is for preventing the case.
+  filterMessage = (data) ->
+    {message} = data  unless data.typeConstant?
+    if cachedMessage = KD.singletons.socialapi._cache?[message.typeConstant]?[message.id]
+      return yes  if cachedMessage.isShown
+
+    message.isShown = yes

   registerAndOpenChannels = (socialApiChannels) ->
@@ -424,9 +436,10 @@
       when 'post', 'message'           then @message.byId {id}, kallback
       else callback { message: "#{type} not implemented in revive" }

+
   getMessageEvents = ->
     [
-      {event: "MessageAdded",       mapperFn: mapActivity, validatorFn: isFromOtherBrowser}
+      {event: "MessageAdded",       mapperFn: mapActivity, validatorFn: isFromOtherBrowser, filterFn: filterMessage}
       {event: "MessageRemoved",     mapperFn: mapActivity, validatorFn: isFromOtherBrowser}
       {event: "AddedToChannel",     mapperFn: mapParticipant}
       {event: "RemovedFromChannel", mapperFn: mapParticipant}


SyncController: method to update array of files to be saved added.

--- client/Main/synclocalcontroller.coffee
+++ client/Main/synclocalcontroller.coffee
@@ -10,12 +10,12 @@
       @syncLocalContentIfDiffExists (res)->
         log "sync complete"

-  addToWillSaved: (fileName)->
-    syncFiles = @storage.getValue "saveRequestedFiles"
-      @filesToSave.push fileName if index == -1
-      @filesToSave.push fileName
-
+  addToSaveArray: (file)->
+    fileName = @getFileFullPath file
+    index    = @filesToSave.indexOf fileName
+    @filesToSave.push fileName if index is -1
     @storage.setValue "saveRequestedFiles", @filesToSave
+    @initializeListeners()

   removeFromSaveArray: (fileName) ->
     index = @filesToSave.indexOf fileName


fix for /home to /me renaming

fix for /home to /me renaming

--- app/assets/javascripts/models/user/user_viewmodel.coffee
+++ app/assets/javascripts/models/user/user_viewmodel.coffee
@@ -20,7 +20,7 @@
     rows : new Backbone.Collection()
     monthlyTotalHours : 0
     dailyTotalHours : 0
-    urlRoot : "home"
+    urlRoot : "me"
     viewTitle : "User Report"

   dataSourceClass : UserTimeCollection


Out of responsibility

Out of responsibility

--- source/core/constants.coffee
+++ source/core/constants.coffee
@@ -10,9 +10,6 @@

 Atoms.Core.Constants =

-  ANIMATION:
-    END   : "webkitAnimationEnd MSAnimationEnd animationend"
-
   APPEND:  "append"

   EXCLUDED_IF_KEYS: ["class", "entity", "events", "if", "parent", "children"]


Client: Center screens.

Client: Center screens.

--- client/app/js/views/battle_view.coffee
+++ client/app/js/views/battle_view.coffee
@@ -685,6 +685,7 @@
     $screen.css(left: x, top: y).appendTo(@$('.battle_pane'))
     if @skip?
       $screen.css(width: finalSize, height: finalSize)
+      $screen.css(x: -halfSize, y: -halfSize)
       done()
     else
       $screen


Rename helper to tumorTypeService. Remove unused Helpers service.

--- app/javascripts/tnm.coffee
+++ app/javascripts/tnm.coffee
@@ -1,107 +1,105 @@
-define ['angular', 'lodash', 'helpers', 'breast', 'sarcoma'], (ng, _) ->
-  tnm = ng.module 'qiprofile.tnm', ['qiprofile.helpers', 'qiprofile.breast',
-                                    'qiprofile.sarcoma']
+define ['angular', 'lodash', 'breast', 'sarcoma'], (ng, _) ->
+  tnm = ng.module 'qiprofile.tnm', ['qiprofile.breast', 'qiprofile.sarcoma']

-  tnm.factory 'TNM', ['Breast', 'Sarcoma',
-    (Breast, Sarcoma) ->
-      # The supported helper factories.
-      HELPERS =
-        Breast: Breast
-        Sarcoma: Sarcoma
+  tnm.factory 'TNM', ['Breast', 'Sarcoma', (Breast, Sarcoma) ->
+    # The supported tumor type-specific services.
+    TUMOR_TYPE_SERVICES =
+      Breast: Breast
+      Sarcoma: Sarcoma

-      # @param tumorType the tumor type name, e.g. 'Breast'
-      # @returns the helper factory for that tumor type
-      helper = (tumorType) ->
-        HELPERS[tumorType] or
-          throw new ReferenceError("Unsupported tumor type: #{ tumorType }")
+    # @param tumorType the tumor type name, e.g. 'Breast'
+    # @returns the tumorTypeService factory for that tumor type
+    tumorTypeService = (tumorType) ->
+      TUMOR_TYPE_SERVICES[tumorType] or
+        throw new ReferenceError("Unsupported tumor type: #{ tumorType }")

-      # Define here for reuse below.
-      _summaryGrade = (tnm) ->
-        # Calculates the cumulative grade as the sum of the component
-        # tumor type factory SCORES property values.
-        #
-        # @param tnm the TNM object
-        # @returns the cumulative grade, or null if a score is missing
-        cumulativeGrade = ->
-          accumulate = (sum, prop) ->
-            sum + grade[prop]
+    # Define here for reuse below.
+    _summaryGrade = (tnm) ->
+      # Calculates the cumulative grade as the sum of the component
+      # tumor type factory SCORES property values.
+      #
+      # @param tnm the TNM object
+      # @returns the cumulative grade, or null if a score is missing
+      cumulativeGrade = ->
+        accumulate = (sum, prop) ->
+          sum + grade[prop]

-          return null unless grade?
-          props = helper(tnm.tumorType).Grade.SCORES
-          if _.every(props, (prop) -> grade[prop]?)
-            _.reduce(props, accumulate, 0)
+        return null unless grade?
+        props = tumorTypeService(tnm.tumorType).Grade.SCORES
+        if _.every(props, (prop) -> grade[prop]?)
+          _.reduce(props, accumulate, 0)
+        else
+          null
+
+      grade = tnm.grade
+      if not grade?
+        return null
+      # The sum of the relevant grade properties.
+      cGrade = cumulativeGrade()
+      if not cGrade? then return null
+      # The tumor type grade ranges.
+      ranges = tumorTypeService(tnm.tumorType).Grade.RANGES
+      inRange = (range) -> cGrade in range
+      index = _.findIndex(ranges, inRange)
+      if index < 0
+        throw new ReferenceError("Unsupported #{ tnm.tumorType }" +
+                                 " cumulative grade: #{ cGrade }")
+
+      # Return the one-based summary grade value.
+      1 + index
+
+    # @param size the size composite object
+    # @returns the standard size string, e.g. 'p2b'
+    # @throws Error if the tumorSize property value is missing
+    formatSize: (size) ->
+      sizeSuffix = ->
+        inSituSuffix = (inSitu) ->
+          if size.inSitu.invasiveType is 'ductal'
+            'is(DCIS)'
+          else if size.inSitu.invasiveType is 'lobular'
+            'is(LCIS)'
           else
-            null
+            'is'

-        grade = tnm.grade
-        if not grade?
-          return null
-        # The sum of the relevant grade properties.
-        cGrade = cumulativeGrade()
-        if not cGrade? then return null
-        # The tumor type grade ranges.
-        ranges = helper(tnm.tumorType).Grade.RANGES
-        inRange = (range) -> cGrade in range
-        index = _.findIndex(ranges, inRange)
-        if index < 0
-          throw new ReferenceError("Unsupported #{ tnm.tumorType }" +
-                                   " cumulative grade: #{ cGrade }")
+        # The size suffix can be a, b, c or is.
+        # in situ (is) can be modified with the invasive
+        # type (ductal or lobular).
+        if size.inSitu?
+          inSituSuffix(size.inSitu)
+        else if size.suffix?
+          size.suffix
+        else
+          ''

-        # Return the one-based summary grade value.
-        1 + index
+      if not size.tumorSize?
+        throw new Error("The TNM tumor size is missing")
+      prefix = size.prefix or ''
+      suffix  = sizeSuffix()
+      "#{ prefix }#{ size.tumorSize }#{ suffix }"

-      # @param size the size composite object
-      # @returns the standard size string, e.g. 'p2b'
-      # @throws Error if the tumorSize property value is missing
-      formatSize: (size) ->
-        sizeSuffix = ->
-          inSituSuffix = (inSitu) ->
-            if size.inSitu.invasiveType is 'ductal'
-              'is(DCIS)'
-            else if size.inSitu.invasiveType is 'lobular'
-              'is(LCIS)'
-            else
-              'is'
+    # Calculates the summary grade based on the cumulative grade
+    # as defined in the tumor type factory RANGES lookup table.
+    #
+    # @param grade the grade composite object
+    # @returns the summary grade
+    # @throws ReferenceError if the grade scores are not supported
+    summaryGrade: _summaryGrade

-          # The size suffix can be a, b, c or is.
-          # in situ (is) can be modified with the invasive
-          # type (ductal or lobular).
-          if size.inSitu?
-            inSituSuffix(size.inSitu)
-          else if size.suffix?
-            size.suffix
-          else
-            ''
-
-        if not size.tumorSize?
-          throw new Error("The TNM tumor size is missing")
-        prefix = size.prefix or ''
-        suffix  = sizeSuffix()
-        "#{ prefix }#{ size.tumorSize }#{ suffix }"
-
-      # Calculates the summary grade based on the cumulative grade
-      # as defined in the tumor type factory RANGES lookup table.
-      #
-      # @param grade the grade composite object
-      # @returns the summary grade
-      # @throws ReferenceError if the grade scores are not supported
-      summaryGrade: _summaryGrade
-
-      # Calculates the tumor stage for the given TNM composite
-      # object.
-      #
-      # This function returns the cancer stage as a string
-      # consisting of a digit in the range 1 to 4 optionally
-      # followed by a suffix A, B or C. This facilitates
-      # accurate comparison, in contrast to the roman numeral
-      # grade, e.g. '1A' < '2B' , but 'IA' > 'IIB'.
-      # The romanize filter can be used to display the stage
-      # in the standard archaic medical format.
-      #
-      # @param tnm the TNM object.
-      # @returns the cancer stage object
-      stage: (tnm) ->
-        g = _summaryGrade(tnm)
-        # Delegate to the tumor type helper.
-        helper(tnm.tumorType).stage(tnm, g)
+    # Calculates the tumor stage for the given TNM composite
+    # object.
+    #
+    # This function returns the cancer stage as a string
+    # consisting of a digit in the range 1 to 4 optionally
+    # followed by a suffix A, B or C. This facilitates
+    # accurate comparison, in contrast to the roman numeral
+    # grade, e.g. '1A' < '2B' , but 'IA' > 'IIB'.
+    # The romanize filter can be used to display the stage
+    # in the standard archaic medical format.
+    #
+    # @param tnm the TNM object.
+    # @returns the cancer stage object
+    stage: (tnm) ->
+      grade = _summaryGrade(tnm)
+      # Delegate to the tumor type service.
+      tumorTypeService(tnm.tumorType).stage(tnm, grade)
   ]


switch from staging to live exp server

switch from staging to live exp server

--- app/lib/experiments.coffee
+++ app/lib/experiments.coffee
@@ -16,7 +16,7 @@
 The URL of the experiment server to use
 ###
 # prod:
-EXPERIMENT_SERVER_URL = "http://experiments.staging.zooniverse.org/"
+EXPERIMENT_SERVER_URL = "http://experiments.zooniverse.org/"
 # dev:
 #EXPERIMENT_SERVER_URL = "http://localhost:4567/"


Error handling for syntax errors in component code.

--- app/lib/world/world.coffee
+++ app/lib/world/world.coffee
@@ -207,7 +207,11 @@
     map = if kind is 'component' then @componentCodeClassMap else @systemCodeClassMap
     c = map[js]
     return c if c
-    c = map[js] = eval js
+    try
+      c = map[js] = eval js
+    catch err
+      console.error "Couldn't compile #{kind} code:", err, "\n", js
+      c = map[js] = {}
     c.className = name
     c


DE 681 Employment , tweaking coffeescript

--- c3/app/assets/javascripts/circs/s2_report_changes/g10_startedEmploymentAndOngoing.coffee
+++ c3/app/assets/javascripts/circs/s2_report_changes/g10_startedEmploymentAndOngoing.coffee
@@ -134,15 +134,14 @@

 window.payDay = (o) ->
   S(o.howOftenFrequency).change ->
-    if visible("#usuallyPaidSameAmountWrap")
+    if visible("usuallyPaidSameAmountWrap")
       S("usuallyPaidSameAmountWrap").slideUp 0, ->
         checked(o.usuallyPaidSameAmountY,false)
         checked(o.usuallyPaidSameAmountN,false)

-    S("monthlyPayDayWrap").slideUp 0, -> val(o.monthlyPayDay,"") if visible("#monthlyPayDayWrap")
+    S("monthlyPayDayWrap").slideUp 0, -> val(o.monthlyPayDay,"") if visible("monthlyPayDayWrap")

-    if val(o.howOftenFrequency) is o.defaultValue or val(o.howOftenFrequency) is ""
-    else
+    if not (val(o.howOftenFrequency) is o.defaultValue or val(o.howOftenFrequency) is "")
       if checked(o.beenPaidYetN)
         textToUse = switch (val(o.howOftenFrequency))
           when "weekly" then o.usuallyPaidSameAmountWeeklyTextExpected
@@ -150,8 +149,7 @@
           when "fourweekly" then o.usuallyPaidSameAmountFourWeeklyTextExpected
           when "monthly" then o.usuallyPaidSameAmountMonthlyTextExpected
           else o.usuallyPaidSameAmountOtherTextExpected
-
-          legend(o.usuallyPaidSameAmount).html(textToUse)
+        legend(o.usuallyPaidSameAmount).html(textToUse)

       if checked(o.beenPaidYetY)
         textToUse = switch (val(o.howOftenFrequency))
@@ -160,7 +158,6 @@
           when "fourweekly" then o.usuallyPaidSameAmountFourWeeklyText
           when "monthly" then o.usuallyPaidSameAmountMonthlyText
           else o.usuallyPaidSameAmountOtherText
-
         legend(o.usuallyPaidSameAmount).html(textToUse)

       checked(o.usuallyPaidSameAmountY, false)


Fix a typo the file description

Fix a typo the file description

--- src/namespace.coffee
+++ src/namespace.coffee
@@ -1,4 +1,4 @@
-# The global namespace, the `Batman` function will also create also create a new
+# The global namespace, the `Batman` function will also create a new
 # instance of Batman.Object and mixin all arguments to it.
 Batman = (mixins...) ->
   new Batman.Object(mixins...)


Fixed #60.

Fixed #60.

--- app/views/home_view.coffee
+++ app/views/home_view.coffee
@@ -8,7 +8,8 @@
   template: template

   events:
-    'hover #beginner-campaign': 'onHover'
+    'mouseover #beginner-campaign': 'onMouseOverButton'
+    'mouseout #beginner-campaign': 'onMouseOutButton'

   getRenderData: ->
     c = super()
@@ -45,7 +46,6 @@
     spriteOptions = thangID: "Beginner Wizard", resolutionFactor: scale
     @wizardSprite = new WizardSprite @wizardType, spriteOptions
     @wizardSprite.update()
-    #@wizardSprite.setColorHue(me.get('wizardColor1'))
     wizardDisplayObject = @wizardSprite.displayObject
     wizardDisplayObject.x = 50
     wizardDisplayObject.y = 85
@@ -53,12 +53,11 @@
     @stage.addChild wizardDisplayObject
     @stage.update()

-  onHover: (e) =>
-    if e.type is 'mouseenter'
-      @wizardSprite.queueAction 'cast'
-    else
-      @wizardSprite.queueAction 'idle'
-      @stage.update()
+  onMouseOverButton: ->
+    @wizardSprite.queueAction 'cast'
+
+  onMouseOutButton: ->
+    @wizardSprite.queueAction 'idle'

   updateStage: =>
     @stage.update()


Order fix

Order fix

--- test/generators/collection/sync.coffee
+++ test/generators/collection/sync.coffee
@@ -68,8 +68,8 @@

       class Collection extends Backbone.Collection
         url: "#{DATABASE_URL}/models"
+        model: SomeModel
         sync: SYNC(Collection)
-        model: SomeModel

       collection = new Collection()
       collection.fetch bbCallback (err, fetched_collection) ->


:art: Use Array map instead of Array forEach

--- lib/linter-registry.coffee
+++ lib/linter-registry.coffee
@@ -46,8 +46,7 @@
         ).catch (e) -> helpers.error(e)
       else return promise
     , Promise.resolve()).then( =>
-      Promises = []
-      @linters.forEach (linter) =>
+      Promises = @linters.map (linter) =>
         return unless helpers.shouldTriggerLinter(linter, false, onChange, scopes)
         try result = linter.lint(editor) catch e then return helpers.error(e)
         unless result instanceof Promise


Re-add check statements.

Re-add check statements.

--- files.coffee
+++ files.coffee
@@ -1440,6 +1440,8 @@
   ###
   findOne: (selector, options) ->
     console.info "[FilesCollection] [findOne(#{JSON.stringify(selector)}, #{JSON.stringify(options)})]" if @debug
+    check selector, Match.Optional Match.OneOf Object, String, null
+    check options, Match.Optional Object

     selector = {} unless arguments.length
     doc = @collection.findOne selector, options
@@ -1456,6 +1458,8 @@
   ###
   find: (selector, options) ->
     console.info "[FilesCollection] [find(#{JSON.stringify(selector)}, #{JSON.stringify(options)})]" if @debug
+    check selector, Match.Optional Match.OneOf Object, String, null
+    check options, Match.Optional Object

     selector = {} unless arguments.length
     return new FilesCursor selector, options, @


Enhancements

Enhancements

 - Avoid `JSON.stringify` and `JSON.parse` for HTTP uploads (*1.5x
improvement*)
 - Overall optimisation

--- files.coffee
+++ files.coffee
@@ -38,6 +38,7 @@
         return

       @stream.on 'error', (error) -> bound ->
+        console.error "[FilesCollection] [writeStream] [ERROR:]", error
         return

     ###
@@ -710,20 +711,27 @@
         if !!~request._parsedUrl.path.indexOf "#{self.downloadRoute}/#{self.collectionName}/__upload"
           if request.method is 'POST'

-            body        = ''
             handleError = (error) ->
               console.warn "[FilesCollection] [Upload] [HTTP] Exception:", error
               response.writeHead 500
               response.end JSON.stringify {error}
               return

+            body = ''
             request.on 'data', (data) -> bound ->
               body += data
               return

             request.on 'end', -> bound ->
               try
-                opts            = JSON.parse body
+                opts = fileId: request.headers['x-fileid']
+
+                if request.headers['x-eof'] is '1'
+                  opts.eof = true
+                else
+                  opts.binData = new Buffer body, 'base64'
+                  opts.chunkId = parseInt request.headers['x-chunkid']
+
                 user            = self._getUser {request, response}
                 _continueUpload = self._continueUpload opts.fileId
                 unless _continueUpload
@@ -868,13 +876,13 @@
           chunkId: Match.Optional Number
         }

+        opts.binData = new Buffer(opts.binData, 'base64') if opts.binData
         _continueUpload = self._continueUpload opts.fileId
         unless _continueUpload
           throw new Meteor.Error 408, 'Can\'t continue upload, session expired. Start upload again.'

         @unblock()
         {result, opts} = self._prepareUpload _.extend(opts, _continueUpload), @userId, 'DDP'
-
         if opts.eof
           try
             return Meteor.wrapAsync(self._handleUpload.bind(self, result, opts))()
@@ -991,14 +999,13 @@
   ###
   _handleUpload: if Meteor.isServer then (result, opts, cb) ->
     self = @
-
     try
       if opts.eof
         @_currentUploads[result._id].end -> bound ->
           self.emit '_finishUpload', result, opts, cb
           return
       else
-        @_currentUploads[result._id].write opts.chunkId, new Buffer(opts.binData, 'base64'), cb
+        @_currentUploads[result._id].write opts.chunkId, opts.binData, cb
     catch e
       cb and cb e
     return
@@ -1554,9 +1561,9 @@
     sendChunk: (evt) ->
       self = @
       opts =
-        fileId:     @fileId
-        binData:    evt.data.bin
-        chunkId:    evt.data.chunkId
+        fileId:  @fileId
+        binData: evt.data.bin
+        chunkId: evt.data.chunkId

       @emitEvent 'data', [evt.data.bin]
       if @pipes.length
@@ -1567,7 +1574,7 @@
         console.timeEnd('loadFile ' + @config.file.name) if @collection.debug
         @emitEvent 'readEnd'

-      if opts.binData and opts.binData.length
+      if opts.binData
         if @config.transport is 'ddp'
           Meteor.call @collection._methodNames._Write, opts, (error) ->
             self.transferTime += (+new Date) - evt.data.start
@@ -1583,8 +1590,13 @@
               self.emitEvent 'calculateStats'
             return
         else
-          opts.file.meta = fixJSONStringify opts.file.meta if opts?.file?.meta
-          HTTP.call 'POST', "#{@collection.downloadRoute}/#{@collection.collectionName}/__upload", {data: opts}, (error, result) ->
+          HTTP.call 'POST', "#{@collection.downloadRoute}/#{@collection.collectionName}/__upload", {
+            content: opts.binData
+            headers:
+              'x-fileid':     opts.fileId
+              'x-chunkid':    opts.chunkId
+              'content-type': 'text/plain'
+          }, (error, result) ->
             self.transferTime += (+new Date) - evt.data.start
             if error
               if "#{error}" is "Error: network"
@@ -1615,7 +1627,13 @@
             self.emitEvent 'end', arguments
             return
         else
-          HTTP.call 'POST', "#{@collection.downloadRoute}/#{@collection.collectionName}/__upload", {data: opts}, (error, result) ->
+          HTTP.call 'POST', "#{@collection.downloadRoute}/#{@collection.collectionName}/__upload", {
+            content: ''
+            headers:
+              'x-eof':        1
+              'x-fileId':     opts.fileId
+              'content-type': 'text/plain'
+          }, (error, result) ->
             res      = JSON.parse result?.content or {}
             res.meta = fixJSONParse res.meta if res?.meta
             self.emitEvent 'end', [error, res]
@@ -1625,23 +1643,38 @@
     proceedChunk: (chunkId, start) ->
       self       = @
       chunk      = @config.file.slice (@config.chunkSize * (chunkId - 1)), (@config.chunkSize * chunkId)
-      fileReader = new FileReader
-
-      fileReader.onloadend = (evt) ->
+
+      if FileReader
+        fileReader = new FileReader
+
+        fileReader.onloadend = (evt) ->
+          self.emitEvent 'sendChunk', [{
+            data: {
+              bin: (fileReader?.result or evt.srcElement?.result or evt.target?.result).split(',')[1]
+              chunkId: chunkId
+              start: start
+            }
+          }]
+          return
+
+        fileReader.onerror = (e) ->
+          self.emitEvent 'end', [(e.target or e.srcElement).error]
+          return
+
+        fileReader.readAsDataURL chunk
+
+      else if FileReaderSync
+        fileReader = new FileReaderSync
+
         self.emitEvent 'sendChunk', [{
           data: {
-            bin: (fileReader?.result or evt.srcElement?.result or evt.target?.result).split(',')[1]
+            bin: fileReader.readAsDataURL(chunk).split(',')[1]
             chunkId: chunkId
             start: start
           }
         }]
-        return
-
-      fileReader.onerror = (e) ->
-        self.emitEvent 'end', [(e.target or e.srcElement).error]
-        return
-
-      fileReader.readAsDataURL chunk
+      else
+        self.emitEvent 'end', ['FileReader and FileReaderSync is not supported in this Browser!']
       return

     upload: ->
@@ -1756,7 +1789,8 @@
           else
             self.emitEvent 'sendChunk', [evt]
           return
-        @worker.onerror   = (e) ->
+        @worker.onerror   = (e) ->
+          console.error '[FilesCollection] [insert] [worker] [ERROR:]', e if self.collection.debug
           self.emitEvent 'end', [e.message]
           return

@@ -1769,7 +1803,7 @@
       self.emitEvent 'prepare'
       return @result

-    manual: ->
+    manual: ->
       self = @
       @result.start = ->
         self.emitEvent 'start'


comentário com caminho de arquivo para melhoria na usabilidade

--- lib/assets/javascripts/do-ui/tabs.coffee
+++ lib/assets/javascripts/do-ui/tabs.coffee
@@ -1,6 +1,10 @@
 #= require "do/helpers/jquery"
 #= require "do/helpers/string"

+
+# para testar e melhorar o código
+# /home/rplaurindo/projects/curriculum_selection/app/assets/javascripts/administrative/tabs.coffee
+# /home/rplaurindo/projects/curriculum_selection/app/views/admin/candidates/show.html.haml

 window.selectTabOnload = (className) ->
     list = $("nav ul.tabs")


jupyter: make  file set potentially more efficient

--- src/smc-webapp/jupyter/actions.coffee
+++ src/smc-webapp/jupyter/actions.coffee
@@ -940,6 +940,11 @@

         @syncdb.exit_undo_mode()

+        # We will re-use any existing ids to make the patch that defines changing
+        # to the contents of ipynb more efficient.   In case of a very slight change
+        # on disk, this can be massively more efficient.
+        existing_ids = @store.get('cell_list').toJS()
+
         # delete everything
         @syncdb.delete(undefined, false)

@@ -948,7 +953,7 @@

         # Read in the cells
         if ipynb.cells?
-            pos = 0
+            n = 0
             for cell in ipynb.cells
                 if cell.source?
                     # "If you intend to work with notebook files directly, you must allow multi-line
@@ -960,10 +965,10 @@
                         input = cell.source
                 set
                     type  : 'cell'
-                    id    : @_new_id()
-                    pos   : pos
+                    id    : existing_ids[n] ? @_new_id()
+                    pos   : n
                     input : input
-                pos += 1
+                n += 1

         # Set the kernel and other settings
         kernel = ipynb.metadata?.kernelspec?.name


Fix the way we compute the number of active servers (remove ghost)

--- admin/static/coffee/sidebar.coffee
+++ admin/static/coffee/sidebar.coffee
@@ -71,15 +71,18 @@
             @data = ''

         render: =>
+            servers_active = 0
+            for machine_id in directory.models
+                if directory.get(machine.get('id'))? # Clean ghost
+                    servers_active++
+
             data =
-                servers_active: directory.length
+                servers_active: servers_active
                 servers_total: machines.length

             data_in_json = JSON.stringify data
             if @data isnt data_in_json
-                @.$el.html @template
-                    servers_active: directory.length
-                    servers_total: machines.length
+                @.$el.html @template data
                 @data = data_in_json
             return @

@@ -113,9 +116,13 @@
                     datacenters_active: dc_visible.length
                     datacenters_total: datacenters.models.length
             else
+                servers_active = 0
+                for machine_id in directory.models
+                    if directory.get(machine.get('id'))? # Clean ghost
+                        servers_active++
                 conn =
                     datacenters_exist: false
-                    servers_active: directory.length
+                    servers_active: servers_active
                     servers_total: machines.length

             return conn


support for regular expressions too test

--- spec-e2e/routing-spec.coffee
+++ spec-e2e/routing-spec.coffee
@@ -141,13 +141,19 @@

      Then -> expect(@res).toEqual 'play nice'

-  describe 'should support wild cards', ->
+  describe 'should support wild cards and regex', ->

     Given -> @hit = 0

     Given ->
       @a = require('./..')()
       @a.on 'some*', (sock, args, next) =>
+        @hit++
+        next()
+      @a.on '*event', (sock, args, next) =>
+        @hit++
+        next()
+      @a.on '[\w\s]+', (sock, args, next) =>
         @hit++
         next()

@@ -165,4 +171,4 @@
       @socket.on 'some event', ->
         done()

-    Then -> expect(@hit).toBe 1
+    Then -> expect(@hit).toBe 3


Social: move all helper functions to the bottom

--- client/Main/socialapicontroller.coffee
+++ client/Main/socialapicontroller.coffee
@@ -1,29 +1,6 @@
 class SocialApiController extends KDController
   constructor: (options = {}, data) ->
     super options, data
-
-  message:
-    edit                  :(rest...)-> messageApiFunc 'edit', rest...
-    post                  :(rest...)-> messageApiFunc 'post', rest...
-    reply                 :(rest...)-> messageApiFunc 'reply', rest...
-    delete                :(rest...)->
-      KD.remote.api.SocialMessage.delete rest...
-    like                  :(rest...)->
-      KD.remote.api.SocialMessage.like rest...
-    unlike                :(rest...)->
-      KD.remote.api.SocialMessage.unlike rest...
-
-  channel:
-    list               : fetchChannels
-    fetchActivity      : fetchChannelActivity
-    fetchGroupActivity : fetchGroupActivity
-    fetchPopularTopics : fetchPopularTopics
-    listPinnedMessages : (rest...)->
-      channelApiFuncMultiRes 'listPinnedMessages', rest...
-    pin                : (rest...)->
-      KD.remote.api.SocialChannel.pinMessage rest...
-    unpin              : (rest...)->
-      KD.remote.api.SocialChannel.unpinMessage rest...

   getCurrentGroup = (callback)->
     groupsController = KD.getSingleton "groupsController"
@@ -121,3 +98,26 @@
       revivedChannels.push c

     return revivedChannels
+
+  message:
+    edit                  :(rest...)-> messageApiFunc 'edit', rest...
+    post                  :(rest...)-> messageApiFunc 'post', rest...
+    reply                 :(rest...)-> messageApiFunc 'reply', rest...
+    delete                :(rest...)->
+      KD.remote.api.SocialMessage.delete rest...
+    like                  :(rest...)->
+      KD.remote.api.SocialMessage.like rest...
+    unlike                :(rest...)->
+      KD.remote.api.SocialMessage.unlike rest...
+
+  channel:
+    list               : fetchChannels
+    fetchActivity      : fetchChannelActivity
+    fetchGroupActivity : fetchGroupActivity
+    fetchPopularTopics : fetchPopularTopics
+    listPinnedMessages : (rest...)->
+      channelApiFuncMultiRes 'listPinnedMessages', rest...
+    pin                : (rest...)->
+      KD.remote.api.SocialChannel.pinMessage rest...
+    unpin              : (rest...)->
+      KD.remote.api.SocialChannel.unpinMessage rest...


SyncController: remove file content from storage after save.

--- client/Main/synclocalcontroller.coffee
+++ client/Main/synclocalcontroller.coffee
@@ -81,10 +81,11 @@

   updateFileContentOnLocalStorage: (file, content)->
     fileName = @getFileFullPath file
-    @saveToLocalStorage fileName, content
+    @storage.setValue "OE-#{fileName}", content

-  saveToLocalStorage: (fileName, contents)->
-    @storage.setValue "OE-#{fileName}", contents
+  removeFileContentFromLocalStorage: (file)->
+    fileName = @getFileFullPath file
+    @storage.unsetKey "OE-#{fileName}"

   addToOpenedFiles: (fileName)->
     vmName = FSHelper.getVMNameFromPath fileName
@@ -100,6 +101,7 @@
       @openedFiles.splice index, 1
       @storage.setValue "openedFiles", @openedFiles
       @removeFromSaveArray file
+      @removeFileContentFromLocalStorage file

   getRecentOpenedFiles: ->
     @openedFiles
@@ -109,11 +111,5 @@
     fileName = "[#{file.vmName}]#{plainPath}"
     return fileName

-  saveOpenedTabsContentToLocalStorage: ->
-    {aceViews} = KD.singletons.appManager.get("Ace").mainView
-    for filePath in Object.keys aceViews
-      content = aceViews[filePath].ace.getContents()
-      @saveToLocalStorage filePath, content
-
   saveEditorHistory: ->
     log "NOT IMPLEMENTED YET"


Client: Battle window scrolls up to the battle pane itself.

--- client/app/js/views/battle_view.coffee
+++ client/app/js/views/battle_view.coffee
@@ -821,8 +821,11 @@
     done()

   continueTurn: (done) =>
+    $battleWindow = @$el.closest('.battle_window')
     @$('.battle_summary').empty().hide()
-    @$el.closest('.battle_window').scrollTop(0)
+    offset = @$('.battle_pane').offset().top + $battleWindow.scrollTop()
+    offset -= $battleWindow.offset().top
+    $battleWindow.scrollTop(offset)
     done()

   makeMove: (e) =>


Use project.addBuffer instead of window.deserialize

--- spec/text-buffer-spec.coffee
+++ spec/text-buffer-spec.coffee
@@ -935,13 +935,13 @@
       it "loads the current contents of the file at the serialized path", ->
         expect(buffer.isModified()).toBeFalsy()

-        state = buffer.serialize()
+        state = buffer.getState().clone()
         state.get('text').insertTextAtPoint([0, 0], 'simulate divergence of on-disk contents from serialized contents')

-        buffer2 = deserialize(state, {project})
-
-        waitsFor ->
-          buffer2.cachedDiskContents
+        buffer2 = project.addBuffer(new TextBuffer(state))
+
+        waitsForPromise ->
+          buffer2.load()

         runs ->
           expect(buffer2.isModified()).toBeFalsy()


remove blank lines

remove blank lines

--- lib/provider/search.coffee
+++ lib/provider/search.coffee
@@ -22,13 +22,12 @@
     string.replace(/\\\//g, '/')
   else
     ''
+
 runCommand = (options) ->
   new BufferedProcess(options).onWillThrowError ({error, handle}) ->
     if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0
       console.log "ERROR"
     handle()
-
-

 RegExpForOutPutLine = /^(.*?):(\d+):(\d+):(.*)$/
 getOutputterForProject = (project, items) ->


Allow RootView::open to not change focus

--- src/root-view.coffee
+++ src/root-view.coffee
@@ -172,6 +172,7 @@
   #
   # Returns a promise that resolves to the {EditSession} for the file URI.
   open: (filePath, options={}) ->
+    changeFocus = options.changeFocus ? true
     filePath = project.resolve(filePath)
     initialLine = options.initialLine
     activePane = @getActivePane()
@@ -188,7 +189,7 @@
         @panes.setRoot(activePane)

       activePane.showItem(editSession)
-      activePane.focus()
+      activePane.focus() if changeFocus
       editSession

   # Private: Only used in specs


Log open exception stack to console

Log open exception stack to console

--- src/root-view.coffee
+++ src/root-view.coffee
@@ -179,15 +179,18 @@
     editSession = activePane.itemForUri(project.relativize(filePath)) if activePane and filePath
     promise = project.open(filePath, {initialLine}) if not editSession

-    Q(editSession ? promise).then (editSession) =>
-      if not activePane
-        activePane = new Pane(editSession)
-        @panes.setRoot(activePane)
-
-      activePane.showItem(editSession)
-      activePane.focus() if changeFocus
-      @trigger "uri-opened"
-      editSession
+    Q(editSession ? promise)
+      .then (editSession) =>
+        if not activePane
+          activePane = new Pane(editSession)
+          @panes.setRoot(activePane)
+
+        activePane.showItem(editSession)
+        activePane.focus() if changeFocus
+        @trigger "uri-opened"
+        editSession
+      .catch (error) ->
+        console.error(error.stack ? error)

   # Private: Only used in specs
   openSync: (uri, {changeFocus, initialLine, pane, split}={}) ->


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/timed_events.coffee
+++ src/timed_events.coffee
@@ -13,7 +13,7 @@

 @see GameObject

-Timed module
+TimedEvents module
 @name Bounded
 @module
 @constructor


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -23,7 +23,7 @@

   tween: (duration, properties) ->
     for property, target of properties
-      activeTweens[property] =
+      I.activeTweens[property] =
         end: target
         start: I[property]
         easing: "linear"


Added rudimentary tweening, wut!

Added rudimentary tweening, wut!

--- src/tween.coffee
+++ src/tween.coffee
@@ -36,7 +36,7 @@

   @name tween
   @methodOf Tween#
-  @param {Number} duration How long (in frames) until the object's properties reach their final values
+  @param {Number} duration How long (in frames) until the object's properties reach their final values.
   @param {Number} [frequency=3] The number of frames in between opacity changes
   @param {Number} [alpha=0.5] The alpha value to flicker to
   ###


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/tween.coffee
+++ src/tween.coffee
@@ -14,9 +14,8 @@
   # Add events and methods here
   self.bind "update", ->
     for property, data of I.activeTweens
-      I[property] = 10
+      I[property] = E

-
   tween: (duration, properties) ->
     for property, target of properties
       activeTweens[property] =


Cleaned up pkgmgr.

Cleaned up pkgmgr.

--- src/pkgmgr.coffee
+++ src/pkgmgr.coffee
@@ -26,15 +26,13 @@

     # The default version is `master`.
     @version = 'master' if not @version or @version is '*'
+    @remote = 'https://raw.github.com'
     logging.info "Installing #{@repo}@#{@version}..."

-    @remote = 'https://raw.github.com'
-    @slug = "#{@repo}@#{@version}"
-
-    # Use `repo@version` to de-duplicate in-flight requests.
-    if inFlight[@slug]
+    # De-duplicate in-flight requests.
+    if inFlight[@repo]
       @install = @emit.bind(@, 'end')
-    inFlight[@slug] = true
+    inFlight[@repo] = true

   dirname: ->
     sysPath.join(project.clientComponentsDir, @repo)
@@ -51,39 +49,28 @@
     if @repo.indexOf('/') < 0
       logging.fatal "Invalid repo: #{@repo}"

-    # If pkgInfo is given, use it to create a local
-    # `component.json` file, then fetch the required files.
+    # If `pkgInfo` is given, use it as `component.json`.
     # Otherwise, fetch `component.json` from the remote repo.
     if @pkgInfo
-      @writeJSON @processJSON
+      @processJSON(@pkgInfo)
     else
-      @getJSON @processJSON
-
-  # Create a local `component.json` from pkgInfo.
-  writeJSON: (callback) ->
-    json = @pkgInfo
-    callback(null, json)
+      @fetchJSON @processJSON

   # Fetch `component.json` from the remote repo.
-  getJSON: (done) ->
+  fetchJSON: (done) ->
     url = @url('component.json')
+    logging.info "Fetching #{url}"

-    logging.info "fetching #{url}"
     request.get url, null, (err, content) ->
-      if err
-        done(err)
-      else
-        try
-          json = JSON.parse(content)
-          done(null, json)
-        catch e
-          logging.fatal "Failed to fetch #{url}"
-          done(e)
+      if err then logging.fatal err
+      try
+        json = JSON.parse(content)
+        done(json)
+      catch e
+        logging.fatal "Failed to fetch #{url}.\n#{e}"

-  processJSON: (err, json) =>
-    if err
-      return @emit('error', err)
-
+  processJSON: (json) =>
+    # Gather all the required files
     files = []
     if json.main then files.push(json.main)
     if json.scripts then files = files.concat(json.scripts)
@@ -97,53 +84,49 @@
     files = _.uniq(files)

     async.parallel [
-      # get dependencies
+      # Get dependencies
       (done) =>
         if json.dependencies
           @getDependencies json.dependencies, done
         else
           done(null)

-      # download the files
+      # Download the files
       (done) =>
         async.each files, @getFile, done

-      # save component.json
+      # Save component.json
       (done) =>
         fs.mkdirSync @dirname()
-        json = JSON.stringify(json, null, 2)
-        @writeFile 'component.json', json, done
+        @writeFile 'component.json', JSON.stringify(json, null, 2), done

     ], (err, results) =>
-      if err
-        @emit 'error', err
-      else
-        @emit 'end'
+      if err then logging.fatal err
+      @emit 'end'

   # Fetch a single file and write it to disk.
   getFile: (file, done) =>
     url = @url(file)
-    logging.info "fetching #{url}"
+    logging.info "Fetching #{url}"

-    @emit 'file', file, url
-    dst = @join(file)
-    fs.mkdirSync sysPath.dirname(dst)
+    # Create the dir
+    dest = @join(file)
+    fs.mkdirSync sysPath.dirname(dest)

-    # download file and save to disk
-    request.get url, dst, (err, res) ->
-      if err then console.log "Problem with request #{err.message}"
+    # Download the file and save it to disk.
+    request.get url, dest, done

   # Write a file to disk.
   writeFile: (file, str, callback) ->
     file = @join(file)
-    logging.info "writing file #{file}"
+    logging.info "Writing file #{file}"
     fs.writeFile file, str, callback

   # Install dependencies,
   getDependencies: (deps, callback) ->
     pkgs = []
-    for name, version of deps
-      pkg = new Package(name, version)
+    for repo, version of deps
+      pkg = new Package(repo, version)
       pkgs.push pkg

     getPkg = (pkg, done) =>


Fix sudden unexpected velocity.x drop during jump.

Removing something that doesn't make sense, at least anymore.

Also just update acceleration.y (don't clobber x component) on jump.

--- src/player.coffee
+++ src/player.coffee
@@ -101,7 +101,6 @@
       @physics.collideWorldBounds = on

       @physics.drag.x = 1500
-      @_originalDrag = @physics.drag.clone()

       h = @sprite.height
       w = @sprite.width
@@ -144,7 +143,6 @@
       if ending
         return unless @_jumpTimer.running
         @acceleration.y = @gravity.y
-        @physics.drag.x = @_originalDrag.x
         @debug 'jump:end', @_jumpTimer.ms, { position: @physics.position }
         @_jumpTimer.stop()
         return
@@ -156,8 +154,7 @@
       @nextState = 'rising'
       # Faster the run, higher the base jump, up to 33% improvement.
       kVelocity = (3 + Math.abs @velocity.x / @maxVelocity.x) / 4
-      @acceleration.set 0, @jumpAcceleration * kVelocity
-      @physics.drag.x = 2 * @_originalDrag.x
+      @acceleration.y = @jumpAcceleration * kVelocity

     _run: (direction) ->
       # Chain a start animation to the landing.


switch to encodeUriComponent to handle url params

fixes #38

--- assets/js/views/_single_inline_share.coffee
+++ assets/js/views/_single_inline_share.coffee
@@ -5,9 +5,9 @@
     template: templates.single_inline_share

     setUrl: ->
-      url = encodeURI(App.request('socialEmbed').get('link'))
-      text = encodeURI(App.request('socialEmbed').get('message'))
-      mediaUrl = encodeURI(App.request('socialEmbed').get('mediaUrl'))
+      url = encodeURIComponent(App.request('socialEmbed').get('link'))
+      text = encodeURIComponent(App.request('socialEmbed').get('message'))
+      mediaUrl = encodeURIComponent(App.request('socialEmbed').get('mediaUrl'))

       @$el.attr 'href', @model.get('shareUrl')(url, text, mediaUrl)


Remove the report ID from the Reports page

--- app/choose-subject-set.cjsx
+++ app/choose-subject-set.cjsx
@@ -21,7 +21,7 @@

Choose a report to work on

-        {
{subject_set.display_name}
{subject_set.metadata.BOROUGH.replace(' (London, England)', '')}
{subject_set.metadata.Date}
 for subject_set in @state.subject_sets}
+        {
{subject_set.metadata.BOROUGH.replace(' (London, England)', '')}
{subject_set.metadata.Date}
 for subject_set in @state.subject_sets}


Finder: update finder appStorage version to 1.2

--- client/landing/StartTab/AppView.coffee
+++ client/landing/StartTab/AppView.coffee
@@ -8,7 +8,7 @@

     @appIcons   = {}
     appStorages = KD.getSingleton('appStorageController')
-    @appStorage = appStorages.storage 'Finder', '1.1.1'
+    @appStorage = appStorages.storage 'Finder', '1.2'
     @_connectAppsController()

     # Main view elements


Set partial option as default value

Set partial option as default value

--- client/ide/lib/views/statusbar/idechatheadwatchitemview.coffee
+++ client/ide/lib/views/statusbar/idechatheadwatchitemview.coffee
@@ -6,7 +6,7 @@

   constructor: (options = {}, data) ->

-    options.partial = 'Watch'
+    options.partial ?= 'Watch'

     super options, data


split keymap into two: dyalog and dyalogDefault

--- client/keymap.coffee
+++ client/keymap.coffee
@@ -5,7 +5,7 @@
 window.onhelp = -> false # prevent IE from acting silly on F1

 prefs.prefixKey (x, old) -> # change listener
-  if x != old then m = CodeMirror.keyMap.dyalog; m["'#{x}'"] = m["'#{old}'"]; delete m["'#{old}'"]
+  if x != old then m = CodeMirror.keyMap.dyalogDefault; m["'#{x}'"] = m["'#{old}'"]; delete m["'#{old}'"]
   return

 squiggleDescriptions =
@@ -28,7 +28,7 @@

 ctid = 0 # backquote completion timeout id

-CodeMirror.keyMap.dyalog = inherit fallthrough: 'default', F1: (cm) ->
+CodeMirror.commands.HLP = (cm) ->
   c = cm.getCursor(); s = cm.getLine(c.line).toLowerCase()
   u =
     if      m = /^ *(\)[a-z]+).*$/.exec s then helpurls[m[1]] || helpurls.WELCOME
@@ -44,7 +44,9 @@
     .focus?()
   return

-CodeMirror.keyMap.dyalog["'#{prefs.prefixKey()}'"] = (cm) ->
+CodeMirror.keyMap.dyalog = fallthrough: 'dyalogDefault'
+CodeMirror.keyMap.dyalogDefault = fallthrough: 'default', F1: 'HLP', End: 'goLineEndSmart'
+CodeMirror.keyMap.dyalogDefault["'#{prefs.prefixKey()}'"] = (cm) ->
   if cm.dyalogBQ
     c = cm.getCursor(); cm.replaceSelection prefs.prefixKey(), 'end'
   else
@@ -217,7 +219,7 @@
   [70] -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
   [80] -- -- -- -- -- -- TO MO -- -- -- -- -- S1 S2 OS
 '''.replace(/\[.*?\]/g, '').replace(/^\s*|\s*$/g, '').split(/\s+/).forEach (xx, i) ->
-  if xx != '--' then createCommand xx; CodeMirror.keyMap.dyalog["'#{chr 0xf800 + i}'"] = xx
+  if xx != '--' then createCommand xx; CodeMirror.keyMap.dyalogDefault["'#{chr 0xf800 + i}'"] = xx
   return

 '''
@@ -233,11 +235,9 @@
   TL Ctrl-Up
   WI Ctrl-Pause
 '''.split('\n').forEach (l) ->
-  [xx, keys...] = l.split /\s+/; createCommand xx; keys.forEach (key) -> CodeMirror.keyMap.dyalog[key] = xx; return
-
-# CodeMirror provides a goLineStartSmart but not a goLineEndSmart command.
-CodeMirror.keyMap.dyalog.End = 'goLineEndSmart'
-CodeMirror.commands.goLineEndSmart = (cm) ->
+  [xx, keys...] = l.split /\s+/; createCommand xx; keys.forEach (key) -> CodeMirror.keyMap.dyalogDefault[key] = xx; return
+
+CodeMirror.commands.goLineEndSmart = (cm) -> # CodeMirror provides a goLineStartSmart but not a goLineEndSmart command.
   cm.extendSelectionsBy(
     ->
       c = cm.getCursor(); l = c.line; s = cm.getLine l; n = s.length; m = s.replace(/\ +$/, '').length


Rmove stale debugger

Rmove stale debugger

--- app/controllers/file.coffee
+++ app/controllers/file.coffee
@@ -32,7 +32,6 @@
       resolve = (result) ->
         Notify.info "Manual assessment requested."
       reject = (result)->
-        debugger
         Notify.error "Something went wrong."
       xhr.then resolve, reject


Update individual content routes to be admin-only (#181)


tests: postComment helper made faster

tests: postComment helper made faster

--- client/test/lib/helpers/helpers.coffee
+++ client/test/lib/helpers/helpers.coffee
@@ -194,7 +194,7 @@
     if shouldPostActivity
       @postActivity(browser)

-    comment = @getFakeText()
+    comment = @getFakeText 50

     @doPostComment(browser, comment, shouldAssert)

@@ -203,29 +203,22 @@

   doPostComment: (browser, comment, shouldAssert = yes, hasEmbeddable = no) ->
     browser
-      .click                    activitySelector + ' [testpath=CommentInputView]'
-      .setValue                 activitySelector + ' [testpath=CommentInputView]', comment
-      .waitForElementVisible    activitySelector + ' .comment-container .comment-input-wrapper', 20000
+      .click                      activitySelector + ' [testpath=CommentInputView]'
+      .setValue                   activitySelector + ' [testpath=CommentInputView]', comment

     if hasEmbeddable
-      browser.pause 3000
-
-    browser
-      .click                    activitySelector + ' .has-markdown' # blur
-      .pause                    3000 # content preview
-
-    if hasEmbeddable
-      browser
-        .waitForElementVisible  '.comment-input-widget .link-embed-box', 20000
-
-    browser
-      .click                    activitySelector + ' .comment-container button[testpath=post-activity-button]'
-
+      browser
+        .click                    activitySelector + ' .has-markdown' # blur
+        .pause                    3000 # content preview
+        .waitForElementVisible    '.comment-input-widget .link-embed-box', 20000
+
+    browser
+      .click                      activitySelector + ' .comment-container button[testpath=post-activity-button]'

     if shouldAssert
       browser
-        .pause               6000 # required
-        .assert.containsText '[testpath=ActivityListItemView]:first-child .comment-body-container', comment # Assertion
+        .pause                    6000 # required
+        .assert.containsText      '[testpath=ActivityListItemView]:first-child .comment-body-container', comment # Assertion

   doPostActivity: (browser, post, shouldAssert = yes, hasEmbeddable = no) ->


deleteing feeds will actually delete the data in db now

--- app/js/classes/feeds.coffee
+++ app/js/classes/feeds.coffee
@@ -1,14 +1,15 @@
 app = @App
+FEEDS_URL = '/v1/feeds'

 @App.FeedsRoute = Ember.Route.extend
 	setupController: (controller, model)->
-		console.log 'v1 feeds'
 		$.get('/v1/feeds').done (json)->
 			# console.log json
 			data = JSON.parse(json)
 			console.log data
 			feeds = data.map( (f)->
-				o = Ember.Object.create().setProperties(f.feed)
+				properties = $.extend(f, f.feed)
+				o = Ember.Object.create().setProperties(properties)
 				o.items = f.feed.items.map( (i)->
 					Ember.Object.create().setProperties(i)
 				)
@@ -26,10 +27,15 @@
 	toggleEditMode: ->
 		@set('isEditMode', !@get('isEditMode'))
 	deleteFeed: (feed)->
-		if @get('selectedFeed') == feed
-			@set('selectedItem', null)
-			@set('selectedFeed', null)
-		@get('content').removeObject(feed)
+		$.ajax {
+			url: FEEDS_URL + '/' + feed.id,
+			type: 'DELETE',
+			success: =>
+				if @get('selectedFeed') == feed
+					@set('selectedItem', null)
+					@set('selectedFeed', null)
+				@get('content').removeObject(feed)
+		}
 	selectFeed: (feed)->
 		@get('content').forEach( (i)->
 			i.set('selected', false)


scale mark-too-close validation to zoom

scale mark-too-close validation to zoom

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -18,7 +18,7 @@

     @canvas.addEventListener 'mousedown', (e) =>
       e.preventDefault()
-      unless @marks.markTooCloseToAnotherMark(e)
+      unless @marks.markTooCloseToAnotherMark(e, @scale)
         @mark = new Mark(e, @)
         @marks.create(@mark)
         @mark.draw(e)
@@ -108,12 +108,12 @@

   toCanvasXPoint: (e) -> e.pageX - e.target.getBoundingClientRect().left - window.scrollX

-  markTooCloseToAnotherMark: (e) ->
+  markTooCloseToAnotherMark: (e, scale) ->
     mouseLocation = @toCanvasXPoint(e)
     markBelow = Math.abs mouseLocation - @closestXBelow(mouseLocation)
     markAbove = Math.abs mouseLocation - @closestXAbove(mouseLocation)
     # larger distance below because marks are created to left of cursor
-    if markBelow < 24 or markAbove < 12 then true else false
+    if markBelow < (24*scale) or markAbove < (12*scale) then true else false

 class Mark
   constructor: (e, @canvasGraph) ->


quick fix: accidentaly broke y-axis! (oops)

--- app/lib/canvas-graph.coffee
+++ app/lib/canvas-graph.coffee
@@ -121,7 +121,7 @@
         mark.element.style.left = (scaledMin) + "px"
         mark.save(scaledMin, scaledMax)

-    @drawYTickMarks([-1..1], [0..@canvas.height], 1, 2)
+    @drawYTickMarks([0..20], [0..@canvas.height], 1, 2)
     @drawXTickMarks(xMin, xMax)

     @scale = (@largestX - @smallestX) / (@xMax - @xMin)


remove token > 3 limitation for repeat, sequence and spatial matching

--- matching.coffee
+++ matching.coffee
@@ -198,7 +198,7 @@
         j += 1
       # otherwise push the pattern discovered so far, if any...
       else
-        if j - i > 2 # only consider length-3 chains and up
+        if j - i > 1 # don't consider length-1 chains
           result.push
             pattern: 'spatial'
             i: i
@@ -226,7 +226,7 @@
       if password.charAt(j-1) == password.charAt(j)
         j += 1
       else
-        if j - i > 2 # length-3 chains and up
+        if j - i > 1
           result.push
             pattern: 'repeat'
             i: i
@@ -266,7 +266,7 @@
         if cur_n - prev_n == seq_direction
           j += 1
         else
-          if j - i > 2 # length-3 chains and up
+          if j - i > 1
             result.push
               pattern: 'sequence'
               i: i


Fixed #1321.

Fixed #1321.

--- app/lib/surface/Mark.coffee
+++ app/lib/surface/Mark.coffee
@@ -66,10 +66,10 @@
     @mark = new createjs.Container()
     @mark.mouseChildren = false
     style = @sprite.thang.drawsBoundsStyle
+    @drawsBoundsIndex = @sprite.thang.drawsBoundsIndex
     return if style is 'corner-text' and @sprite.thang.world.age is 0

     # Confusingly make some semi-random colors that'll be consistent based on the drawsBoundsIndex
-    @drawsBoundsIndex = @sprite.thang.drawsBoundsIndex
     colors = (128 + Math.floor(('0.'+Math.sin(3 * @drawsBoundsIndex + i).toString().substr(6)) * 128) for i in [1 ... 4])
     color = "rgba(#{colors[0]}, #{colors[1]}, #{colors[2]}, 0.5)"
     [w, h] = [@sprite.thang.width * Camera.PPM, @sprite.thang.height * Camera.PPM * @camera.y2x]


Client: Sidebar: Feed: Deleted showPopular prop. Using resultListState instead

--- client/activity/lib/components/publicfeedpane/feedpanetabcontainer.coffee
+++ client/activity/lib/components/publicfeedpane/feedpanetabcontainer.coffee
@@ -5,12 +5,12 @@
 Link         = require 'app/components/common/link'
 classnames   = require 'classnames'
 ActivityFlux = require 'activity/flux'
+ResultState  = require 'activity/util/resultStates'

 module.exports = class FeedPaneTabContainer extends React.Component

   @defaultProps=
     thread              : immutable.Map()
-    showPopularMessages : no

   showPopularMessages: (event) ->
@@ -24,9 +24,7 @@

     kd.utils.stopDOMEvent event

-    ActivityFlux.actions.channel.setShowPopularMessagesFlag null
-
-    @handleRoute '/'
+    @handleRoute '/Recent'

   handleRoute: (route) ->
@@ -39,12 +37,12 @@

   getMostRecentTabClassNames: -> classnames
     'FeedPane-tab' : yes
-    'active'       : not @props.showPopularMessages
+    'active'       : @props.thread.getIn(['flags', 'resultListState']) is ResultState.RECENT

   getMostLikedTabClassNames: -> classnames
     'FeedPane-tab' : yes
-    'active'       : @props.showPopularMessages
+    'active'       : @props.thread.getIn(['flags', 'resultListState']) is ResultState.LIKED

   render: ->


payment: refactor billing methods list view

(used currently on the account page; will be used also on the group dashboard)

--- client/app/Applications/Account.kdapplication/views/paymentmethods.coffee
+++ client/app/Applications/Account.kdapplication/views/paymentmethods.coffee
@@ -10,33 +10,27 @@
       @loadItems()

     list = @getListView()
-    list.on 'reload', (data)=>
-      @loadItems()
+
+    list.on 'ItemWasAdded', (item) =>
+      item.on 'PaymentMethodEditRequested', @bound 'editPaymentMethod'
+      item.on 'PaymentMethodRemoveRequested', @bound 'removePaymentMethod'
+
+    list.on 'reload', (data) => @loadItems()
+
+  editPaymentMethod: ({ recurlyId }) ->
+    @showModal()
+
+  removePaymentMethod: ({ recurlyId }) ->
+    paymentController = KD.getSingleton 'paymentController'
+    paymentController.removePaymentMethod recurlyId

   loadItems: ->
     @removeAllItems()
     @showLazyLoader no
-    KD.remote.api.JPaymentPlan.fetchAccountDetails (err, res) =>
-      accounts = []
-      if err
-        @instantiateListItems []
-        @hideLazyLoader()
-      unless err
-        if res.billing
-          { cardFirstName, cardLastName, cardNumber, cardType, cardMonth,
-            cardYear, address1, address2, city, state, zip } = res.billing

-          accounts.push
-            title        : "#{cardFirstName} #{cardLastName}"
-            type         : cardType
-            cardNumber   : cardNumber
-            cardExpiry   : [ cardMonth, cardYear ].join '/'
-            cardAddress  : [ address1, address2 ].filter(Boolean).join ' '
-            cardCity     : city
-            cardState    : state
-            cardZip      : zip
-        @instantiateListItems accounts
-        @hideLazyLoader()
+    KD.whoami().fetchPaymentMethods (err, paymentMethods) =>
+      @instantiateListItems paymentMethods
+      @hideLazyLoader()

   loadView:->
     super
@@ -46,8 +40,18 @@
       icon      : yes
       iconOnly  : yes
       iconClass : "plus"
-      callback  : =>
-        @getListView().showModal this
+      callback  : => @showModal()
+
+  showModal: ->
+    paymentController = KD.getSingleton 'paymentController'
+    modal = paymentController.createBillingInfoModal 'user', {}
+    paymentController.fetchBillingInfo 'user', (err, initialBillingInfo) ->
+
+      modal.setBillingInfo initialBillingInfo.billing  if initialBillingInfo?
+
+      modal.on 'PaymentInfoSubmitted', (updatedBillingInfo) ->
+        paymentController.updateBillingInfo updatedBillingInfo
+

 class AccountPaymentMethodsList extends KDListView
   constructor:(options,data)->
@@ -57,36 +61,28 @@
     ,options
     super options,data

-  showModal: (controller) ->
-    paymentController = KD.getSingleton 'paymentController'
-    modal = paymentController.createBillingInfoModal 'user', {}
-    paymentController.fetchBillingInfo 'user', (err, initialBillingInfo) ->
-
-      modal.setBillingInfo initialBillingInfo.billing
-
-      modal.on 'PaymentInfoSubmitted', (updatedBillingInfo) ->
-        paymentController.updateBillingInfo updatedBillingInfo
-
-
-      form = modal.modalTabs.forms["Billing Info"]
-
-
 class AccountPaymentMethodsListItem extends KDListItemView
-  constructor:(options,data)->
+  constructor:(options, data)->
     options.tagName = "li"
+    options.cssClass = 'credit-card-list-item'
     super options,data

-  viewAppended:->
-    @setPartial @partial @data
+    data = @getData()

-  partial:(data)->
+    @billingMethod = new BillingMethodView {}, @getData().billing
+
+    @editLink = new CustomLinkView
+      title: 'edit'
+      click: => @emit 'PaymentMethodEditRequested', data
+
+    @removeLink = new CustomLinkView
+      title: 'remove'
+      click: => @emit 'PaymentMethodRemoveRequested', data
+
+  viewAppended: JView::viewAppended
+
+  pistachio:->
     """
-

-
#{data.title} - #{data.type}

-
#{data.cardNumber} - #{data.cardExpiry}

-

-          #{data.cardAddress}

-          #{data.cardCity}, #{data.cardState} #{data.cardZip}
-

-

+    {{> @billingMethod}}
+
{{> @editLink}} | {{> @removeLink }}

     """


Changes in group create

Changes in group create

--- client/landing/app/Applications/Groups.kdapplication/AppController.coffee
+++ client/landing/app/Applications/Groups.kdapplication/AppController.coffee
@@ -454,20 +454,19 @@
       cssClass                       : "group-admin-modal compose-message-modal admin-kdmodal"
       width                          : 500
       overlay                        : yes
+      goToNextFormOnSubmit           : no
       tabs                           :
         navigable                    : no
         goToNextFormOnSubmit         : yes
         hideHandleContainer          : yes
         callback                     : (formData)=>
           _createGroupHandler.call @, formData, (err) =>
-            modal.modalTabs.forms["VM Settings"].buttons.Save.hideLoader()
+            modal.modalTabs.forms["VM Settings"].buttons["Create Group"].hideLoader()
             unless err
               modal.destroy()
         forms                        :
           "Select group type"        :
             title                    : 'Group type'
-            callback                 :(formData)=>
-              log "here"
             buttons                  :
               "Next"                 :
                 style                : "modal-clean-gray"
@@ -490,13 +489,22 @@
             buttons                  :
               "Next"                 :
                 style                : "modal-clean-gray"
-                type                 : "submit"
+                type                 : "button"
                 loader               :
                   color              : "#444444"
                   diameter           : 12
+                callback             : ->
+                  form = modal.modalTabs.forms["General Settings"]
+                  if form.inputs["Group VM"].getValue()
+                    modal.modalTabs.showNextPane()
+                  else
+                    modal.modalTabs.fireFinalCallback()
               "Back"                 :
                 style                : "modal-cancel"
-                callback             : -> modal.modalTabs.showPreviousPane()
+                callback             : ->
+                  form = modal.modalTabs.forms["Select group type"]
+                  form.buttons.Next.hideLoader()
+                  modal.modalTabs.showPreviousPane()
             fields                   :
               "Title"                :
                 label                : "Title"
@@ -508,7 +516,6 @@
                 keydown              : (pubInst, event)->
                   @utils.defer =>
                     makeSlug()
-
                 placeholder          : 'Please enter your group title...'
               "Slug"                 :
                 label                : "Address"
@@ -561,10 +568,14 @@
                 itemClass            : KDOnOffSwitch
                 name                 : "group-vm"
                 defaultValue         : yes
+                callback             : (state)->
+                  form = modal.modalTabs.forms["General Settings"]
+                  form.buttons.Next.setTitle unless state then "Create Group" \
+                                                          else "Next"
           "VM Settings"              :
             title                    : 'VM Settings'
             buttons                  :
-              "Save"                 :
+              "Create Group"         :
                 style                : "modal-clean-gray"
                 type                 : "submit"
                 loader               :
@@ -572,7 +583,10 @@
                   diameter           : 12
               "Back"                 :
                 style                : "modal-cancel"
-                callback             : -> modal.modalTabs.showPreviousPane()
+                callback             : ->
+                  modal.modalTabs.showPreviousPane()
+                  form = modal.modalTabs.forms["General Settings"]
+                  form.buttons.Next.hideLoader()
             fields                   :
               "VM Host"               :
                 label                : "VM Host"


Add factory for Form

Add factory for Form

--- app/scripts/services.coffee
+++ app/scripts/services.coffee
@@ -16,3 +16,130 @@
     apiResource $resource, 'waterpoints'
   .factory 'Facility', ($resource) ->
     apiResource $resource, 'facilities'
+  .factory 'Form', () ->
+    fields =
+      date_recorded:
+        type: "datetime"
+        label: "Date recorded"
+      company:
+        type: "text"
+        label: "Company"
+      region:
+        type: "text"
+        label: "Region"
+      district:
+        type: "text"
+        label: "District"
+      lga_name:
+        type: "text"
+        label: "LGA Name"
+      ward:
+        type: "text"
+        label: "Ward"
+      village:
+        type: "text"
+        label: "Village"
+      subvillage:
+        type: "text"
+        label: "Subvillage"
+      wpt_name:
+        type: "text"
+        label: "Waterpoint Name"
+      wpt_code:
+        type: "text"
+        label: "Waterpoint Code"
+      population:
+        type: "number"
+        label: "Population"
+      scheme_name:
+        type: "text"
+        label: "Scheme Name"
+      water_perm:
+        type: "text"
+        label: "Water Permission"
+      catchment:
+        type: "text"
+        label: "Catchment"
+      funder:
+        type: "text"
+        label: "Funder"
+      installer:
+        type: "text"
+        label: "Installer"
+      construction_year:
+        type: "datetime"
+        label: "Construction Year"
+      source_type:
+        type: "text"
+        label: "Source Type"
+      extraction:
+        type: "text"
+        label: "Extraction"
+      waterpoint:
+        type: "text"
+        label: "Waterpoint"
+      status_detail:
+        type: "text"
+        label: "Status Detail"
+      status:
+        type: "select"
+        label: "Status"
+        options:
+          "Functional":
+            label: "Functional"
+          "Not functional":
+            label: "Not Functional"
+      breakdown_year:
+        type: "datetime"
+        label: "Breakdown Year"
+      hardware_defect:
+        type: "text"
+        label: "Hardware Defect"
+      reason_wpt:
+        type: "text"
+        label: "Reason for Defect"
+      water_quantity:
+        type: "text"
+        label: "Water Quantity"
+      water_quality:
+        type: "text"
+        label: "Water Quality"
+      scheme_management:
+        type: "text"
+        label: "Scheme Management"
+      wp_management:
+        type: "text"
+        label: "Waterpoint Management"
+      water_payment:
+        type: "text"
+        label: "Water Payment"
+      amount_tsh:
+        type: "text"
+        label: "Amount (Tanzanian shilling)"
+      public_meeting:
+        type: "text"
+        label: "Public Meeting"
+      comment:
+        type: "text"
+        label: "Comment"
+      gps_height:
+        type: "text"
+        label: "GPS Height"
+      latitude:
+        type: "text"
+        minValue: -90
+        maxValue: 90
+        label: "Latitude"
+      longitude:
+        type: "text"
+        minValue: -180
+        maxValue: 180
+        label: "Longitude"
+    for f of fields
+      fields[f].class = "form-control"
+      fields[f].wrapper = '

'
+    fields.submit =
+      type: "submit"
+      label: "Save"
+      class: "btn btn-primary"
+    return fields


:racehorse: Don't pretty print serialized window state

--- src/atom.coffee
+++ src/atom.coffee
@@ -243,7 +243,7 @@
     windowState =
       site: site.serialize()
       document: @getWindowState().serialize()
-    windowStateJson = JSON.stringify(windowState, null, 2)
+    windowStateJson = JSON.stringify(windowState)
     if windowStatePath = @getWindowStatePath()
       fsUtils.writeSync(windowStatePath, "#{windowStateJson}\n")
     else


Silence some output when tests are running

--- content_scripts/vimium_frontend.coffee
+++ content_scripts/vimium_frontend.coffee
@@ -79,7 +79,8 @@
     return @continueBubbling unless DomUtils.isFocusable element
     unless @logged or element == document.body
       @logged = true
-      console.log "An auto-focusing action on this page was blocked by Vimium."
+      unless window.vimiumDomTestsAreRunning
+        console.log "An auto-focusing action on this page was blocked by Vimium."
     element.blur()
     @suppressEvent


Removed useless tracking

Removed useless tracking

--- core/app/backbone/views/evidence/add_evidence_form_view.coffee
+++ core/app/backbone/views/evidence/add_evidence_form_view.coffee
@@ -81,7 +81,6 @@
     @ui.switchToFactlink.show()
     @ui.switchToComment.hide()
     @inputRegion.switchTo 'add_comment_view'
-    mp_track "Evidence: Switching to comment"

   _switchToAddFactRelationView: ->
     @ui.switchToFactlink.hide()


Add missing good.require.

Add missing good.require.

--- app/client/js/errorreporter.coffee
+++ app/client/js/errorreporter.coffee
@@ -1,5 +1,6 @@
 goog.provide 'app.ErrorReporter'

+goog.require 'app.errors.InnocuousError'
 goog.require 'app.errors.SilentError'
 goog.require 'goog.Promise'
 goog.require 'goog.async.throwException'


Remove log

Remove log

--- app/components/image-selector.cjsx
+++ app/components/image-selector.cjsx
@@ -92,7 +92,6 @@
         img.onload = =>
           @cropImage img, file
         img.src = e.target.result
-      console.log {file}
       reader.readAsDataURL file

   cropImage: (srcImg, srcFile) ->


don't try to highlight classes,ns,etc as tradfns

--- client/codemirror-apl-mode.coffee
+++ client/codemirror-apl-mode.coffee
@@ -54,7 +54,7 @@
   token: (stream, state) ->
     if state.hdr
       delete state.hdr; stream.match /[^⍝\n\r]*/; s = stream.current()
-      if dfnHeader.test s
+      if /^\s*:/.test(s) || dfnHeader.test s
         stream.backUp s.length # re-tokenize without hdr
       else if /^\s*$/.test s
         delete state.vars


Show level in toppanel

Show level in toppanel

--- client/components/TopPanel.coffee
+++ client/components/TopPanel.coffee
@@ -23,6 +23,8 @@
                 if props.myUser?.user

                         <UserName user={props.myUser.user}/>
+
+                        {props.myUser.user.level.current}

                         {props.myUser.user.rating}
                         {" / "}


Fix ladder simulation exception (#5937)

Fix ladder simulation exception (#5937)

* handle malformed sessions

* handle malformed sessions

* grammar update

Co-authored-by: smallst_wsl <6c42689ff77371401bf43b7d56b93ac1ca3e92d3@163.com>

--- app/lib/simulator/Simulator.coffee
+++ app/lib/simulator/Simulator.coffee
@@ -67,15 +67,26 @@
         @simulatingPlayerStrings = {}
         for team in ['humans', 'ogres']
           session = _.find(taskData.sessions, {team: team})
+          unless session
+            @trigger 'statusUpdate', "Error simulating game: didn't find both teams' sessions. Trying another game in #{@retryDelayInSeconds} seconds."
+            @simulateAnotherTaskAfterDelay()
+            return
           @simulatingPlayerStrings[team] = "#{session.creatorName or session.creator} #{session.team}"
         @trigger 'statusUpdate', "Setting up #{taskData.sessions[0].levelID} simulation between #{@simulatingPlayerStrings.humans} and #{@simulatingPlayerStrings.ogres}"
         #refactor this
         @task = new SimulationTask(taskData)
+        try
+          levelID = @task.getLevelName()
+        catch err
+          console.error err
+          @trigger 'statusUpdate', "Error simulating game: #{err}. Trying another game in #{@retryDelayInSeconds} seconds."
+          @simulateAnotherTaskAfterDelay()
+          return

         @supermodel ?= new SuperModel()
         @supermodel.resetProgress()
         @stopListening @supermodel, 'loaded-all'
-        @levelLoader = new LevelLoader supermodel: @supermodel, levelID: @task.getLevelName(), sessionID: @task.getFirstSessionID(), opponentSessionID: @task.getSecondSessionID(), headless: true
+        @levelLoader = new LevelLoader supermodel: @supermodel, levelID: levelID, sessionID: @task.getFirstSessionID(), opponentSessionID: @task.getSecondSessionID(), headless: true

         if @supermodel.finished()
           @simulateSingleGame()


Added Needed ApiGet dependency in app.coffee.

--- app/scripts/controllers/app.coffee
+++ app/scripts/controllers/app.coffee
@@ -1,7 +1,7 @@
 'use strict'

 angular.module('appApp.controllers')
-  .controller 'AppCtrl', ['$scope', ($scope) ->
+  .controller 'AppCtrl', ['$scope', 'ApiGet', ($scope, ApiGet) ->
     # init root ctrl vars to share data between controllers
     $scope.reps = {}
     $scope.reps_list = []


Exempt deprecated editor events from warning message

Also name the command being listened for or triggered for clarity.

--- src/space-pen.coffee
+++ src/space-pen.coffee
@@ -473,7 +473,7 @@
 jQuery.event.add = (elem, types, handler, data, selector) ->
   if /\:/.test(types)
     Grim?.deprecate """
-      Are you trying to listen for an Atom command with `jQuery::on`?
+      Are you trying to listen for the '#{types}' Atom command with `jQuery::on`?
       `jQuery::trigger` can no longer be used to listen for Atom commands. Please
       use `atom.commands.add` instead. See the docs at
       https://atom.io/docs/api/latest/CommandRegistry#instance-add for details.
@@ -483,9 +483,9 @@

 JQueryTrigger = $.fn.trigger
 $.fn.trigger = (eventName, data) ->
-  if typeof eventName is 'string' and /\:/.test(eventName)
+  if typeof eventName is 'string' and /\:/.test(eventName) and eventName not in ['cursor:moved', 'selection:changed', 'editor:display-updated']
     Grim?.deprecate """
-      Are you trying to dispatch an Atom command with `jQuery::trigger`?
+      Are you trying to dispatch the '#{eventName}' Atom command with `jQuery::trigger`?
       `jQuery::trigger` can no longer emit Atom commands as it will not correctly route
       the command to its handlers. Please use `atom.commands.dispatch` instead.
       See the docs at https://atom.io/docs/api/latest/CommandRegistry#instance-dispatch


dumb mistake

dumb mistake

--- src/utils.coffee
+++ src/utils.coffee
@@ -97,8 +97,9 @@
     return true if firstLine.firstChild == firstLine.lastChild and firstLine.firstChild.tagName == 'BR'
     return false

-  isIE: (version = 0) ->
-    return ieVersion? and ieVersion >= version
+  # We'll take a leap of faith that IE11 is good enough...
+  isIE: (version = 10) ->
+    return ieVersion? and version >= ieVersion

   isLineNode: (node) ->
     return node?.parentNode? and ScribeDOM.hasClass(node.parentNode, 'editor') and ScribeUtils.isBlock(node)


Set owner=only on form 4 requests

Set owner=only on form 4 requests

--- src/transactions.coffee
+++ src/transactions.coffee
@@ -6,6 +6,7 @@
   getTransactions: (cik, type, callback) ->
     baseUrl = process.env.SEC_EDGAR_URL ? 'http://www.sec.gov/cgi-bin/browse-edgar'
     url = "#{baseUrl}?action=getcompany&output=atom&start=0&count=1000&CIK=#{cik}&type=#{type}"
+    url = "#{url}&owner=only"if parseInt(type) is 4

     request url, (error, response, body) ->
       if error?


Ping and location of peer start at 0

Ping and location of peer start at 0

--- src/public/models/remote.peer.coffee
+++ src/public/models/remote.peer.coffee
@@ -52,8 +52,8 @@
 			@on('channel.opened', @_onChannelOpened)
 			@on('channel.closed', @_onChannelClosed)

-			@coordinates = new Vector(Math.random(), Math.random(), Math.random())
-			@latency = Math.random() * 5
+			@coordinates = new Vector(0, 0, 0)
+			@latency = 0

 			if instantiate
 				@connect()


Tests: IDE: Layout: Fixed openDrawingBoard test.

--- client/test/lib/ide/layout.coffee
+++ client/test/lib/ide/layout.coffee
@@ -32,7 +32,7 @@

   splitPanesVertically: (browser) ->

-    split(browser, 'li.split-horizontally')
+    split(browser, 'li.split-vertically')

   splitPanesHorizontally: (browser, selector) ->
@@ -72,6 +72,7 @@

     helpers.beginTest(browser)
     helpers.waitForVMRunning(browser)
+
     helpers.splitPanesUndo(browser)

     browser
@@ -87,10 +88,12 @@
     helpers.waitForVMRunning(browser)

     browser
-      .waitForElementVisible   '.application-tab-handle-holder', 20000
-      .click                   '.application-tab-handle-holder .plus'
+      .waitForElementVisible   paneSelector + ' .plus', 20000
+      .pause   2000
+      .click                   paneSelector + ' .plus'
       .waitForElementVisible   '.context-list-wrapper', 20000
-      .click                   '.context-list-wrapper li.new-drawing-board'
+      .pause   2000
+      .click                   '.context-list-wrapper ' + ' .new-drawing-board'
       .pause                   2000
       .waitForElementVisible   '.drawing-pane .drawing-board-toolbar', 20000 # Assertion
       .end()


Fix bug

Fix bug

--- index.cjsx
+++ index.cjsx
@@ -15,7 +15,7 @@
   reactClass: React.createClass
     getInitialState: ->
       fs = require "fs-extra"
-      json = fs.readJsonSync join(__dirname, "assets", "expedition.json"), "utf8"
+      json = fs.readJsonSync join(__dirname, "assets", "expedition.json")
       expeditions = []
       expeditions[expedition.id] = expedition for expedition in json
       {


fix port formation

fix port formation

--- index.cjsx
+++ index.cjsx
@@ -526,6 +526,7 @@
           flag = true
           _deck = window._decks[deckId]
           {_ships} = window
+          enemyFormation = 0
           shipLv[i] = -1 for i in [0..11]
           for shipId, i in _deck.api_ship
             continue if shipId == -1


fix: subtitle data miss when user switch to other language

--- index.cjsx
+++ index.cjsx
@@ -49,20 +49,17 @@
   data = "{}" if not data or data.length is 0
   JSON.parse data

-loadBackupSubtitles = async () ->
-  if i18n["poi-plugin-subtitle"].locale not in langs
-    locale = 'ja-JP'
-  else
-    locale = i18n["poi-plugin-subtitle"].locale
+loadBackupSubtitles = async (locale) ->
   abbr = locale[...2]
   if abbr is 'zh'
     dataBackup = yield fs.readFileAsync subtitlesBackupPath['zh-CN']
     subtitlesBackup = JSON.parse dataBackup
-    if not subtitles['zh-CN']?.version or +subtitles['zh-CN']?.version < +subtitlesBackup?.version
+    if not subtitles['zh-CN']?.version or +subtitles['zh-CN']?.version < +subtitlesBackup?.version or not subtitles['zh-TW']?.version
       data = dataBackup
       subtitles['zh-CN'] = subtitlesBackup
       err = yield fs.writeFileAsync subtitlesI18nPath['zh-CN'], data
       # Convert backup subtitle to zh-TW and save
+      subtitles['zh-TW'] = {}
       for shipIdOrSth, value of subtitlesBackup
         if typeof value isnt 'object'
           subtitles['zh-TW'][shipIdOrSth] = value
@@ -81,11 +78,13 @@
   dataBackup

 getSubtitles = async () ->
+  dataBackup = {}
   # Load I18n Data
   for lang in langs
+    continue if lang is 'zh-TW'
     subtitles[lang] = yield loadSubtitles(subtitlesI18nPath[lang])
-  # Load backup subtitle
-  data = yield loadBackupSubtitles()
+    # Load backup subtitle
+    dataBackup[lang] = yield loadBackupSubtitles(lang)
   # Update subtitle data from remote server
   if i18n["poi-plugin-subtitle"].locale not in langs
     locale = 'ja-JP'
@@ -99,39 +98,39 @@
     throw "获取字幕数据失败" unless repData
     rep = JSON.parse repData
     throw "字幕数据异常：#{rep.reason}" if rep.result is 'error'
-    return unless rep.version
-    for shipIdOrSth,value of rep
-      if abbr is 'zh'
-        if typeof value isnt "object"
-          subtitles[lang][shipIdOrSth] = value for lang in ['zh-CN', 'zh-TW']
+    if rep.version
+      for shipIdOrSth,value of rep
+        if abbr is 'zh'
+          if typeof value isnt "object"
+            subtitles[lang][shipIdOrSth] = value for lang in ['zh-CN', 'zh-TW']
+          else
+            for lang in ['zh-CN', 'zh-TW']
+              subtitles[lang][shipIdOrSth] = {} unless subtitles[lang][shipIdOrSth]
+            for voiceId, text of value
+              subtitles['zh-CN'][shipIdOrSth][voiceId] = text
+              subtitles['zh-TW'][shipIdOrSth][voiceId] = Traditionalized(text)
         else
-          for lang in ['zh-CN', 'zh-TW']
-            subtitles[lang][shipIdOrSth] = {} unless subtitles[lang][shipIdOrSth]
-          for voiceId, text of value
-            subtitles['zh-CN'][shipIdOrSth][voiceId] = text
-            subtitles['zh-TW'][shipIdOrSth][voiceId] = Traditionalized(text)
-      else
-        if typeof value isnt "object"
-          subtitles[locale][shipIdOrSth] = value
-        else
-          subtitles[locale][shipIdOrSth] = {} unless subtitles[locale][shipIdOrSth]
-          for voiceId, text of value
-            subtitles[locale][shipIdOrSth][voiceId] = text
+          if typeof value isnt "object"
+            subtitles[locale][shipIdOrSth] = value
+          else
+            subtitles[locale][shipIdOrSth] = {} unless subtitles[locale][shipIdOrSth]
+            for voiceId, text of value
+              subtitles[locale][shipIdOrSth][voiceId] = text
+      version = if abbr is 'zh' then subtitles['zh-CN'].version else subtitles[locale].version
+      window.success "#{__('Update Success')}(#{version})",
+        stickyFor: 3000
     for lang in langs
       err = yield fs.writeFileAsync subtitlesI18nPath[lang], JSON.stringify(subtitles[lang], null, '\t')
       throw err if err
-    version = if abbr is 'zh' then subtitles['zh-CN'].version else subtitles[locale].version
-    window.success "#{__('Update Success')}(#{version})",
-      stickyFor: 3000
   catch e
     if e instanceof Error
       console.error "#{e.name}: #{e.message}"
     else
       console.error e
     if locale isnt 'zh-TW'
-      err = yield fs.writeFileAsync subtitlesI18nPath[locale], data
+      err = yield fs.writeFileAsync subtitlesI18nPath[locale], dataBackup[locale]
       console.error err if err
-      subtitles[locale] = JSON.parse data
+      subtitles[locale] = JSON.parse dataBackup[locale]
     # window.warn "语音字幕自动更新失败，请联系有关开发人员，并手动更新插件以更新字幕数据",
     #   stickyFor: 5000
   finally