Fixed the plugin unsupported checks over-riding each other

--- src/lib/plugin-loader.coffee
+++ src/lib/plugin-loader.coffee
@@ -111,45 +111,43 @@
 	unsupported: (next) ->
 		# Prepare
 		docpad = @docpad
-		unsupported = false
-
-		# Check type
+
+		# Extract
+		version = @packageData.version
 		keywords = @packageData.keywords or []
-		unless 'docpad-plugin' in keywords
-			unsupported = 'type'
-
-		# Check version
-		unless semver.satisfies(packageData.version, docpad.pluginVersion)
-			unsupported = 'version-plugin'
-
-		# Check platform
-		if @packageData.platforms
-			platforms = @packageData.platforms or []
-			unless process.platform in platforms
-				unsupported = 'platform'
-
-		# Check engines
-		if @packageData.engines
-			engines = @packageData.engines or {}
-
-			# Node engine
-			if engines.node?
-				unless semver.satisfies(process.version, engines.node)
-					unsupported = 'engine-node'
-
-			# DocPad engine
-			if engines.docpad?
-				unless semver.satisfies(docpad.version, engines.docpad)
-					unsupported = 'version-docpad'
-
-		# Check peerDependencies
-		if @packageData.peerDependencies
-			peerDependencies = @packageData.peerDependencies or {}
-
-			# DocPad engine
-			if peerDependencies.docpad?
-				unless semver.satisfies(docpad.version, peerDependencies.docpad)
-					unsupported = 'version-docpad'
+		platforms = @packageData.platforms or []
+		engines = @packageData.engines or {}
+		peerDependencies = @packageData.peerDependencies or {}
+
+		# Check
+		unsupported =
+			# Check type
+			if 'docpad-plugin' not in keywords
+				'type'
+
+			# Check version
+			else if version and not semver.satisfies(version, docpad.pluginVersion)
+				'version-plugin'
+
+			# Check platform
+			else if platforms.length and process.platform not in platforms
+				'platform'
+
+			# Check node engine
+			else if engines.node? and not semver.satisfies(process.version, engines.node)
+				'engine-node'
+
+			# Check docpad engine
+			else if engines.docpad? and not semver.satisfies(docpad.version, engines.docpad)
+				'version-docpad'
+
+			# Check docpad peerDependencies
+			else if peerDependencies.docpad? and not semver.satisfies(docpad.version, peerDependencies.docpad)
+				'version-docpad'
+
+			# Supported
+			else
+				false

 		# Supported
 		next(null, unsupported)


increase profiling cutoff to 1s

increase profiling cutoff to 1s

--- app/coffee/Profiler.coffee
+++ app/coffee/Profiler.coffee
@@ -5,7 +5,7 @@
 	return Math.floor(((ta[0]-tb[0])*1e9 + (ta[1]-tb[1]))*1e-6)

 module.exports = class Profiler
-	LOG_CUTOFF_TIME: 100
+	LOG_CUTOFF_TIME: 1000

 	constructor: (@name, @args) ->
 		@t0 = @t = process.hrtime()


Added test with namespace prefixes for tagsNotClosedInFragment. Also added test for preexisting behavior with incomplete tags.

--- spec/close-tag-spec.coffee
+++ spec/close-tag-spec.coffee
@@ -26,10 +26,21 @@
       tags = tagFinder.tagsNotClosedInFragment(fragment)
       expect(tags).toEqual(['html', 'body', 'h1'])

+    # Is this desirable? It results in malformed XML for closing tag insertions.
+    it 'detects an incomplete tag', ->
+      fragment = '<html<body

       fragment = '

'
       tags = tagFinder.tagsNotClosedInFragment(fragment)
       expect(tags).toEqual(['html', 'body', 'h1'])
+
+    it 'is not confused by namespace prefixes', ->
+      fragment = '<xhtml:html><xhtml:body><xhtml:h1>'
+      tags = tagFinder.tagsNotClosedInFragment(fragment)
+      expect(tags).toEqual(['xhtml:html', 'xhtml:body', 'xhtml:h1'])

   describe 'TagFinder::tagDoesNotCloseInFragment', ->
     it 'returns true if the given tag is not closed in the given fragment', ->


Adding comments about possible synchronization issues

--- src/server/index.coffee
+++ src/server/index.coffee
@@ -73,6 +73,8 @@
                 if @config.debug
                     console.log "#{k} : #{@config[k]}"

+        # There may be a synchronization issue
+        # The final server may be usable only if all the components have been initialized
         @mongoInterface = new MongoInterface('cloudbrowser')

         @permissionManager = new PermissionManager(@mongoInterface)


Show all tags on preview

Show all tags on preview

--- src/components/exercise.cjsx
+++ src/components/exercise.cjsx
@@ -38,13 +38,17 @@
       <Question id={_.first(questions)?.id} sync={@sync} />
     </BS.TabPane>

-  previewTag: ->
+  previewTag: (tag) ->
     content = _.compact([tag.name, tag.description]).join(' ') or tag.id
     isLO = _.include(['lo', 'aplo'], tag.type)
     {content, isLO}

   onToggleMPQ: (ev) ->
     @setState(isShowingMPQ: ev.target.checked)
+
+  exercisePreviewData: (ex) ->
+    content: ex
+    tags: _.map ex.tags, (tag) -> name: tag

   addQuestion: ->
     ExerciseActions.addQuestionPart(@props.exerciseId)
@@ -54,7 +58,11 @@
     return null unless exercise

-      <ExercisePreview extractTag={@previewTag} exercise={tags: exercise.tags, content: exercise} />
+      <ExercisePreview
+        extractTag={@previewTag}
+        displayAllTags={true}
+        exercise={@exercisePreviewData(exercise)}
+      />

           <form className="navbar-form navbar-right" role="search">


process function needs to pass down to children in question component

--- src/components/question.cjsx
+++ src/components/question.cjsx
@@ -79,6 +79,9 @@
     {disabled, keyControl} = @props
     keysHelper.off(keyControl, 'multiple-choice') if keyControl and not disabled

+  contextTypes:
+    processHtmlAndMath: React.PropTypes.func
+
   render: ->
     {answer, iter, qid, type, correctAnswerId, answered_count, hasCorrectAnswer, chosenAnswer, onChangeAnswer, disabled} = @props
     qid ?= "auto-#{idCounter++}"
@@ -112,7 +115,7 @@
     if @props.show_all_feedback and answer.feedback_html
       feedback = <Feedback key='question-mc-feedback'>{answer.feedback_html}</Feedback>

-    htmlAndMathProps = _.pick(@props, 'processHtmlAndMath')
+    htmlAndMathProps = _.pick(@context, 'processHtmlAndMath')

@@ -138,9 +141,11 @@
     position: React.PropTypes.oneOf(['top', 'bottom', 'left', 'right'])
   getDefaultProps: ->
     position: 'bottom'
+  contextTypes:
+    processHtmlAndMath: React.PropTypes.func
   render: ->
     wrapperClasses = classnames 'question-feedback', @props.position
-    htmlAndMathProps = _.pick(@props, 'processHtmlAndMath')
+    htmlAndMathProps = _.pick(@context, 'processHtmlAndMath')

@@ -174,6 +179,12 @@
     show_all_feedback: false
     keySet: 'multiple-choice'

+  childContextTypes:
+    processHtmlAndMath: React.PropTypes.func
+
+  getChildContext: ->
+    processHtmlAndMath: @props.processHtmlAndMath
+
   onChangeAnswer: (answer, changeEvent) ->
     if @props.onChange?
       @setState({answer_id:answer.id})


Fix image loading spinner for already loaded images

--- app/assets/javascripts/alchemy/alchemy.base.js.coffee
+++ app/assets/javascripts/alchemy/alchemy.base.js.coffee
@@ -46,16 +46,17 @@
   # fades the image after its been loaded
   ImageLoader: (scope = document, options = {color: '#fff'}) ->
     $('img', scope).each ->
-      image = $(this).hide()
-      $parent = image.parent()
-      spinner = Alchemy.Spinner.small options
-      spinner.spin $parent[0]
-      image.on 'load', ->
-        spinner.stop()
-        image.fadeIn 400
-      image.on 'error', ->
-        spinner.stop()
-        $parent.html('')
+      if !this.complete
+        image = $(this).hide()
+        $parent = image.parent()
+        spinner = Alchemy.Spinner.small options
+        spinner.spin $parent[0]
+        image.on 'load', ->
+          spinner.stop()
+          image.fadeIn 400
+        image.on 'error', ->
+          spinner.stop()
+          $parent.html('')

   removePicture: (selector) ->
     $form_field = $(selector)


test inclusive behaviour of %

test inclusive behaviour of %

--- spec/operators-spec.coffee
+++ spec/operators-spec.coffee
@@ -679,6 +679,88 @@
           keydown('G', shift: true)
           keydown('escape')
           expect(editor.getText()).toBe("12345\n\n")
+
+    describe "when followed by a %", ->
+      beforeEach ->
+        editor.setText("12345(67)8\nabc(d)e\nA()BCDE")
+
+      describe "before brackets or on the first one", ->
+        it "replaces inclusively until matching bracket", ->
+          editor.setCursorScreenPosition([0, 1])
+          editor.addCursorAtScreenPosition([1, 1])
+          editor.addCursorAtScreenPosition([2, 1])
+          keydown('c')
+          keydown('%')
+          editor.insertText('x')
+          expect(editor.getText()).toBe("1x8\naxe\nAxBCDE")
+          expect(vimState.mode).toBe "insert"
+
+      describe "inside brackets or on the ending one", ->
+        it "replaces inclusively backwards until matching bracket", ->
+          editor.setCursorScreenPosition([0, 6])
+          editor.addCursorAtScreenPosition([1, 5])
+          editor.addCursorAtScreenPosition([2, 2])
+          keydown('c')
+          keydown('%')
+          editor.insertText('x')
+          expect(editor.getText()).toBe("12345x7)8\nabcxe\nAxBCDE")
+          expect(vimState.mode).toBe "insert"
+
+      describe "after or without brackets", ->
+        it "deletes nothing", ->
+          editor.setText("12345(67)8\nabc(d)e\nABCDE")
+          editor.setCursorScreenPosition([0, 9])
+          editor.addCursorAtScreenPosition([2, 2])
+          keydown('c')
+          keydown('%')
+          expect(editor.getText()).toBe("12345(67)8\nabc(d)e\nABCDE")
+          # differs from VIM, fixed in #774
+          expect(vimState.mode).toBe "insert"
+
+      describe "repeats correctly with .", ->
+        beforeEach ->
+          editor.setCursorScreenPosition([0, 1])
+          keydown('c')
+          keydown('%')
+          editor.insertText('x')
+          keydown('escape')
+
+        it "undoes correctly with u", ->
+          expect(editor.getText()).toBe("1x8\nabc(d)e\nA()BCDE")
+          expect(vimState.mode).toBe "command"
+          keydown 'u'
+          expect(editor.getText()).toBe("12345(67)8\nabc(d)e\nA()BCDE")
+
+        it "repeats correctly before a bracket", ->
+          editor.setCursorScreenPosition([1, 0])
+          keydown('.')
+          expect(editor.getText()).toBe("1x8\nxe\nA()BCDE")
+          expect(vimState.mode).toBe "command"
+
+        it "repeats correctly on the opening bracket", ->
+          editor.setCursorScreenPosition([1, 3])
+          keydown('.')
+          expect(editor.getText()).toBe("1x8\nabcxe\nA()BCDE")
+          expect(vimState.mode).toBe "command"
+
+        it "repeats correctly inside brackets", ->
+          editor.setCursorScreenPosition([1, 4])
+          keydown('.')
+          expect(editor.getText()).toBe("1x8\nabcx)e\nA()BCDE")
+          expect(vimState.mode).toBe "command"
+
+        it "repeats correctly on the closing bracket", ->
+          editor.setCursorScreenPosition([1, 5])
+          keydown('.')
+          expect(editor.getText()).toBe("1x8\nabcxe\nA()BCDE")
+          expect(vimState.mode).toBe "command"
+
+        it "does nothing when repeated after a bracket", ->
+          editor.setCursorScreenPosition([2, 3])
+          keydown('.')
+          # differs from VIM, fixed in #774
+          expect(editor.getText()).toBe("1x8\nabc(d)e\nA()xBCDE")
+          expect(vimState.mode).toBe "command"

     describe "when followed by a goto line G", ->
       beforeEach ->


Fix typos and polish punctuation of spec titles

--- spec/tree-view-spec.coffee
+++ spec/tree-view-spec.coffee
@@ -2820,7 +2820,7 @@
       fileView3 = treeView.find('.file:contains(test-file3.txt)')

     describe 'selecting multiple items', ->
-      it 'switches the contextual menu to muli-select mode', ->
+      it 'switches the contextual menu to multi-select mode', ->
         fileView1.click()
         fileView2.trigger($.Event('mousedown', {shiftKey: true}))
         expect(treeView.find('.tree-view')).toHaveClass('multi-select')
@@ -2828,13 +2828,13 @@
         expect(treeView.find('.tree-view')).toHaveClass('full-menu')

     describe 'selecting multiple items', ->
-      it 'switches the contextual menu to muli-select mode', ->
+      it 'switches the contextual menu to multi-select mode', ->
         fileView1.click()
         fileView2.trigger($.Event('mousedown', {shiftKey: true}))
         expect(treeView.find('.tree-view')).toHaveClass('multi-select')

       describe 'using the shift key', ->
-        it 'selects the items between the already selected item and the shift clicked item', ->
+        it 'selects the items between the already selected item and the shift-clicked item', ->
           fileView1.click()
           fileView3.trigger($.Event('mousedown', {shiftKey: true}))
           expect(fileView1).toHaveClass('selected')
@@ -2842,7 +2842,7 @@
           expect(fileView3).toHaveClass('selected')

       describe 'using the metakey(cmd) key', ->
-        it 'selects the cmd clicked item in addition to the original selected item', ->
+        it 'selects the cmd-clicked item in addition to the original selected item', ->
           fileView1.click()
           fileView3.trigger($.Event('mousedown', {metaKey: true}))
           expect(fileView1).toHaveClass('selected')
@@ -2861,7 +2861,7 @@
           Object.defineProperty(process, "platform", {__proto__: null, value: originalPlatform})

         describe 'using the ctrl key', ->
-          it 'selects the ctrl clicked item in addition to the original selected item', ->
+          it 'selects the ctrl-clicked item in addition to the original selected item', ->
             fileView1.click()
             fileView3.trigger($.Event('mousedown', {ctrlKey: true}))
             expect(fileView1).toHaveClass('selected')
@@ -2880,7 +2880,7 @@
           Object.defineProperty(process, "platform", {__proto__: null, value: originalPlatform})

         describe 'using the ctrl key', ->
-          describe "previous item is selected but the ctrl clicked item is not", ->
+          describe "previous item is selected but the ctrl-clicked item is not", ->
             it 'selects the clicked item, but deselects the previous item', ->
               fileView1.click()
               fileView3.trigger($.Event('mousedown', {ctrlKey: true}))
@@ -2894,7 +2894,7 @@
               expect(treeView.list).toHaveClass('full-menu')
               expect(treeView.list).not.toHaveClass('multi-select')

-          describe 'previous item is selected including the ctrl clicked', ->
+          describe 'previous item is selected including the ctrl-clicked', ->
             it 'displays the multi-select menu', ->
               fileView1.click()
               fileView3.trigger($.Event('mousedown', {metaKey: true}))
@@ -2917,7 +2917,7 @@
               expect(treeView.list).not.toHaveClass('multi-select')

           describe 'when no item is selected', ->
-            it 'selects the ctrl clicked item', ->
+            it 'selects the ctrl-clicked item', ->
               fileView3.trigger($.Event('mousedown', {ctrlKey: true}))
               expect(fileView3).toHaveClass('selected')

@@ -2944,18 +2944,18 @@
               expect(treeView.list).toHaveClass('full-menu')
               expect(treeView.list).not.toHaveClass('multi-select')

-            it 'selects right clicked item', ->
+            it 'selects right-clicked item', ->
               fileView1.click()
               fileView3.trigger($.Event('mousedown', {button: 2}))
               expect(fileView3).toHaveClass('selected')

-            it 'de-selects the previously selected item', ->
+            it 'deselects the previously selected item', ->
               fileView1.click()
               fileView3.trigger($.Event('mousedown', {button: 2}))
               expect(fileView1).not.toHaveClass('selected')

           describe 'when no item is selected', ->
-            it 'selects the right clicked item', ->
+            it 'selects the right-clicked item', ->
               fileView3.trigger($.Event('mousedown', {button: 2}))
               expect(fileView3).toHaveClass('selected')


always show author face

always show author face

Former-commit-id: 1b8336bc754650147d5a20a791da2fb711e12047 [formerly 1f91dfdefa67542b67f2729fe25a931b32ac42a1] [formerly 59e9357958c648c7111182ce530f8e4c440438bc [formerly c763ebb336a290356499a07b2f3385c5b34a1900]] [formerly f478f23142a39cc7f4550baddf4bad14859dc40f [formerly aa9449c7dca51a41d8df20e47d07db1b64ddcf11] [formerly 518fcc745534d40bea73020790648e3cdbbf48af [formerly 518fcc745534d40bea73020790648e3cdbbf48af [formerly 403c2f0ef2e13ebcbbb094dc373c6471bf02a9b7]]]]
Former-commit-id: 958535001a2af362fb06526e1815dd872b45e103 [formerly 2a9aa373ca6ddb7161b9cde3d4865fee5a4113d7] [formerly 0bd1359c3fa29add766768c9aa248b96b73cf150 [formerly c44b63ee2eba3154a6e6fa8a9435650fa7bb7c02]]
Former-commit-id: 59a2d09cf562fd47f72c6d6b5726c997109b154a [formerly 3bf7c249b3e6a343818afda9b30d258432835ebd]
Former-commit-id: e1e4a976cf941a8061b92a0c2bb628c00a804ec6
Former-commit-id: b6f09f373884007cbf2749c49bfeba7a4761b0ad

--- @client/point.coffee
+++ @client/point.coffee
@@ -17,10 +17,10 @@

       if @data().includers

-        if !draw_all_includers
+        if draw_all_includers
+          includers = @buildIncluders()
+        else
           includers = [point.user]
-        else
-          includers = @buildIncluders()

         s = #includers_style
           rows: 8
@@ -34,25 +34,28 @@
         # Now we'll go through the list from back to front
         i = includers.length

-        for includer in includers
-          i -= 1
-          curr_column = Math.floor(i / s.rows)
-          side_offset = curr_column * s.col_gap + i * s.dx
-          top_offset = (i % s.rows) * s.dy
-          left_right = if @data().is_pro && @props.rendered_as != 'under_review' then 'left' else 'right'
-          style =
-            top: top_offset
-            position: 'absolute'
-
-          style[left_right] = side_offset
-
-          # Finally draw the guys
-          Avatar
-            key: includer
-            className: "point_includer_avatar"
-            style: style
-            hide_tooltip: @props.rendered_as == 'under_review'
-            anonymous: point.user == includer && point.hide_name
+        DIV null,
+
+          for includer in includers
+            i -= 1
+            curr_column = Math.floor(i / s.rows)
+            side_offset = curr_column * s.col_gap + i * s.dx
+            top_offset = (i % s.rows) * s.dy
+            left_right = if @data().is_pro && @props.rendered_as != 'under_review' then 'left' else 'right'
+            style =
+              top: top_offset
+              position: 'absolute'
+
+            style[left_right] = side_offset
+
+            # Finally draw the guys
+            Avatar
+              key: includer
+              className: "point_includer_avatar"
+              style: style
+              hide_tooltip: @props.rendered_as == 'under_review'
+              anonymous: point.user == includer && point.hide_name
+

     renderNewIndicator = =>
       if @data().includers
@@ -490,7 +493,7 @@

   buildIncluders : ->
     point = @data()
-    author_has_included = _.contains point.includers, point.user
+    #author_has_included = _.contains point.includers, point.user

     includers = point.includers

@@ -504,9 +507,9 @@
       # only show includers from the current opinion selection
       selected_users = (fetch(o).user for o in selected_opinions)
       includers = _.intersection includers, selected_users
-      author_has_included = _.contains selected_users, point.user
-
-    if author_has_included
+      #author_has_included = _.contains selected_users, point.user
+
+    if true #author_has_included
       includers = _.without includers, point.user
       includers.push point.user


upload photo from filesystem to vk server: log read_stream

--- src/controllers/index.coffee
+++ src/controllers/index.coffee
@@ -44,7 +44,9 @@

     form_data = new FormData()
     #form_data.append 'photo', body
-    form_data.append 'photo', fs.createReadStream('/img/life_is_random.jpg')
+    read_stream = fs.createReadStream('/img/life_is_random.jpg')
+    console.log read_stream
+    form_data.append 'photo', read_stream
     console.log form_data.getHeaders()
     console.log form_data
     form_data.submit server_url, (err, resp)->


Fix #126 by escaping serial strings

Fix #126 by escaping serial strings

--- lib/views/serial-monitor-view.coffee
+++ lib/views/serial-monitor-view.coffee
@@ -71,9 +71,18 @@
     pane = atom.workspace.paneForUri @getUri()
     pane?.destroy()

+  escapeHtml: (unsafe) ->
+    unsafe
+      .replace(/&/g, "&")
+      .replace(/</g, "<")
+      .replace(/>/g, ">")
+      .replace(/"/g, """)
+      .replace(/'/g, "'")
+
   appendText: (text, appendNewline=true) ->
     at_bottom = (@output.scrollTop() + @output.innerHeight() + 10 > @output[0].scrollHeight)

+    text = @escapeHtml text
     text = text.replace "\r", ''
     if appendNewline
       text += "\n"


Client:Profile: Change resetToken to recoverPassword

--- client/Account/views/username.coffee
+++ client/Account/views/username.coffee
@@ -152,8 +152,7 @@
         cssClass     : "action-link verify-email"
         testPath     : "account-email-edit"
         click        : =>
-          KD.remote.api.JEmailConfirmation.resetToken @account.profile.nickname, (err)=>
-
+          KD.remote.api.JPasswordRecovery.recoverPassword @account.profile.nickname, (err)=>
             message = "Email confirmation mail is sent"
             if err then message = err.message else @verifyEmail.hide()


Activity: public listitem selection regression fix

--- client/Social/Activity/routes.coffee
+++ client/Social/Activity/routes.coffee
@@ -24,7 +24,7 @@

   KD.registerRoutes 'Activity',

-    '/:name?/Activity/Public' : ({params: {name}}) -> handleChannel 'group', name or 'koding'
+    '/:name?/Activity/Public' : ({params: {name}}) -> handleChannel 'public', name or 'koding'

     '/:name?/Activity/Topic/:slug?' : ({params:{name, slug}, query}) ->
       handleChannel 'topic', slug


if redis fails once mark it to always fail

--- libraries/redis-wrapper/index.coffee
+++ libraries/redis-wrapper/index.coffee
@@ -25,6 +25,7 @@
 		sub = RedisSharelatex.createClient(connectionInfo)
 		pub = RedisSharelatex.createClient(connectionInfo)

+		redisIsOk = true
 		heartbeatInterval = 2000 #ms
 		isAliveTimeout = 10000 #ms

@@ -45,8 +46,11 @@

 		isAlive = ->
 			timeSinceLastHeartbeat = Date.now() - lastHeartbeat
-			if timeSinceLastHeartbeat > isAliveTimeout
+			if !redisIsOk
+				return false
+			else if timeSinceLastHeartbeat > isAliveTimeout
 				console.error "heartbeat from redis timed out"
+				redisIsOk = false
 				return false
 			else
 				return true


add createMonitoredSubscriptionClient method

--- libraries/redis-wrapper/index.coffee
+++ libraries/redis-wrapper/index.coffee
@@ -20,6 +20,7 @@
 			client = require("redis").createClient opts.port, opts.host, standardOpts
 		return client

+	# DO NOT USE. Not robust at all (ends up getting subscribed twice, etc)!
 	createRobustSubscriptionClient: (opts, heartbeatOpts = {}) ->
 		sub = RedisSharelatex.createClient(opts)
 		pub = RedisSharelatex.createClient(opts)
@@ -64,6 +65,36 @@
 		, heartbeatInterval

 		return sub
+
+	# Attaches an isAlive() method to the subsciption client which returns true
+	# so long as we have had a heartbeat recently.
+	createMonitoredSubscriptionClient: (opts, heartbeatOpts = {}) ->
+		sub = RedisSharelatex.createClient(opts)
+		pub = RedisSharelatex.createClient(opts)
+
+		heartbeatInterval = heartbeatOpts.heartbeat_interval or 2000 #ms
+		isAliveTimeout = heartbeatOpts.is_alive_timeout or 10000 #ms
+
+		id = require("crypto").pseudoRandomBytes(16).toString("hex")
+		heartbeatChannel = "heartbeat-#{id}"
+		lastHeartbeat = Date.now()
+
+		sub.subscribe heartbeatChannel, (error) ->
+			if error?
+				console.error "ERROR: failed to subscribe to #{heartbeatChannel} channel", error
+		sub.on "message", (channel, message) ->
+			if channel == heartbeatChannel
+				lastHeartbeat = Date.now()
+
+		sub.isAlive = () ->
+			timeSinceLastHeartbeat = Date.now() - lastHeartbeat
+			return (timeSinceLastHeartbeat < isAliveTimeout)
+
+		setInterval () ->
+			pub.publish heartbeatChannel, "PING"
+		, heartbeatInterval
+
+		return sub


I suck...

I suck...

Signed-off-by: Philip Vieira <3ba3974e96044dcde818e6310ed799abe10671d4@vallin.se>

--- app/assets/javascripts/modules/cloud/extras.js.coffee
+++ app/assets/javascripts/modules/cloud/extras.js.coffee
@@ -21,18 +21,20 @@
         @userPurge(data) if data.status == 'leave'

     userRefresh: (users) =>
-      $.each users, (index,d) =>
-        userInList = @usersList.find("li.#{d.user_id}")
-        if userInList[0] == undefined
-          image_src = d.user_avatar
-          newItem = @usersList.append("

-
-
-
-
").find("li.#{d.user_id}")
-          newItem.find('a').twipsy()
-        else
-          userInList.removeClass('dropped')
+      $.each(users, (index,d) =>
+        console.log d
+        # userInList = @usersList.find("li.#{d.user_id}")
+        # if userInList[0] == undefined
+        #   image_src = d.user_avatar
+        #   newItem = @usersList.append("

+        #
+        #
+        #
+        #
").find("li.#{d.user_id}")
+        #   newItem.find('a').twipsy()
+        # else
+        #   userInList.removeClass('dropped')
+      )

     userPurge: (data) =>
       userInList = @usersList.find("li.#{data.user_id}")


Show Cc field if needed when replying a message, refs #303

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -300,8 +300,8 @@

         state.sending  = false
         state.saving   = false
-        state.ccShown  = false
-        state.bccShown = false
+        state.ccShown  = Array.isArray(state.cc) and state.cc.length > 0
+        state.bccShown = Array.isArray(state.bcc) and state.bcc.length > 0
         return state

     componentWillReceiveProps: (nextProps) ->


Should prevent flashing when message is displayed

--- client/app/components/message.coffee
+++ client/app/components/message.coffee
@@ -421,39 +421,41 @@
             loadContent = =>
                 doc = frame.contentDocument or frame.contentWindow.document
                 if doc?
-                    s = document.createElement 'style'
-                    doc.head.appendChild(s)
-                    font = "./fonts/sourcesanspro/SourceSansPro-Regular"
-                    rules = [
-                        """
-                        @font-face{
-                          font-family: 'Source Sans Pro';
-                          font-weight: 400;
-                          font-style: normal;
-                          font-stretch: normal;
-                          src: url('#{font}.eot') format('embedded-opentype'),
-                               url('#{font}.otf.woff') format('woff'),
-                               url('#{font}.otf') format('opentype'),
-                               url('#{font}.ttf') format('truetype');
-                        }
-                        """,
-                        "body {
-                            font-family: 'Source Sans Pro';
-                        }",
-                        "blockquote {
-                            margin-left: .5em;
-                            padding-left: .5em;
-                            border-left: 2px solid blue;
-                            color: blue;
-                        }",
-                        "blockquote blockquote { border-color: red !important; color: red; }",
-                        "blockquote blockquote blockquote { border-color: green !important; color: green; }",
-                        "blockquote blockquote blockquote blockquote { border-color: magenta !important; color: magenta; }",
-                        "blockquote blockquote blockquote blockquote blockquote { border-color: blue !important; color: blue; }",
-                    ]
-                    rules.forEach (rule, idx) ->
-                        s.sheet.insertRule rule, idx
-                    doc.body.innerHTML = @_htmlContent
+                    if not doc.getElementById 'cozystyle'
+                        s = document.createElement 'style'
+                        s.id = "cozystyle"
+                        doc.head.appendChild(s)
+                        font = "./fonts/sourcesanspro/SourceSansPro-Regular"
+                        rules = [
+                            """
+                            @font-face{
+                              font-family: 'Source Sans Pro';
+                              font-weight: 400;
+                              font-style: normal;
+                              font-stretch: normal;
+                              src: url('#{font}.eot') format('embedded-opentype'),
+                                   url('#{font}.otf.woff') format('woff'),
+                                   url('#{font}.otf') format('opentype'),
+                                   url('#{font}.ttf') format('truetype');
+                            }
+                            """,
+                            "body {
+                                font-family: 'Source Sans Pro';
+                            }",
+                            "blockquote {
+                                margin-left: .5em;
+                                padding-left: .5em;
+                                border-left: 2px solid blue;
+                                color: blue;
+                            }",
+                            "blockquote blockquote { border-color: red !important; color: red; }",
+                            "blockquote blockquote blockquote { border-color: green !important; color: green; }",
+                            "blockquote blockquote blockquote blockquote { border-color: magenta !important; color: magenta; }",
+                            "blockquote blockquote blockquote blockquote blockquote { border-color: blue !important; color: blue; }",
+                        ]
+                        rules.forEach (rule, idx) ->
+                            s.sheet.insertRule rule, idx
+                        doc.body.innerHTML = @_htmlContent
                     rect = doc.body.getBoundingClientRect()
                     frame.style.height = "#{rect.height + 60}px"
                 else


#162852547 case for when there is no max photos set

--- app/assets/javascripts/hanuman/hanuman-cloudinary-uploads.js.coffee
+++ app/assets/javascripts/hanuman/hanuman-cloudinary-uploads.js.coffee
@@ -230,27 +230,28 @@
     $(self).find('.photo-upload').attr('style','display:block;')

 $ ->
+
   $('.file-upload').each( () ->
     maxPhotos = $(this).find('#max-photos').attr("data-max-photos")
-    addedPhotos = $(this).find('.gallery-item').find("img").length
-    console.log(maxPhotos + ' ' + addedPhotos)
-    checkMaxPhotos(this, maxPhotos, addedPhotos)
+    if maxPhotos
+      addedPhotos = $(this).find('.gallery-item').find("img").length
+      console.log(maxPhotos + ' ' + addedPhotos)
+      checkMaxPhotos(this, maxPhotos, addedPhotos)
   )

   $('.file-upload').on 'click', (e) ->
     maxPhotos = $(this).find('#max-photos').attr("data-max-photos")
-    $(this).bind 'cloudinarydone', (e) ->
-      addedPhotos = $(this).find('.photo-preview').find("img").length
-      checkMaxPhotos(this, maxPhotos, addedPhotos)
+    if maxPhotos
+      $(this).bind 'cloudinarydone', (e) ->
+        addedPhotos = $(this).find('.photo-preview').find("img").length
+        checkMaxPhotos(this, maxPhotos, addedPhotos)

   $('.file-upload').on 'click', '.remove-upload, .delete-saved-file', (e) ->
-    e.preventDefault()
-    self = $(this).parents('.file-upload')
-    $(self).find('.photo-upload').attr('style','display:block;')
-    # checkMaxPhotos(maxPhotos, addedPhotos)
-  # $('.file-upload-input-button').on 'click', () ->
-  #   addedPhotos += 1
-  #   checkMaxPhotos(maxPhotos, addedPhotos)
+    maxPhotos = $(this).parents('.file-upload').find('#max-photos').attr("data-max-photos")
+    if maxPhotos
+      e.preventDefault()
+      self = $(this).parents('.file-upload')
+      $(self).find('.photo-upload').attr('style','display:block;')

   # when a user removes an upload in edit, we are resetting the sortorder


do not show new link on unparented neurons

--- app/assets/javascripts/admin/moi_tree.js.coffee
+++ app/assets/javascripts/admin/moi_tree.js.coffee
@@ -127,8 +127,12 @@
     $popover = $(".popover")
     # format:
     $popover.find(".popover-title").html(node.title)
-    $popover.find(".new-child-link")
-            .attr("href", "/admin/neurons/new?parent_id=#{node.id}")
+    $newChildLink = $popover.find(".new-child-link")
+    if node.parent_id
+      $newChildLink.show()
+                   .attr("href", "/admin/neurons/new?parent_id=#{node.id}")
+    else
+      $newChildLink.hide()
     $popover.find(".edit-link")
             .attr("href", "/admin/neurons/#{node.id}/edit")
     $popover.removeClass("hidden")


updates everything when modifying any field

--- app/assets/javascripts/backend/budget.js.coffee
+++ app/assets/javascripts/backend/budget.js.coffee
@@ -1,22 +1,6 @@
 (($) ->
   'use strict'
-  $(document).ready ->
-    # sorts budgets by direction
-    $("tr[data-budget-direction]").each ->
-      direction = $(this).attr('data-budget-direction')
-      target = $("tr[data-budget-add=#{direction}]")
-      $(this).insertBefore(target)
-    #adds items
-    $("table#budget_visualization").on 'cocoon:after-insert', (event, inserted) ->
-      # adds items to new budget
-      if inserted.hasClass("budget_nested_fields")
-        link_to_add_budget_item = inserted.find("a[data-association='item']")
-        #adds items for supports
-        $("input[id^='production_supports_attributes_'][id$='destroy'][type='hidden']").each ->
-          if $(this).closest('td').is(':visible')
-            link_to_add_budget_item.click()
-            new_item = link_to_add_budget_item.closest("td").prev()
-            new_item.attr('data-support-destroy', $(this).attr('id'))
+  calculate = () ->
     # calculations
     # item calculation
     $("tr.budget_nested_fields").each ->
@@ -42,13 +26,13 @@
       $("[data-budget-global-amount=#{direction}]").each ->
         sum = sum + parseFloat($(this).text())
       $(this).text(sum)
-    # global amount
+    # global balance
     $("[data-balance='global']").each ->
       revenue = parseFloat($("[data-budgets-global-amount='revenue']").text())
       expense = parseFloat($("[data-budgets-global-amount='expense']").text())
       sum = revenue - expense
       $(this).text(sum)
-    #total per support
+    #total revenue/expense per support
     $("[data-budget-add]").each ->
       cells = $(this).find("[data-support-total]")
       cells.each (index) ->
@@ -66,6 +50,28 @@
       revenues = $("[data-support-total='revenue']")[index]
       revenues = parseFloat($(revenues).text())
       $(this).text(revenues - expenses)
+
+  $(document).ready ->
+    # sorts budgets by direction
+    $("tr[data-budget-direction]").each ->
+      direction = $(this).attr('data-budget-direction')
+      target = $("tr[data-budget-add=#{direction}]")
+      $(this).insertBefore(target)
+    #adds items
+    $("table#budget_visualization").on 'cocoon:after-insert', (event, inserted) ->
+      # adds items to new budget
+      if inserted.hasClass("budget_nested_fields")
+        link_to_add_budget_item = inserted.find("a[data-association='item']")
+        #adds items for supports
+        $("input[id^='production_supports_attributes_'][id$='destroy'][type='hidden']").each ->
+          if $(this).closest('td').is(':visible')
+            link_to_add_budget_item.click()
+            new_item = link_to_add_budget_item.closest("td").prev()
+            new_item.attr('data-support-destroy', $(this).attr('id'))
+    # finally calculates everything
+    calculate()
+    return false
+
   # when adding a support
   $(document).on 'click keyup', "a[data-association='support']", ->
     # adds items
@@ -91,5 +97,10 @@
     $("[data-appendable]").each ->
       $(this).find("[data-append-before]").prev().remove()
     return false
+
+  # when updating any field
+  $(document).on 'change', "*", ->
+    calculate()
+    return false
   return
 ) jQuery


Handle dates gracefully

Handle dates gracefully

--- app/assets/javascripts/components/posts.js.cjsx
+++ app/assets/javascripts/components/posts.js.cjsx
@@ -118,6 +118,12 @@
     else
       "default"

+  isoDate: ->
+    try
+      (new Date(@props.post.date.replace(/-/g, "/"))).toISOString()
+    catch e
+      null
+
   handleEdit: ->
     @transitionTo("post_edit", post_id: @props.post.identifier)

@@ -165,7 +171,7 @@

{@props.post.title}

-          {moment((new Date(@props.post.date.replace(/-/g, "/"))).toISOString()).format("LLL")}
+          {if @isoDate() then moment(@isoDate()).format("LLL") else ""}


Correct registration form behavior

Correct registration form behavior

--- app/assets/javascripts/app/registrations/_signup_form.js.coffee
+++ app/assets/javascripts/app/registrations/_signup_form.js.coffee
@@ -17,6 +17,6 @@
   @setup: ->
     $fullname = $("#user__full_name")
     $username = $("#user_username")
-    $username.attr "value", stringToSlug($fullname.attr("value"))
+    $username.attr "value", stringToSlug($fullname.val())
     $fullname.on "input keyup", () ->
-      $username.attr "value", stringToSlug($fullname.attr("value"))
+      $username.attr "value", stringToSlug($fullname.val())


Fixed bug in User mode (via Airbrake)

Fixed bug in User mode (via Airbrake)

--- app/assets/javascripts/oxalis/model/user.coffee
+++ app/assets/javascripts/oxalis/model/user.coffee
@@ -71,12 +71,12 @@

   resetBrightnessContrastColorSettings : (model) ->

-    Request.json("/user/configuration/default").then( (defaultData) =>
+    Request.$(Request.json("/user/configuration/default").then( (defaultData) =>
       @get("brightnessContrastColorSettings")[model.datasetPostfix] =
         defaultData.brightnessContrastColorSettings[model.datasetPostfix]

       @getOrCreateBrightnessContrastColorSettings(model)
-    )
+    ))

   triggerAll : ->


remember global control cookie in a correct path

--- app/assets/javascripts/swat/app/factories/global_control.coffee
+++ app/assets/javascripts/swat/app/factories/global_control.coffee
@@ -1,18 +1,23 @@
 angular.module("SWAT").factory "GlobalControl", ($cookies)->
   class GlobalControl
+
     constructor: ->
+      @COOKIE_KEY = 'MainMenuStatus'
+      @COOKIE_OPTS = { path: '/swat' }
+      @OPENED = 'opened'
+      @CLOSED = 'closed'
       @initStatus()
       @setReloader(->{})

     initStatus: ->
-      status = $cookies.get('MainMenuStatus') || 'closed';
-      @setStatus(status)
+      status = $cookies.get(@COOKIE_KEY, @COOKIE_OPTS);
+      @setStatus(status || @CLOSED)

     toggle: ->
-      @setStatus(if @status == 'closed' then 'opened' else 'closed')
+      @setStatus(if @status == @CLOSED then @OPENED else @CLOSED)

     setStatus: (value)->
-      $cookies.put('MainMenuStatus', value);
+      $cookies.put(@COOKIE_KEY, value, @COOKIE_OPTS);
       @status = value

     setReloader: (func)->


mute video release all streams

mute video
release all streams

--- app/assets/javascripts/uploadcare/widget/tabs/camera-tab.coffee
+++ app/assets/javascripts/uploadcare/widget/tabs/camera-tab.coffee
@@ -67,12 +67,15 @@
           .removeClass('uploadcare-dialog-camera-requested')
           .removeClass('uploadcare-dialog-camera-denied')
           .addClass('uploadcare-dialog-camera-ready')
+
         @__stream = stream
         if @URL
           @video.prop('src', @URL.createObjectURL(stream))
         else
           @video.prop('src', stream)
+        @video[0].volume = 0
         @video[0].play()
+
       , (error) =>
         if error == "NO_DEVICES_FOUND" or error.name == 'DevicesNotFoundError'
           @container.addClass('uploadcare-dialog-camera-not-founded')
@@ -91,8 +94,8 @@
         return
       if @URL
         @URL.revokeObjectURL(@video.prop('src'))
-      if @__stream.getVideoTracks
-        $.each @__stream.getVideoTracks(), ->
+      if @__stream.getTracks
+        $.each @__stream.getTracks(), ->
           @stop?()
       @__stream.stop?()
       @__stream = null


prepopulate name field on confirm account page

--- app/components/confirm-account-page/confirm-account-page.coffee
+++ app/components/confirm-account-page/confirm-account-page.coffee
@@ -12,6 +12,8 @@
     $scope.email = $stateParams.email
     $scope.name = $stateParams.name
     $scope.setupGroup = $stateParams.setup_group
+    $scope.formData = []
+    $scope.formData.name = $stateParams.name

     $scope.confirmAccount = (formData) ->
       LoadBar.start()


No need for double arrow here

No need for double arrow here

--- SingularityUI/app/application.coffee
+++ SingularityUI/app/application.coffee
@@ -15,7 +15,7 @@

 class Application

-    initialize: =>
+    initialize: ->
         app.isMobile = touchDevice = 'ontouchstart' of document.documentElement

         @views = {}
@@ -36,7 +36,7 @@

             Object.freeze? @

-    fetchResources: (success) =>
+    fetchResources: (success) ->
         @resolveCountdown = 0

         @collections.requestsStarred = new RequestsStarred


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -74,7 +74,7 @@
   Math.round(this)

 ###*
-Get a bunch of points equally spaced
+Get a bunch of points equally spaced around the un

 4.circularPoints (p) ->


number_extensions

--- src/number_extensions.coffee
+++ src/number_extensions.coffee
@@ -17,14 +17,14 @@
 Returns the mathematical ceiling of this number.

-   4.9.ceil()
-=> 5
-
-   4.2.ceil()
-=> 5
-
-   (-1.2).ceil()
-=> -1
+4.9.ceil()
+# => 5
+
+4.2.ceil()
+# => 5
+
+(-1.2).ceil()
+# => -1

 @name ceil


Object.isString

Object.isString

--- src/object_extensions.coffee
+++ src/object_extensions.coffee
@@ -14,8 +14,30 @@
 @param {Object} object The object to check for array-ness.
 @returns {Boolean} A boolean expressing whether the object is an instance of Array
 ###
-Object.isArray = (object)->
-  Object::toString.call(object) == '[object Array]'
+Object.isArray = (object) ->
+  Object::toString.call(object) == "[object Array]"
+
+###*
+Checks whether an object is a string.
+
+

+Object.isString("a string")
+# => true
+
+Object.isString([1, 2, 4])
+# => false
+
+Object.isString({key: "value"})
+# => false
+

+
+@name isString
+@methodOf Object
+@param {Object} object The object to check for string-ness.
+@returns {Boolean} A boolean expressing whether the object is an instance of String
+###
+Object.isString = (object) ->
+  Object::toString.call(object) == "[object String]"

 ###*
 Merges properties from objects into target without overiding.


Show a Job's id in the inspect output for it.

--- application/chrome/content/wesabe/download/Job.coffee
+++ application/chrome/content/wesabe/download/Job.coffee
@@ -114,6 +114,6 @@

   contentForInspect: ->
-    {@status, @result, @done, @options, @player, @data}
+    {@id, @status, @result, @done, @options, @player, @data}

 module.exports = Job


change user page to list forums of user

change user page to list forums of user

--- server/src/website/controllers/ui/users.coffee
+++ server/src/website/controllers/ui/users.coffee
@@ -17,18 +17,22 @@
     item: (req, res, next) =>
         @attachUser arguments, =>
             models.Forum.find({ 'createdBy.username': req.params.id, network: req.network.stub }, ((cursor) -> cursor.sort({ lastPost: -1 }).limit 12), {}, db)
-                .then (featured) =>
-                    for forum in featured
-                        forum.summary = forum.getView("card")
-                        forum.summary.view = "standard"
-
-                    res.render req.network.views.forums.index, {
-                        featured,
-                        pageName: 'forums-page',
-                        pageType: 'cover-page',
-                        cover: '/pub/images/cover.jpg'
-                    }
+                .then (forums) =>
+                    for forum in forums
+                        models.Post.find({ 'forum.stub.id': forum.id, 'forum.network': req.network.stub }, ((cursor) -> cursor.sort({ _id: -1 }).limit 12), {}, db)
+                            .then (posts) =>
+                                for post in posts
+                                        post.summary = post.getView("card")
+                                        post.summary.view = "standard"

+                                res.render req.network.views.forums.item, {
+                                    forum,
+                                    posts,
+                                    pageName: 'forum-page',
+                                    pageType: 'cover-page',
+                                    cover: @cover ? '/pub/images/cover.jpg'
+                                    }
+

-
+
 exports.Users = Users


hide fallback 'zotero-null-id' keys

hide fallback 'zotero-null-id' keys

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -132,6 +132,7 @@
           return ''
         else
           key = Zotero.BetterBibTeX.keymanager.get({itemID: item.id, libraryID: item.libraryID}, {metadata: true})
+          return '' if key.citekey.match(/^zotero-null-[0-9]+$/)
           return key.citekey + (if key.citeKeyFormat then ' *' else '')

       return original.apply(this, arguments)


fixes #236

fixes #236

--- chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
+++ chrome/content/zotero-better-bibtex/zotero-better-bibtex.coffee
@@ -445,7 +445,8 @@
   @testing = (@pref.get('tests') != '')

   try
-    BetterBibTeXPatternFormatter::skipWords = @pref.get('cacheFlushInterval').split(',')
+    BetterBibTeXPatternFormatter::skipWords = @pref.get('skipWords').split(',')
+    Zotero.BetterBibTeX.debug('skipwords:', BetterBibTeXPatternFormatter::skipWords)
   catch err
     Zotero.BetterBibTeX.error('could not read skipwords:', err)
     BetterBibTeXPatternFormatter::skipWords = []


try edx openended

try edx openended

--- common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
+++ common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
@@ -242,7 +242,7 @@
           if(options[i].length > 0) {
             var value = options[i].replace(/^\s+|\s+$/g,'');
             var lower_option = value.toLowerCase();
-            type = lower_option.match(/(peer|self|ai)/gmi)
+            type = lower_option.match(/(peer|self|ai|edx)/gmi)
             if(type != null) {
               type = type[0]
               var min_max = value.match(/\{\n?([^\]]*)\}/gmi);
@@ -260,6 +260,9 @@
               } else if (type=="peer") {
                 config = "peer_grading.conf"
                 groupString += template(open_ended_template,{min_max_string: min_max_string, grading_config: config});
+              } else if (type=="edx") {
+                config = "edX"
+                groupString += template(open_ended_template,{min_max_string: min_max_string, grading_config: config});
               } else if (type=="ai") {
                                        config = "ml_grading.conf"
                                        groupString += template(open_ended_template,{min_max_string: min_max_string, grading_config: config});


fixed simple edx openended problem

fixed simple edx openended problem

--- common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
+++ common/lib/xmodule/xmodule/js/src/combinedopenended/edit.coffee
@@ -63,8 +63,6 @@
     else
       @createXMLEditor()

-    @alertTaskRubricModification()
-
   ###
   Creates the XML Editor and sets it as the current editor. If text is passed in,
   it will replace the text present in the HTML template.
@@ -95,8 +93,6 @@
       # Hide markdown-specific toolbar buttons
       $(@element.find('.editor-bar')).hide()

-  alertTaskRubricModification: ->
-    return alert("Before you edit, please note that if you alter the tasks block or the rubric block of this question after students have submitted responses, it may result in their responses and grades being deleted!  Use caution when altering problems that have already been released to students.")
   ###
   Have the user confirm the one-way conversion to XML.
   Returns true if the user clicked OK, else false.
@@ -261,7 +257,7 @@
                 config = "peer_grading.conf"
                 groupString += template(open_ended_template,{min_max_string: min_max_string, grading_config: config});
               } else if (type=="edx") {
-                config = "edX"
+                config = "openended"
                 groupString += template(open_ended_template,{min_max_string: min_max_string, grading_config: config});
               } else if (type=="ai") {
                                        config = "ml_grading.conf"


`.load` is removed since jQuery 3.0

`.load` is removed since jQuery 3.0

Use `.on("load", fn)`.

--- app/assets/javascripts/app.js.coffee
+++ app/assets/javascripts/app.js.coffee
@@ -1,6 +1,6 @@
 $ ->
   # img の height を可変にしたいので
-  $(window).load ->
+  $(window).on "load", ->
     if $(".grid").length > 0
       new Masonry(".grid", {
           itemSelector: ".grid-item",


fixes turbo links issue with nav when loading certain pages

--- app/assets/javascripts/header.coffee
+++ app/assets/javascripts/header.coffee
@@ -58,6 +58,5 @@
       flyingHeader.position()
   catch err

-$(document).ready(load)
-$(document).on('page:load', load)
+$(document).on('page:change', load)


raise timeout setting for video uploads even more

--- app/assets/javascripts/upload.coffee
+++ app/assets/javascripts/upload.coffee
@@ -66,7 +66,7 @@
   uppy.use Uppy.XHRUpload,
     endpoint: '/videos/upload'
     fieldName: 'file'
-    timeout: 600 * 1000
+    timeout: 3600 * 1000

   # give the user feedback after upload has started
   uppy.on 'upload', (data) ->


Update FSPersistorManager.coffee

Update FSPersistorManager.coffee

Do not propagate the error if trying to delete a directory that does not exist.

--- app/coffee/FSPersistorManager.coffee
+++ app/coffee/FSPersistorManager.coffee
@@ -60,7 +60,10 @@
     filteredName = filterName name
     fs.rmdir "#{location}/#{filteredName}", (err) ->
       logger.err err:err, location:location, name:filteredName, "Error on rmdir."
-      callback err
+      if err and err.errno != 34
+        callback err
+      else
+        callback()

   checkIfFileExists:(location, name, callback = (err,exists)->)->
     filteredName = filterName name


Rename request -> s3Request to prevent overwriting main import

--- app/coffee/S3PersistorManager.coffee
+++ app/coffee/S3PersistorManager.coffee
@@ -95,9 +95,9 @@
 		}
 		if opts.start? and opts.end?
 			s3Params['Range'] = "bytes=#{opts.start}-#{opts.end}"
-		request = s3.getObject(s3Params)
-
-		request.on 'httpHeaders', (statusCode, headers, response, statusMessage) =>
+		s3Request = s3.getObject(s3Params)
+
+		s3Request.on 'httpHeaders', (statusCode, headers, response, statusMessage) =>
 			if statusCode in [403, 404]
 				# S3 returns a 403 instead of a 404 when the user doesn't have
 				# permission to list the bucket contents.
@@ -109,11 +109,11 @@
 			stream = response.httpResponse.createUnbufferedStream()
 			callback(null, stream)

-		request.on 'error', (err) =>
+		s3Request.on 'error', (err) =>
 			logger.err({ err: err, bucketName: bucketName, key: key }, "error getting file stream from s3")
 			callback(err)

-		request.send()
+		s3Request.send()

 	getFileSize: (bucketName, key, callback) ->
 		logger.log({ bucketName: bucketName, key: key }, "getting file size from S3")


Fixed bug #135 (translate some text)

Fixed bug #135 (translate some text)

--- app/coffee/controllers/issues.coffee
+++ app/coffee/controllers/issues.coffee
@@ -81,11 +81,10 @@
             if userCounter[0] is null
                 tag = {
                     id: "null",
-                    name: "Unassigned",
+                    name: $i18next.t("common.unassigned"),
                     count: userCounter[1],
                     type: type
                 }
-
             else
                 user = $scope.constants.users[userCounter[0]]
                 tag = {


Fixed spacing on Available From/Until inputs

For New Files, the RestrictedDialogForm has the wrong
indentation/styles for the .control-group for the
Available From & Available Until form date select
inputs.  This has been fixed.

fixes CNVS-17042

Test Plan:
* Enable "Better File Browsing" feature flag
* Go to any course's Files page (as a teacher or admin)
* Select a File or Folder, then click "Restrict Access"
  in the toolbar
* On the modal that appears, select "Restricted Access" ->
  "Schedule student availability"
* There should be proper spacing between the Available
  From and Available Until form fields.

Change-Id: I64cf23bbfa2ffa66fab3ad972112666642955956
Reviewed-on: https://gerrit.instructure.com/44905
Reviewed-by: Ryan Shaw <ea3cd978650417470535f3a4725b6b5042a6ab59@instructure.com>
Tested-by: Jenkins <d95b56ce41a2e1ac4cecdd398defd7414407cc08@instructure.com>
Product-Review: Cosme Salazar <d37f5e06cd46b8fd23ce5553334e09ede5394232@instructure.com>
QA-Review: Caleb Guanzon <54fa1f30d451e4f3c341a8f3c47c9215fd0be9c6@instructure.com>

--- app/coffeescripts/react_files/components/RestrictedDialogForm.coffee
+++ app/coffeescripts/react_files/components/RestrictedDialogForm.coffee
@@ -162,9 +162,7 @@

           div {
             className: 'control-group'
-            style:
-              display: (if @state.selectedOption is 'date_range' then 'block' else 'none')
-            'aria-hidden': (if @state.selectedOption is 'date_range' then 'false' else 'true')
+            hidden: @state.selectedOption isnt 'date_range'
           },
             label className: 'control-label dialog-adapter-form-calendar-label', I18n.t('Available From')
               div className: 'controls dateSelectInputContainer',
@@ -174,15 +172,18 @@
                   className: 'form-control dateSelectInput',
                   type: 'text',
                   'aria-label': I18n.t('Available From')
-            div className: 'control-group',
-              label className: 'control-label dialog-adapter-form-calendar-label', I18n.t('Available Until')
-                div className: 'controls dateSelectInputContainer',
-                input
-                  ref: 'lock_at'
-                  defaultValue: $.datetimeString(@state.lock_at) if @state.lock_at,
-                  className: 'form-control dateSelectInput',
-                  type: 'text'
-                  'aria-label': I18n.t('Available Until')
+          div {
+            className: 'control-group'
+            hidden: @state.selectedOption isnt 'date_range'
+          },
+            label className: 'control-label dialog-adapter-form-calendar-label', I18n.t('Available Until')
+              div className: 'controls dateSelectInputContainer',
+              input
+                ref: 'lock_at'
+                defaultValue: $.datetimeString(@state.lock_at) if @state.lock_at,
+                className: 'form-control dateSelectInput',
+                type: 'text'
+                'aria-label': I18n.t('Available Until')

         div className:"form-controls",
           button {


fix search user details

fix search user details

--- app/controllers/found_friends.coffee
+++ app/controllers/found_friends.coffee
@@ -16,6 +16,11 @@
 class FoundFriendShow extends UsersShow
   @configure FoundFriend
   tab: 'explore'
+
+  change: (params) =>
+    @item_id = params.id if params.id
+    @item = FoundFriend.exists(@item_id) || SearchFriend.exists(@item_id)
+    @render()

   back: ->
     @navigate('/found_friends', trans: 'left')


fix page load always navigating to /user/edit/show

--- app/controllers/found_friends.coffee
+++ app/controllers/found_friends.coffee
@@ -21,7 +21,7 @@

   change: (params) =>
     @item_id = params.id if params.id
-    @navigate('/user/edit/show') if MyUser.first()?.id == @item_id
+    @navigate('/user/edit/show') if @item_id && MyUser.first()?.id == @item_id
     @item = Friend.exists(@item_id) || FoundFriend.exists(@item_id) || SearchFriend.exists(@item_id)
     @render()


Fix slice for numbers

Fix slice for numbers

--- admin/static/coffee/util.coffee
+++ admin/static/coffee/util.coffee
@@ -362,8 +362,8 @@
 human_readable_shard_obj = (shard) ->
     name_len = 8
     json_shard = $.parseJSON(shard)
-    from = pretty_key(json_shard[0])
-    to = pretty_key(json_shard[1])
+    from = pretty_key(json_shard[0]).toString()
+    to = pretty_key(json_shard[1]).toString()
     # Returns an object with shard names truncated to name_len characters, and
     # appends ellipses if the shard name was longer
     return {


Track when COPPA dialog is shown.

Track when COPPA dialog is shown.

--- app/views/core/COPPADenyModal.coffee
+++ app/views/core/COPPADenyModal.coffee
@@ -6,3 +6,7 @@
   id: 'coppa-deny-modal'
   template: template
   closeButton: true
+
+  constructor: ->
+    super()
+    window.tracker?.trackEvent 'COPPA Message Shown', category: 'Homepage'


teams: activity: threadsidebar is now shared between both public and private panes

--- client/activity/lib/components/privatemessagethreadpane/index.coffee
+++ client/activity/lib/components/privatemessagethreadpane/index.coffee
@@ -7,8 +7,9 @@
 PrivateChatPane           = require 'activity/components/privatechatpane'
 ThreadSidebarContentBox   = require 'activity/components/threadsidebarcontentbox'
 ChannelParticipantAvatars = require 'activity/components/channelparticipantavatars'
+ThreadSidebar             = require 'activity/components/threadsidebar'
 prepareThreadTitle        = require 'activity/util/prepareThreadTitle'
-ImmutableRenderMixin = require 'react-immutable-render-mixin'
+ImmutableRenderMixin      = require 'react-immutable-render-mixin'

 module.exports = class PrivateMessageThreadPane extends React.Component
@@ -58,18 +59,6 @@
     />

-  renderSidebar: ->
-
-

-      <ThreadSidebarContentBox title="Participants">
-        <ChannelParticipantAvatars
-          channelThread = { @state.channelThread }
-          participants  = { @state.channelParticipants }
-        />
-      </ThreadSidebarContentBox>
-

-
-
   render: ->

@@ -83,7 +72,9 @@

-        {@renderSidebar()}
+        <ThreadSidebar
+          channelThread={@state.channelThread}
+          channelParticipants={@state.channelParticipants}/>


client: stacks: no stacks sidebar warning opens stack modal

--- client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
+++ client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
@@ -8,7 +8,7 @@
   render: ->

-      <Link className='SidebarSection-headerTitle' href='/Welcome'>
+      <Link className='SidebarSection-headerTitle' href='/Stacks/Welcome'>
         STACKS
       </Link>

@@ -18,7 +18,7 @@
             fully configured yet, please
             finalize onboarding steps.

-          <Link href='/Welcome'>Finalize steps</Link>
+          <Link href='/Stacks/Welcome'>Finalize steps</Link>


sidebar: change props of no stacks widget

Signed-off-by: Umut Sirin <4055abbc0e40a1254e715b952ccd414607e0b365@gmail.com>

--- client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
+++ client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
@@ -1,33 +1,27 @@
 React = require 'app/react'
 Link  = require 'app/components/common/link'
-canCreateStacks = require 'app/util/canCreateStacks'
-isAdmin = require 'app/util/isAdmin'

 module.exports = class SidebarNoStacks extends React.Component

   renderMessage: ->

-    { teamStacks, privateStacks } = @props
+    { hasTemplate, hasPermission } = @props

-    if teamStacks or privateStacks
+    if hasTemplate

-        Open your dashboard and add your stacks to sidebar to continue
+        Open your dashboard and add your stacks to sidebar to continue.

-    else if canCreateStacks()
+    else if hasPermission

-        Your stacks has not been
-        fully configured yet, please
-        finalize onboarding steps.
+        Your stacks has not been fully configured yet, please finalize
+        onboarding steps.

     else

-        Your stacks has not been
-        fully configured yet, please
-        wait until your admin sets
-        them up for you.
+        Your stacks has not been fully configured yet, please wait until your
+        admin sets them up for you.

-

   render: ->


sidebar: welcome link copy changed

sidebar: welcome link copy changed

--- client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
+++ client/app/lib/components/sidebarstacksection/sidebarnostacks.coffee
@@ -24,7 +24,7 @@
             fully configured yet, please
             finalize onboarding steps.
           </p>
-          <Link href='/Welcome'>Go to welcome page</Link>
+          <Link href='/Welcome'>Finalize steps</Link>
         </div>
       </section>
     </div>


prepare-and-check-layouts: Fix typo

prepare-and-check-layouts: Fix typo

--- src/scripts/prepare-and-check-layouts.coffee
+++ src/scripts/prepare-and-check-layouts.coffee
@@ -14,7 +14,7 @@
     ["More than one pacman", (layout) -> countChar(layout, 'P') > 1]
     ["No pacman", (layout) -> countChar(layout, 'P') is 0]
     ["Invalid character", (layout) -> (c for c in layout when c isnt '\n').some((c) -> c not in VALID_CHARACTERS)]
-    ["More than 4 ghosts", (lauout) -> countChar(layout, 'G') > 4]
+    ["More than 4 ghosts", (layout) -> countChar(layout, 'G') > 4]
 ]

 error = (layoutPath, text) ->


refactor unnecessary variables

refactor unnecessary variables

--- client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
+++ client/landing/app/Applications/Environments.kdapplication/views/domains/domaincreationform.coffee
@@ -206,12 +206,11 @@

     else # create a subdomain
       # replace the dots at the end if there are any
-      subDomain = domainName.replace /\.{1,}(?=[^.]*$)/, ''
       subDomainPattern = /^[a-zA-Z0-9][a-zA-Z0-9.-]+[a-zA-Z0-9]$/
-      unless subDomainPattern.test subDomain
+      unless subDomainPattern.test domainName
         createButton.hideLoader()
-        return notifyUser "#{subDomain} is an invalid subdomain."
-      domainName = "#{subDomain}.#{domains.getValue()}"
+        return notifyUser "#{domainName} is an invalid subdomain."
+      domainName = "#{domainName}.#{domains.getValue()}"

       @createDomain {domainName, regYears:0}, (err, domain)=>
         createButton.hideLoader()


sync ui with tool state

sync ui with tool state

--- src/ubret/spacewarp_viewer/initialize.coffee
+++ src/ubret/spacewarp_viewer/initialize.coffee
@@ -53,12 +53,11 @@

   constructor: (selector) ->
     super selector
-
+    # Default stretch
     @stretch = 'linear'

     # Run when the WebFITS API is received
     @on 'swviewer:ready', =>
-      # NOTE: Dimensions and global extent are hard coded
       el = document.querySelector(@selector)
       @wfits = new astro.WebFITS(el, @dimension)
       unless @wfits.ctx?
@@ -83,7 +82,6 @@
     )

   start: =>
-
     # Request the appropriate WebFITS API (canvas or webgl)
     @getApi()

@@ -99,7 +97,7 @@
       dfs.push new $.Deferred()

     # Setup callback for when all requests are received
-    $.when.apply(this, dfs)
+    $.when.apply(@, dfs)
       .done( (e) =>
         @computeNormalizedScales()
         scales = @collection.getColorScales()
@@ -110,7 +108,7 @@
         @wfits.setupControls()

         # Set color composite as default
-        @setBand('gri')
+        @trigger 'swviewer:loaded'
       )

     # Request FITS images
@@ -124,7 +122,7 @@
           dataunit = hdu.data

           # Get image data
-          arr = dataunit.getFrameAsync( ->
+          dataunit.getFrameAsync(0, (arr) =>
             # Compute extent
             [min, max] = dataunit.getExtent(arr)

@@ -137,7 +135,6 @@

             # Load texture
             @wfits.loadImage(band, arr, dataunit.width, dataunit.height)
-
             dfs[index].resolve()
           )
         )


Pimp shooting

Pimp shooting

--- src/components/weapons/RapidWeaponLaser.coffee
+++ src/components/weapons/RapidWeaponLaser.coffee
@@ -2,8 +2,8 @@
   init: ->
     @requires '2D,WebGL,muzzleFlash,ColorEffects'
     @attr
-      w: 30
-      h: 16
+      w: 40
+      h: 24

     @stats =
       rapid: 0
@@ -65,14 +65,14 @@
     @trigger('boost', aspect: aspect)

   _determineWeaponSettings: ->
-    @cooldown = 175 - ((@boosts.rapidb || @stats.rapid) * 10)
+    @cooldown = 135 - ((@boosts.rapidb || @stats.rapid) * 7)

     @damage = 100 + ((@boosts.damageb || @stats.damage) * 50)

     @aimAngle = 0 + ((@boosts.aimb || @stats.aim) * 6)
     @aimDistance = Math.min(40 + ((@boosts.aimb || @stats.aim) * 50), 500)

-    @speed = 650 + ((@boosts.speedb || @stats.speed) * 70)
+    @speed = 750 + ((@boosts.speedb || @stats.speed) * 70)

     levels = (value for k, value of @stats when k isnt 'damage')
     @overallLevel = Math.min(levels...)
@@ -97,7 +97,7 @@
         @_determineWeaponSettings()
         @trigger('boostExpired', aspect: k)

-    @attr alpha: 0 if @lastShot >= 60
+    @attr alpha: 0 if @lastShot >= 30
     return unless @shooting
     allowBullet = (@shotsFired < @burstCount)
     return unless @ship.weaponsEnabled
@@ -121,7 +121,7 @@

     start =
       x: @x + @w
-      y: @y + (@h / 2) - (settings.h / 2) + 1 + settings.o
+      y: @y + (@h / 2) - (settings.h / 2) + 10 + settings.o
     Crafty.e('Bullet')
       .attr
         w: settings.w
@@ -142,7 +142,7 @@

     start =
       x: @x + @w
-      y: @y + (@h / 2) - (settings.h / 2) - 2 - settings.o
+      y: @y + (@h / 2) - (settings.h / 2) - 10 - settings.o
     Crafty.e('Bullet')
       .attr
         w: settings.w


#310 Fix error in RGB shader

#310 Fix error in RGB shader

--- app/assets/javascripts/oxalis/geometries/materials/plane_material_factory.coffee
+++ app/assets/javascripts/oxalis/geometries/materials/plane_material_factory.coffee
@@ -163,7 +163,7 @@

         /* Get Color Value(s) */
         <% if (isRgb) { %>
-          vec4 data_color = texture2D( color_texture, vUv * repeat + offset);
+          vec3 data_color = texture2D( color_texture, vUv * repeat + offset).xyz;
         <% } else { %>
           vec3 data_color = vec3(0.0, 0.0, 0.0)
           <% _.each(layers, function(name){ %>


don't assume DirectoryViews have entries when serializing

--- src/extensions/tree-view/directory-view.coffee
+++ src/extensions/tree-view/directory-view.coffee
@@ -70,7 +70,7 @@

   serializeEntryExpansionStates: ->
     entryStates = {}
-    @entries.find('> .directory.expanded').each ->
+    @entries?.find('> .directory.expanded').each ->
       view = $(this).view()
       entryStates[view.directory.getName()] = view.serializeEntryExpansionStates()
     entryStates


Also add the final email to the array of emails on the account

--- services/web/app/coffee/Features/AccountMerge/AccountMergeEmailController.coffee
+++ services/web/app/coffee/Features/AccountMerge/AccountMergeEmailController.coffee
@@ -35,6 +35,11 @@
 					'$set': {
 						'overleaf.id': v1_id,
 						email: final_email
+					},
+					'$push': {
+						emails: {
+							email: final_email, createdAt: new Date()
+						}
 					}
 				}, (err) ->
 					return next(err) if err?


xhr reset log message

xhr reset log message

--- Framework/Network.coffee
+++ Framework/Network.coffee
@@ -41,7 +41,9 @@
     else
       @xhr.send(options.params)

-  reset: -> @xhr.abort()
+  reset: ->
+    Ti.API.info("xhr reset")
+    @xhr.abort()

   onError: (options) =>
     options.try++


Change instruction text and markup for Verify tool


Client: Activity: Added onChange method to set ChannelParticipantsInputQuery query and ChannelParticipantsDropdownVisibility

--- client/activity/lib/components/channelparticipantavatars/index.coffee
+++ client/activity/lib/components/channelparticipantavatars/index.coffee
@@ -169,6 +169,21 @@
     'slide-down': @state.addNewParticipantMode

+  onChange: (event) ->
+
+    { value } = event.target
+    @setState { value }
+
+    matchResult = value.match /^@(.*)/
+
+    return no  unless matchResult
+
+    query = matchResult[1]
+
+    { channel, user  } = ActivityFlux.actions
+
+    user.setChannelParticipantsInputQuery query
+    channel.setChannelParticipantsDropdownVisibility yes
   renderAddNewParticipantInput: ->

     <div className={@getNewParticipantInputClassNames()}>


home: if logged in homepage scrolls to activity

--- client/landing/app/Applications/Activity.kdapplication/AppView.coffee
+++ client/landing/app/Applications/Activity.kdapplication/AppView.coffee
@@ -66,6 +66,10 @@
     @addSubView @innerNav
     @addSubView @feedWrapper

+    if KD.isLoggedIn()
+      @utils.wait 1500, =>
+        @navigateHome pageName :"Activity"
+
   # pistachio:->
   #   """
   #     {{> @header}}


os kite: physicalMemoryLimit vs. totalMemoryLimit

--- client/landing/app/MainApp/filetree/itemsubviews/vmdetailsview.coffee
+++ client/landing/app/MainApp/filetree/itemsubviews/vmdetailsview.coffee
@@ -44,7 +44,7 @@

     if info.state is "RUNNING"
       # Memory
-      mem = ((info.memoryUsage * 100) / info.memoryLimit).toFixed(2)
+      mem = ((info.memoryUsage * 100) / info.totalMemoryLimit).toFixed(2)
       @labelRAM.updateTitle "#{mem}%"
       # RX
       @kcRun cmdRX, (err, out)=>


handle properly the exit of compose after sending

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -172,35 +172,32 @@
                 div className: 'clearfix', null

-    # Cancel brings back to default view. If it's while replying to a message,
-    # it brings back to this message.
-    onCancel: (event) ->
-        event.preventDefault()
-
-        # Action after cancelation: call @props.onCancel
-        # or navigate to message list
-        if @props.onCancel?
-            @props.onCancel()
-
-        # If we are answering to a message, canceling should bring back to
-        # this message.
-        # The message URL requires many information: account ID, mailbox ID,
-        # conversation ID and message ID.
-        else if @props.inReplyTo?
+    # Build message hash with the many ids required: account ID, mailbox ID,
+    # conversation ID and message ID.
+    buildMessageHash: (options) ->
+        {accountID, mailboxID, conversationID, messageID} = options
+        hash = "#account/#{accountID}/"
+        hash += "mailbox/#{mailboxID}/"
+        hash += "conversation/#{conversationID}/#{messageID}/"
+        return hash
+
+
+    # If we are answering to a message, canceling should bring back to
+    # this message.
+    # The message URL requires many information: account ID, mailbox ID,
+    # conversation ID and message ID.
+    finalRedirect: ->
+        if @props.inReplyTo?
             messageID = @props.inReplyTo.get 'id'
             accountID = @props.inReplyTo.get 'accountID'
             mailboxID = AccountStore.getSelectedMailbox().get 'id'
-            mailboxID = AccountStore.getMailbox message, account unless mailboxID?
+            unless mailboxID?
+                mailboxID = AccountStore.getMailbox message, account
             account = @props.accounts[@props.selectedAccountID]
             conversationID = @props.inReplyTo.get('conversationID')
             conversation = MessageStore.getConversation conversationID
-
-            @redirect "#account/#{accountID}/mailbox/#{mailboxID}/conversation/#{conversationID}/#{messageID}/"
-            #@buildUrl
-                #direction: 'first'
-                #action: 'message'
-                #messageID: @props.inReplyTo.get 'messageID'
-                #fullWidth: true
+            options = {accountID, mailboxID, conversationID, messageID}
+            @redirect @buildMessageHash options

         # Else it should bring to the default view
         else
@@ -208,6 +205,20 @@
                 direction: 'first'
                 action: 'default'
                 fullWidth: true
+
+
+    # Cancel brings back to default view. If it's while replying to a message,
+    # it brings back to this message.
+    onCancel: (event) ->
+        event.preventDefault()
+
+        # Action after cancelation: call @props.onCancel
+        # or navigate to message list.
+        if @props.onCancel?
+            @props.onCancel()
+        else
+            @finalRedirect()
+

     _initCompose: ->

@@ -490,10 +501,7 @@
                             # reload conversation to update its length
                             cid = message.conversationID
                             MessageActionCreator.fetchConversation cid
-                        if @props.callback?
-                            @props.callback error
-                        else
-                            @redirect @buildClosePanelUrl @props.layout
+                        @finalRedirect()

     _autosave: ->


FIX : do not change state for removedCOmponent

--- client/app/components/compose.coffee
+++ client/app/components/compose.coffee
@@ -311,13 +311,13 @@
         event.preventDefault() if event?
         @state.isDraft = true
         @sendActionMessage =>
-            @refs.toolbox.setState action: null
+            @refs.toolbox.setState action: null if @refs.toolbox

     sendMessage: (event) ->
         event.preventDefault() if event?
         @state.isDraft = false
         @sendActionMessage =>
-            @refs.toolbox.setState action: null
+            @refs.toolbox.setState action: null if @refs.toolbox

     validateMessage: ->
         return if @state.isDraft


Fix message.toolbox.trash action

Fix message.toolbox.trash action

--- client/app/components/message.coffee
+++ client/app/components/message.coffee
@@ -277,23 +277,29 @@
     onDelete: (event) ->
         event.preventDefault()
         event.stopPropagation()
+
+        success = =>
+            messageID = @props.message.get('id')
+            MessageActionCreator.delete {messageID}
+
+             # Close Detail Panel for deleted message
+            @redirect (url = @buildClosePanelUrl 'second')
+
         needConfirmation = @props.settings.get('messageConfirmDelete')
-        messageID = @props.message.get('id')
+        unless needConfirmation
+            success()
+            return
+
         confirmMessage = t 'mail confirm delete',
             subject: @props.message.get('subject')
-
-        if not needConfirmation
-            MessageActionCreator.delete {messageID}
-        else
-            LayoutActionCreator.displayModal
-                title       : t 'app confirm delete'
-                subtitle    : confirmMessage
-                closeLabel  : t 'app cancel'
-                actionLabel : t 'app confirm'
-                action      : ->
-                    MessageActionCreator.delete {messageID}
-                    LayoutActionCreator.hideModal()
-
+        LayoutActionCreator.displayModal
+            title       : t 'app confirm delete'
+            subtitle    : confirmMessage
+            closeLabel  : t 'app cancel'
+            actionLabel : t 'app confirm'
+            action      : ->
+                LayoutActionCreator.hideModal()
+                success()

     onConversationDelete: ->
         conversationID = @props.message.get('conversationID')


LinkController: don't redirect profile links in non-team

--- client/app/lib/linkcontroller.coffee
+++ client/app/lib/linkcontroller.coffee
@@ -23,9 +23,11 @@
         else if slug is 'koding'
           "/#{nickname}"
         else
-          "/#{slug}/#{nickname}"
+          null
       when JGroup
         "/#{data.slug}"
+
+    return  unless route

     kd.getSingleton('router').handleRoute route, {state : data}  if route?


shortcuts/pane: utilize list controller

shortcuts/pane: utilize list controller

--- client/app/lib/shortcuts/pane.coffee
+++ client/app/lib/shortcuts/pane.coffee
@@ -19,10 +19,13 @@

   viewAppended: ->

-    @addSubView new ListHead
+    @addSubView @listHead = new ListHead
       cssClass: 'list-head'
       description: @description

-    @listViewController = new ShortcutsListController {}, @collection
+    @listController = new ShortcutsListController
+
+    @collection.each (model) =>
+      @listController.addItem model

-    @addSubView @listViewController.getView()
+    @addSubView @listController.getView()


engine#add

engine#add

--- src/engine_game_state.coffee
+++ src/engine_game_state.coffee
@@ -36,7 +36,7 @@
   return {
     # Just pass through to the current state
     add: (className, entityData) ->
-      if en
+      if entityData?
       self.trigger "beforeAdd", entityData
       object = I.currentState.add(entityData)
       self.trigger "afterAdd", object


make drag end mouse coord computation consistent with everywhere else

--- bokehjs/src/coffee/tool/event_generators.coffee
+++ bokehjs/src/coffee/tool/event_generators.coffee
@@ -131,8 +131,8 @@
         if not @button_activated
           @$tool_button.removeClass('active')
         offset = $(e.currentTarget).offset()
-        e.bokehX = e.pageX #- offset.left
-        e.bokehY = e.pageY #- offset.top
+        e.bokehX = e.pageX - offset.left
+        e.bokehY = e.pageY - offset.top
         @eventSink.trigger("#{@options.eventBasename}:DragEnd", e)


fix typo causing no mouse callbacks

fix typo causing no mouse callbacks

--- bokehjs/src/coffee/tool/event_generators.coffee
+++ bokehjs/src/coffee/tool/event_generators.coffee
@@ -42,7 +42,7 @@
           eventSink.trigger("#{toolName}:UpdatingMouseMove", e)
           e.preventDefault()
           e.stopPropagation()
-        )
+      )
       callbacks.push((e, x, y) =>
         if @dragging
           set_bokehXY(e)
@@ -68,8 +68,8 @@
           if y < ystart or y > yend
             @_stop_drag(e)
             return false
-        @plotview.canvas.set('mousemove_callbacks', callbacks)
       )
+      @plotview.canvas.set('mousemove_callbacks', callbacks)
       $(document).bind('keydown', (e) =>
         if e.keyCode == 27 # ESC
           eventSink.trigger("clear_active_tool"))


Make region splitting return left region

--- app/assets/javascripts/mix.js.coffee
+++ app/assets/javascripts/mix.js.coffee
@@ -58,9 +58,11 @@
   # Assume that time is always within a region, since we initialize a region over full audio at start
   splitCandidate = this.getRegionsAt(time)[0]
   wavesurfer.addRegion { start: round1000(time), end: splitCandidate.end, drag: false, resize: false, data: { type: 'segment' }, color: randomColor(0.3) }
-  wavesurfer.regions.list[splitCandidate.id].update(end: round1000(time))
+  this.regions.list[splitCandidate.id].update(end: round1000(time))
+  splitCandidate

 wavesurfer.getRegionsAt = (time) ->
+  time ||= wavesurfer.getCurrentTime()
   region for region_id, region of wavesurfer.regions.list when (region.start <= time && time < region.end)

 window.saveRegions = ->


Added debug output to log how long sheets take to load

--- app/assets/javascripts/sheets.coffee
+++ app/assets/javascripts/sheets.coffee
@@ -22,6 +22,7 @@

 @initializeSheet = (filter_element = '') ->
+  timeInMs = Date.now()
   $("#{filter_element} .chzn-select").chosen({ allow_single_deselect: true })

   $("#{filter_element} [data-object~='variable-typeahead']").each( () ->
@@ -37,6 +38,7 @@
     handle: ".grid-handle"
   )
   signaturesReady()
+  console.log "Sheet initialized in #{Date.now() - timeInMs} ms"

 @evaluateBranchingLogic = () ->
   $('[data-object~="evaluate-branching-logic"]').each( (index, element) ->


fix(composer): remove colon from "Subject"

--- internal_packages/composer/lib/composer-view.cjsx
+++ internal_packages/composer/lib/composer-view.cjsx
@@ -243,7 +243,7 @@
                  key="subject"
                  name="subject"
                  tabIndex="108"
-                 placeholder="Subject:"
+                 placeholder="Subject"
                  disabled={not @state.showsubject}
                  className="compose-field compose-subject"
                  value={@state.subject}


equation builder adds ability for "fractions" to be evaluated

--- javascripts/lib/math/expression_evaluation.coffee
+++ javascripts/lib/math/expression_evaluation.coffee
@@ -87,6 +87,19 @@
             throw new MalformedExpressionError("Invalid Expression")

       });
+
+    refinement.forType(comps.classes.fraction,
+      {
+        eval: (evaluation, pass)->
+          return if pass != "multiplication"
+          num = refinement.refine(@numerator()).eval().toCalculable()
+          denom = refinement.refine(@denominator()).eval().toCalculable()
+          if num && denom
+            comps.build_number(value: precise.div(num, denom))
+          else
+            throw new MalformedExpressionError("Invalid Expression")
+      }
+    )

     @refinement_val = refinement


horizon3: expanded purpose names

horizon3: expanded purpose names

--- app/frontend/horizon/horizon3.coffee
+++ app/frontend/horizon/horizon3.coffee
@@ -10,6 +10,7 @@
   bandWidth = 350
   valueFormat = formatMagnitudeLong
   interval = d3.time.year
+  labelAttr = "key"

   title = ""
   eventListeners = {}
@@ -23,6 +24,7 @@
   chart.useLog10BandSplitting = (_) -> if (!arguments.length) then useLog10Bands else useLog10Bands = _; chart
   chart.showLegend = (_) -> if (!arguments.length) then showLegend else showLegend = _; chart
   chart.valueFormat = (_) -> if (!arguments.length) then valueFormat else valueFormat = _; chart
+  chart.labelAttr = (_) -> if (!arguments.length) then labelAttr else labelAttr = _; chart

   # Supported events: "applyFilter", "ruleMoved"
   chart.on = (eventName, listener) ->
@@ -227,7 +229,7 @@

     item.append("label")
       .attr("class", "title")
-      .text((d) -> d.key)
+      .text((d) -> d[labelAttr])
       # .on "click", ->  d3.select(this.parentElement).select("input")[0][0].click()

@@ -352,7 +354,7 @@

         for d in data
-          t = Math.ceil(tscale(d.date))
+          t = Math.round(tscale(d.date))
           v = d.value

           [band, limits] = valueToBand(v)
@@ -547,6 +549,7 @@
 purposesChart = horizonChart()
   .title("Purposes")
   .interval(timeInterval)
+  .labelAttr("purpose")
   .showLegend(false)
   .on("applyFilter", (selected) -> applyFilter "purpose", selected)
   .on("ruleMoved", (t) ->
@@ -597,7 +600,25 @@
         .entries(data)
       #nested.sort((a, b) -> d3.descending(a.values.extent[1], b.values.extent[1]))

+
   cache = cachingLoad(100)
+  purposesByCode = null
+
+  expandPurposes = (data) ->
+    for obj in data
+      obj.purpose = purposesByCode[obj.key]
+    data
+
+  flatten = (purposeTree) ->
+    codeToName = {}
+    recurse = (node) ->
+      codeToName[node.code] = node.name
+      if node.values?
+        recurse(n) for n in node.values
+    recurse purposeTree
+    codeToName
+
+

   (filters) ->
     loadingStarted()
@@ -614,6 +635,8 @@

       [ donors, recipients, purposes, purposeTree ] = loaded

+      purposesByCode = flatten purposeTree
+      purposes = expandPurposes(purposes)

       d3.select("#donorsChart")
         .datum(donors)
@@ -627,7 +650,6 @@
         .datum(purposes)
         .call(purposesChart)

-      #console.log purposeTree

       loadingFinished()


Add four ozaria level songs

Add four ozaria level songs

--- app/schemas/models/level.ozar.coffee
+++ app/schemas/models/level.ozar.coffee
@@ -154,6 +154,10 @@
     music: c.object {title: 'Music', description: 'Control music playing'},
       play: {title: 'Play', type: 'boolean'}
       file: c.shortString(title: 'File', enum: [
+        '/music/OzariaPhoenixLandsMusicLoop4'
+        '/music/OzariaPhoenixLandsMusicLoop3'
+        '/music/OzariaPhoenixLandsMusicLoop2b'
+        '/music/OzariaPhoenixLandsMusicLoop1b'
         '/music/OzariaCarnivalMusicLoop1',
         '/music/OzariaCarnivalMusicLoop2',
         '/music/OzariaCarnivalMusicLoop3',


Don't show key mods on scoreboard if beatmap is not a convert.

--- resources/assets/coffee/react/beatmapset-page/scoreboard.coffee
+++ resources/assets/coffee/react/beatmapset-page/scoreboard.coffee
@@ -20,6 +20,10 @@
 el = React.createElement

 class BeatmapsetPage.Scoreboard extends React.Component
+  DEFAULT_MODS = ['NM', 'EZ', 'NF', 'HT', 'HR', 'SD', 'PF', 'DT', 'NC', 'HD', 'FL', 'SO']
+  MANIA_KEY_MODS = ['4K', '5K', '6K', '7K', '8K', '9K']
+  MANIA_MODS = ['NM', 'EZ', 'NF', 'HT', 'HR', 'SD', 'PF', 'DT', 'NC', 'FI', 'HD', 'FL']
+
   constructor: (props) ->
     super props

@@ -45,9 +49,13 @@
     modsClassName += ' beatmapset-scoreboard__mods--initial' if _.isEmpty @props.enabledMods

     mods = if @props.beatmap.mode == 'mania'
-      ['NM', 'EZ', 'NF', 'HT', 'HR', 'SD', 'PF', 'DT', 'NC', 'FI', 'HD', 'FL', '4K', '5K', '6K', '7K', '8K', '9K']
+      if @props.beatmap.convert
+        _.concat(MANIA_MODS, MANIA_KEY_MODS)
+      else
+        MANIA_MODS
+
     else
-      ['NM', 'EZ', 'NF', 'HT', 'HR', 'SD', 'PF', 'DT', 'NC', 'HD', 'FL', 'SO']
+      DEFAULT_MODS

     div className: 'beatmapset-scoreboard',
       div className: 'page-tabs',


Make sure that we set record and not a promise on controller

--- assets/scripts/app/routes/job.coffee
+++ assets/scripts/app/routes/job.coffee
@@ -19,6 +19,7 @@
     repo.activate('job')

     if build = model.get('build')
+      build = @store.recordForId('build', build.get('id'))
       @controllerFor('build').set('build', build)

   model: (params) ->


Add `coerceValue` method for cleaning values based on the data type

--- cilantro/static/cilantro/scripts/coffeescript/forms/controls.coffee
+++ cilantro/static/cilantro/scripts/coffeescript/forms/controls.coffee
@@ -107,8 +107,21 @@
             else
                 @$exclude.prop 'disabled', true

+        coerceValue: (value) ->
+            type = @model.get('data').type
+            # Special case since datatypes can be null
+            if value is 'null' then return null
+            if type is 'boolean'
+                if value is 'false'
+                    return false
+                return true
+            if type is 'number'
+                return parseFloat(value)
+            return value
+
+
         getValue: (options) ->
-            @$value.val()
+            @coerceValue @$value.val()

         getOperator: (options={}) ->
             operator = @$operator.val()
@@ -195,10 +208,10 @@
             # Hide the secondary value by default
             @$value2 = @$('[name=value-2]').hide()

-        getValue: ->
-            value = parseFloat @$value.val()
+        getValue: (options) ->
+            value = @coerceValue @$value.val()
             if /range/.test @getOperator()
-                value2 = parseFloat @$value2.val()
+                value2 = @coerceValue @$value2.val()
                 [value, value2]
             else
                 value
@@ -255,7 +268,8 @@
                         @$value.append "<option value=#{obj.value}>#{obj.name} (#{obj.count})</option>"
                     #@$value.select2().css 'width', 'auto'

-
+        getValue: (options) ->
+            (@coerceValue value for value in @$value.val())

     App.StringControl = StringControl
     App.NumberControl = NumberControl


Log from quitAndInstall

Log from quitAndInstall

--- src/browser/auto-updater-win32.coffee
+++ src/browser/auto-updater-win32.coffee
@@ -9,8 +9,11 @@
   setFeedUrl: (@updateUrl) ->

   quitAndInstall: ->
+    console.log 'restarting new atom.exe'
+
     if SquirrelUpdate.existsSync()
       SquirrelUpdate.spawn ['--processStart', 'atom.exe'], ->
+        console.log 'spawned new atom.exe'
         shellAutoUpdater.quitAndInstall()
     else
       shellAutoUpdater.quitAndInstall()


pruned debugger

pruned debugger

--- src/javascript/views/menu/menu-interior.coffee
+++ src/javascript/views/menu/menu-interior.coffee
@@ -17,9 +17,5 @@
     view.pipe(@._eventOutput)
     @add(view)

-    debugger
-
-
-
-
+
 module.exports = MenuInterior


All it takes for mudged functional dependencies to work for now - to be precise, fixed at first param implying the rest

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -138,6 +138,7 @@
     expressions.push "#{collapse toHtml expression} :: #{highlightType expression.tea}" if expression.tea
   types: values mapMap (__ highlightType, _type), subtractMaps ctx._scope(), builtInDefinitions()
   subs: subToObject mapSub highlightType, ctx.substitution
+  fails: ctx.substitution.fails
   ast: expressions
   deferred: ctx.deferredBindings()

@@ -153,9 +154,12 @@
   deferred: ctx.deferredBindings()

 highlightType = (type) ->
-  typeAst = astize tokenize printType type
-  syntaxType typeAst
-  collapse toHtml typeAst
+  if type
+    typeAst = astize tokenize printType type
+    syntaxType typeAst
+    collapse toHtml typeAst
+  else
+    "undefined"

 subToObject = (sub) ->
   ob = {}
@@ -1057,6 +1061,8 @@
           (findFree currentType),
           (substituteList ctx.substitution, matched.tea.constraints)
         # log "assign type", name, (printType currentType), retainedConstraints
+        # Finalizing type again after possibly added substitution when defer constraints
+        currentType = substitute ctx.substitution, type
         ctx.assignType name,
           if polymorphic
             quantifyUnbound ctx, (addConstraints currentType, retainedConstraints)
@@ -1992,8 +1998,10 @@
 constraintsFromInstance = (ctx, constraint) ->
   {className, type} = constraint
   for instance in (ctx.classNamed className).instances
+    # log "trying to find the instance", (printType instance.type.type), (printType constraint)
     substitution = toMatchTypes instance.type.type.types, constraint.types
     if substitution
+      ctx.extendSubstitution substitution
       return map ((c) -> substitute substitution, c), instance.type.constraints
   null

@@ -2393,7 +2401,8 @@
   undefined

 typeNamesOfNormalized = (constraint) ->
-  map (({name}) -> name), constraint.types.types
+  #map (({name}) -> name), constraint.types.types
+  [constraint.types.types[0].name]

 accessList = (what, list) ->
   [first, rest...] = list
@@ -3582,8 +3591,11 @@
     if t1.types.length isnt t2.types.length
       unifyFail t1, t2
     else if _notEmpty t1.types
+      # log "matching ", (printType t1.types[0]), (printType t2.types[0])
       s1 = matchType t1.types[0], t2.types[0]
-      s2 = matchType (new Types t1.types[1...]), (new Types t2.types[1...])
+      # log "after matching", s1
+      # I will imply functional dependency of the form A a b c | a -> b c
+      s2 = mostGeneralUnifier (new Types (substituteList s1, t1.types[1...])), (new Types t2.types[1...])
       s3 = mergeSubs s1, s2
       s3 or
         unifyFail t1, t2
@@ -4898,7 +4910,6 @@
   'functional deps'
   """
     Collection (class [ce e]
-      (# (| ce: e))
       first (fn [in]
         (: (Fn ce e))))

@@ -4911,6 +4922,26 @@
         (list-first in)))
   """
   "(first {42 43 44})", 42
+
+  'functional deps on function'
+  """
+    Map (class [m k v]
+      put (fn [key value map]
+        (: (Fn k v m m))))
+
+    map-map (instance (Map (Map k v) k v)
+      put (macro [key value map]
+        (: (Fn k v (Map k v) (Map k v)))
+        (Js.call (Js.access map "set") {key value})))
+
+    count (macro [map]
+      (: (Fn (Map k v) Num))
+      (Js.access map "size"))
+
+    magic (fn [key map]
+      (put key 42 map))
+  """
+  "(count (magic \\C (Map)))", 1

   'nested pattern matching'
   """


conflict

conflict

--- client/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/app/Applications/Activity.kdapplication/AppController.coffee
@@ -97,24 +97,14 @@
           controller.followedActivityArrived activities.first

     @getView().innerNav.on "NavItemReceivedClick", (data)=>
-<<<<<<< HEAD
-
-      console.log("data??????", data)
+      @isLoading = no
+      @resetAll()
       # the filterList on top of the innerNav is clicked
       if data.filterType
         @filterType = data.filterType
-        @resetAll()
-        @populateActivity()
-      else
-        @resetAll()
+      else
         @setFilter data.type
-        @populateActivity()
-=======
-      @isLoading = no
-      @resetAll()
-      @setFilter data.type
       @populateActivity()
->>>>>>> 8f04135d33d4da3e5351f103c3518b1d7931eb75

   activitiesArrived:(activities)->
     for activity in activities when activity.bongo_.constructorName in @getFilter()


Fixed hoisting search to avoid functions declarations

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -72,6 +72,16 @@
   if Array.isArray ast
     typed ast, (for node in ast
       crawl node, cb)
+  else
+    cb ast, ast.token
+
+crawlWhile = (ast, cond, cb) ->
+  if Array.isArray ast
+    if cond ast
+      typed ast, (for node in ast
+        crawl node, cb)
+    else
+      ast
   else
     cb ast, ast.token

@@ -584,15 +594,18 @@
         names[node.token] = yes
   for [pattern, def], i in wheres
     decided = no
-    crawl def, (node) ->
-      if not node.label
-        name = node.token
-        if not names[name] and not lookupIdentifier name, node
-          if not decided
-            hoistable.push [pattern, def]
-            decided = yes
-          # also save all undefined names
-          (hoistable[hoistable.length - 1][2] ?= []).push name
+    crawlWhile def,
+      (node) ->
+        node.type != 'function'
+      (node) ->
+        if not node.label and not node.type
+          name = node.token
+          if not names[name] and not lookupIdentifier name, node
+            if not decided
+              hoistable.push [pattern, def]
+              decided = yes
+            # also save all undefined names
+            (hoistable[hoistable.length - 1][2] ?= []).push name
     if not decided
       valid.push [pattern, def]
   [valid, hoistable]


Real-time following activity

Real-time following activity

Client receives activity's id, calling bongo to get the full activity data and
displaying it.
Code snippet is displayed as Text for the real time update.
Need to migrate existing following relationships to exchange-to-exchange
bindings.

--- client/app/Applications/Activity.kdapplication/AppController.coffee
+++ client/app/Applications/Activity.kdapplication/AppController.coffee
@@ -99,9 +99,11 @@

     @createFollowedAndPublicTabs()

-    account.on "FollowedActivityArrived", (activity) =>
-      if activity.constructor.name in @currentFilter
-        @activityListController.followedActivityArrived activity
+    account.on "FollowedActivityArrived", (activityId) =>
+      KD.remote.api.CActivity.one {_id: activityId}, (err, activity) =>
+        if activity.constructor.name in @currentFilter
+          activity.snapshot?.replace /"/g, '"'
+          @activityListController.followedActivityArrived activity

     # INITIAL HEIGHT SET FOR SPLIT
     @utils.wait 1000, =>


Added sorting of where definitions

Added sorting of where definitions

--- src/js/app/compilers/teascript/compiler.coffee
+++ src/js/app/compilers/teascript/compiler.coffee
@@ -418,7 +418,73 @@
   ].join '\n'

 compileWhereImpl = (wheres) ->
-  "#{wheres.map(compileDef).join '\n'}"
+  sorted = (sortTopologically constructDependencyGraph wheres).map ({def}) -> def
+  "#{sorted.map(compileDef).join '\n'}"
+
+sortTopologically = ([graph, dependencies]) ->
+  reversedDependencies = reverseGraph graph
+  independent = []
+
+  moveToIndependent = (node) ->
+    independent.push node
+    delete dependencies[name] for name in node.names
+
+  for parent in graph when parent.set.numDependencies is 0
+    moveToIndependent parent
+
+  sorted = []
+  while independent.length > 0
+    finishedParent = independent.pop()
+    sorted.push finishedParent
+    for child in reversedDependencies[finishedParent.names[0]] or []
+      removeDependency child.set, name for name in finishedParent.names
+      moveToIndependent child if child.set.numDependencies is 0
+
+  for parent of dependencies
+    throw new Error "Cyclic definitions between #{(name for name of dependencies).join ','}"
+  sorted
+
+reverseGraph = (nodes) ->
+  reversed = {}
+  for child in nodes
+    for dependencyName of child.set.dependencies
+      (reversed[dependencyName] ?= []).push child
+  reversed
+
+constructDependencyGraph = (wheres) ->
+  lookupByName = {}
+  deps = dependencySet()
+  # find all defined names
+  graph = []
+  for [pattern, def], i in wheres
+    graph[i] = whereDef = def: [pattern, def], set: dependencySet(), names: []
+    crawl pattern, (node) ->
+      if node.label is 'name'
+        whereDef.names.push node.token
+        lookupByName[node.token] = whereDef
+  # then construct local graph
+  for [pattern, def], i in wheres
+    crawl def, (node) ->
+      if node.token of lookupByName
+        child = graph[i]
+        parent = lookupByName[node.token]
+        addDependency child.set, name for name in parent.names unless child is parent
+        addDependency deps, parent
+  [graph, lookupByName]
+
+dependencySet = ->
+  numDependencies: 0
+  dependencies: {}
+
+addDependency = (set, parentName) ->
+  return if set.dependencies[parentName]
+  set.numDependencies += 1
+  set.dependencies[parentName] = true
+
+removeDependency = (set, parentName) ->
+  return if !set.dependencies[parentName]
+  set.numDependencies -= 1
+  delete set.dependencies[parentName]

 compileDef = ([name, def]) ->
   if !def?


wording fix

wording fix

--- client/app/MainApp/filetree/controllers/findertreecontroller.coffee
+++ client/app/MainApp/filetree/controllers/findertreecontroller.coffee
@@ -131,7 +131,7 @@
     if folder.depth > 10
       new KDNotificationView
         type     : "mini"
-        title    : "The folder is in a deep place, changing top folder."
+        title    : "Folder is nested deeply, making it top folder"
         duration : 3000
       @makeTopFolder nodeView