Added block bouncing

Added block bouncing

--- src/script/GameplayState.coffee
+++ src/script/GameplayState.coffee
@@ -60,7 +60,7 @@
     game.physics.arcade.overlap(@ball, @wallTop, @collideY)
     game.physics.arcade.overlap(@ball, @wallLeft, @collideX)
     game.physics.arcade.overlap(@ball, @wallRight, @collideX)
-    game.physics.arcade.overlap(@ball, @boxes, @collideBlock)
+    game.physics.arcade.collide(@ball, @boxes, @collideBlock)

   render: () ->
     game.debug.body(@square)
@@ -79,3 +79,9 @@

   collideBlock: (ball, block) ->
     block.kill()
+
+    if ball.body.touching.up or ball.body.touching.down
+      ball.body.velocity.y *= -2
+
+    if ball.body.touching.left or ball.body.touching.right
+      ball.body.velocity.x *= -2


classy

classy

--- communist.coffee
+++ communist.coffee
@@ -1,13 +1,14 @@
 class Communist
-	contructor : (fun) ->
+	constructor : (fun) ->
 		window.URL = window.URL or window.webkiURL
 		body = "var send;var _f =  #{ fun };self.addEventListener('message', function(_e) {send = function(data){self.postMessage({messege:data,cb:_e.data.cb})};try{self.postMessage({body:_f.apply(null, _e.data.body),cb:_e.data.cb})}catch(_err){self.postMessege({error:_err,cb:_e.data.cb})}})"
 		blob = new Blob [body],
 			type: "text/javascript"
 		bUrl = window.URL.createObjectURL(blob)
 		@_worker = new Worker(bUrl)
+		@
 	CBs : {}
-	send : (data..., cb) ->
+	send : (data..., cb) =>
 		id = ("" + Math.random()).slice(2)
 		@CBs[id] = cb
 		@_worker.postMessage {body:data,cb : id}


Added Lumberjack show_in_sitetree snippet

--- completions.cson
+++ completions.cson
@@ -2540,3 +2540,21 @@
   'module': 'unclecheese/display-logic'
   'scope': '.text.html.php'
   'body': "hasCheckedLessThan(${1})${0}"
+
+######
+#
+# Lumberjack
+#
+######
+
+'show_in_sitetree':
+  'prefix': 'show_in_sitetree'
+  'module': 'silverstripe/lumberjack'
+  'scope': '.text.html.php'
+  'body': """
+  /**
+   * Defines whether a page is displayed within the site tree
+   * @var boolean
+   */
+  private static $show_in_sitetree = ${1:false};${0}
+  """


chat preview and chat histories

chat preview and chat histories

--- src/smc-webapp/editor_chat.cjsx
+++ src/smc-webapp/editor_chat.cjsx
@@ -92,7 +92,6 @@
                 message = immutable.fromJS(x.insert)
                 message = message.set('history', immutable.Stack(immutable.fromJS(x.insert.history)))
                 message = message.set('editing', immutable.Map(x.insert.editing))
-                message = message.set('show_history', immutable.Map(x.insert.show_history))
                 messages = messages.set("#{x.insert.date - 0}", message)
             else if x.remove
                 messages = messages.delete(x.remove.date - 0)
@@ -158,25 +157,6 @@
             is_equal: (a, b) => (a - 0) == (b - 0)
         @syncdb.save()

-    set_show_history: (message, is_history) =>
-        if not @syncdb?
-            # TODO: give an error or try again later?
-            return
-        author_id = @redux.getStore('account').get_account_id()
-
-        if is_history
-            show_history = message.get('show_history').set(author_id, 'true')
-        else
-            show_history = message.get('show_history').remove(author_id)
-
-        @syncdb.update
-            set :
-                show_history : show_history.toJS()
-            where :
-                date: message.get('date')
-            is_equal: (a, b) => (a - 0) == (b - 0)
-        @syncdb.save()
-
     set_to_last_input: =>
         @setState(input:@redux.getStore(@name).get('last_sent'))

@@ -231,8 +211,6 @@
                         x.history = immutable.Stack([initial])
                     if not x.editing
                         x.editing = {}
-                    if not x.show_history
-                        x.show_history = {}
                     v[x.date - 0] = x

                 actions.setState(messages : immutable.fromJS(v))
@@ -246,6 +224,7 @@
         message        : rtypes.object.isRequired  # immutable.js message object
         history        : rtypes.array
         history_author : rtypes.array
+        history_date   : rtypes.array
         account_id     : rtypes.string.isRequired
         date           : rtypes.string
         sender_name    : rtypes.string
@@ -267,14 +246,10 @@
     getInitialState: ->
         edited_message  : @newest_content()
         history_size    : @props.message.get('history').size
+        show_history    : false
         new_changes     : false

     componentWillReceiveProps: (newProps) ->
-        #if @props.edited_input != newProps.edited_input
-        #    @props.actions.set_edited_input()
-        #if @refs.editedMessage
-        #    @setState(edited_message: @refs.editedMessage.getValue())
-        #console.log('component will receive props is called')
         if @state.history_size != @props.message.get('history').size
             @setState(history_size:@props.message.get('history').size)
         changes = false
@@ -285,7 +260,6 @@
         @setState(new_changes : changes)

     shouldComponentUpdate: (next, next_state) ->
-        #@props.edited_input != next.edited_input or
         return @props.message != next.message or
                @props.user_map != next.user_map or
                @props.account_id != next.account_id or
@@ -295,6 +269,7 @@
                @props.editor_name != next.editor_name or
                @props.saved_mesg != next.saved_mesg or
                @state.edited_message != next_state.edited_message or
+               @state.show_history != next_state.show_history or
                ((not @props.is_prev_sender) and (@props.sender_name != next.sender_name))

     componentDidMount: ->
@@ -317,20 +292,26 @@

     show_history: ->
-

-            <Icon name='history'/>
-

+        #No history for mobile, since right now messages in mobile are too clunky
+        if not IS_MOBILE
+

+                <Icon name='history'/>
+

     hide_history: ->
-

-            <Icon name='history'/>
-

+        #No history for mobile, since right now messages in mobile are too clunky
+        if not IS_MOBILE
+

+                <Icon name='history'/>
+

     disable_history: ->
-        @props.actions.set_show_history(@props.message, false)
+        @setState(show_history:false)
+        @props.set_scroll()

     enable_history: ->
-        @props.actions.set_show_history(@props.message, true)
+        @setState(show_history:true)
+        @props.set_scroll()

     show_user_name: ->

@@ -370,16 +351,16 @@
         text ?= "Last edit by #{@props.editor_name}"
         color ?= "#888"

-        if not @is_editing() and other_editors.size == 0 and @newest_content() != ''
+        if not @is_editing() and other_editors.size == 0 and @newest_content().trim() != ''
             edit = "Last edit "
             name = " by #{@props.editor_name}"
-

+

                 {edit}
                 <TimeAgo date={new Date(@props.message.get('history').peek()?.get('date'))} />
                 {name}

         else
-

+

                 {text}

@@ -456,7 +437,7 @@
         if not @props.is_prev_sender and @sender_is_viewer()
             marginTop = "17px"

-        if not @props.is_prev_sender and not @props.is_next_sender and not @props.message.get('show_history').has(@props.account_id)
+        if not @props.is_prev_sender and not @props.is_next_sender and not @state.show_history
             borderRadius = '10px 10px 10px 10px'
         else if not @props.is_prev_sender
             borderRadius = '10px 10px 5px 5px'
@@ -471,20 +452,20 @@
                         {@render_markdown(value) if not @is_editing()}
                         {@render_input() if @is_editing()}
                         {@editing_status() if @props.message.get('history').size > 1 or  @props.message.get('editing').size > 0}
-                        {@show_history() if not @props.message.get('show_history').has(@props.account_id) and @props.message.get('history').size > 1}
-                        {@hide_history() if @props.message.get('show_history').has(@props.account_id) and @props.message.get('history').size > 1}
+                        {@show_history() if not @state.show_history and @props.message.get('history').size > 1}
+                        {@hide_history() if @state.show_history and @props.message.get('history').size > 1}
                         {@get_timeago()}
                     </ListGroupItem>

  {#This div tag fixes a weird bug where
 tags would be rendered below the <ListGroupItem>}
                 </ListGroup>
             </Panel>
-            {@render_history_title(color, font_size) if @props.message.get('show_history').has(@props.account_id)}
-            {@render_history(color, font_size) if @props.message.get('show_history').has(@props.account_id)}
-            {@render_history_footer(color, font_size) if @props.message.get('show_history').has(@props.account_id)}
+            {@render_history_title(color, font_size) if @state.show_history}
+            {@render_history(color, font_size) if @state.show_history}
+            {@render_history_footer(color, font_size) if @state.show_history}
         </Col>

     blank_column:  ->
-        <Col key={2} xs={0} sm={2}></Col>
+        <Col key={2} xs={2} sm={2}></Col>

     # All the render methods
     render_markdown: (value) ->
@@ -495,7 +476,7 @@

     render_history_title: (color, font_size) ->
-        <ListGroupItem style={background:color, fontSize: font_size, borderRadius: '10px 10px 0px 0px'}>
+        <ListGroupItem style={background:color, fontSize: font_size, borderRadius: '10px 10px 0px 0px', textAlign:'center'}>
             Message History
         </ListGroupItem>

@@ -504,7 +485,7 @@
         </ListGroupItem>

     render_history: (color, font_size) ->
-        for date of @props.history and @props.history_author
+        for date of @props.history and @props.history_author and @props.history_date
             value = @props.history[date]
             value = misc.smiley
                 s: value
@@ -512,22 +493,22 @@
             value = misc_page.sanitize_html(value)
             author = @props.user_map.get(@props.history_author[date]).get('first_name') + ' ' + @props.user_map.get(@props.history_author[date]).get('last_name')
             if @props.history[date].trim() == ''
-                text = "Message deleted by"
+                text = "Message deleted "
             else
-                text = "Author:"
+                text = "Last edit "
             <ListGroupItem key={date} style={background:color, fontSize: font_size, paddingBottom:'20px'}>

                     <Markdown value={value}/>

-                    {text + ' ' + author}
+                    {text}
+                    <TimeAgo date={new Date(@props.history_date[date])} />
+                    {' by ' + author}

             </ListGroupItem>

     # TODO: Make this a codemirror input
     render_input: ->
-        #=>@props.edit_func
-        #onChange  = {#=>@setState(edited_message: @refs.editedMessage.getValue())}

             <Input
                 autoFocus = {true}
@@ -572,9 +553,13 @@
         show_heads   : rtypes.bool
         focus_end    : rtypes.func
         saved_mesg   : rtypes.string
+        set_scroll   : rtypes.func

     shouldComponentUpdate: (next) ->
-        return @props.messages != next.messages or @props.user_map != next.user_map or @props.account_id != next.account_id or @props.saved_mesg != next.saved_mesg
+        return @props.messages != next.messages or
+               @props.user_map != next.user_map or
+               @props.account_id != next.account_id or
+               @props.saved_mesg != next.saved_mesg

     get_user_name: (account_id) ->
         account = @props.user_map?.get(account_id)
@@ -614,9 +599,11 @@
             historyList = @props.messages.get(date).get('history').pop().toJS()
             h = []
             a = []
+            t = []
             for j of historyList
                 h.push(historyList[j].content)
                 a.push(historyList[j].author_id)
+                t.push(historyList[j].date)

             sender_name = @get_user_name(@props.messages.get(date)?.get('sender_id'))
             last_editor_name = @get_user_name(@props.messages.get(date)?.get('history').peek()?.get('author_id'))
@@ -625,6 +612,7 @@
                      account_id       = {@props.account_id}
                      history          = {h}
                      history_author   = {a}
+                     history_date     = {t}
                      user_map         = {@props.user_map}
                      message          = {@props.messages.get(date)}
                      date             = {date}
@@ -642,6 +630,7 @@
                      focus_end        = {@props.focus_end}
                      saved_mesg       = {@props.saved_mesg}
                      close_input      = {@close_edit_inputs}
+                     set_scroll       = {@props.set_scroll}
                     />

         return v
@@ -677,17 +666,12 @@
         project_id  : rtypes.string.isRequired
         file_use_id : rtypes.string.isRequired
         path        : rtypes.string
-       # edited_input: rtypes.string

     getInitialState: ->
         input          : ''
         preview        : ''
         preview_button : false
         is_preview_on  : true
-
-    #edit_func: ->
-    #    @props.actions.set_edited_input(@refs.editedMessage.getValue())
-    #    console.log(@props.edited_input)

     mark_as_read: ->
         @props.redux.getActions('file_use').mark_file(@props.project_id, @props.path, 'read')
@@ -729,6 +713,8 @@
         if @refs.on?
             @setState(preview_button:false)
             @setState(is_preview_on:true)
+            if @is_at_bottom() and @refs.preview
+                @_use_saved_position = false

     chat_input_style:
         margin       : "0"
@@ -757,6 +743,11 @@
     is_at_bottom: ->
         # 20 for covering margin of bottom message
         @props.saved_position + @props.offset + 20 > @props.height
+
+    set_chat_log_state: ->
+        if @refs.log_container?
+            node = ReactDOM.findDOMNode(@refs.log_container)
+            @props.actions.save_scroll_state(node.scrollTop, node.scrollHeight, node.offsetHeight)

     scroll_to_bottom: ->
         if @refs.log_container?
@@ -783,26 +774,22 @@
     set_preview_state: ->
         if @refs.log_container?
             @setState(preview:@props.input)
+        if @refs.preview
+            node = ReactDOM.findDOMNode(@refs.preview)
+            @_preview_height = node.offsetHeight - 12 # sets it to 75px starting then scales with height.

     componentWillMount: ->
         @set_preview_state = underscore.debounce(@set_preview_state, 500)
+        @set_chat_log_state = underscore.debounce(@set_chat_log_state, 10)

     componentDidMount: ->
-        #console.log(@props.edited_input)
-        #@props.actions.set_edited_input('')
         @scroll_to_position()

     componentWillReceiveProps: (next) ->
-        #console.log('chatroom component will receive props is called')
         if (@props.messages != next.messages or @props.input != next.input) and @is_at_bottom()
             @_use_saved_position = false

-    #componentWillUpdate: ->
-        #console.log('chatroom component will update is called')
-
     componentDidUpdate: ->
-        #console.log(@props.edited_input)
-        #console.log('chatroom component did update is called')
         if not @_use_saved_position
             @scroll_to_bottom()

@@ -837,7 +824,7 @@
                 wrap: ['', '']
             value = misc_page.sanitize_html(value)

-            <Row style={position:'absolute', bottom:'0px', width:'97.2%'}>
+            <Row ref="preview" style={position:'absolute', bottom:'0px', width:'97.2%'}>
                 <Col xs={0} sm={2}></Col>

                 <Col xs={10} sm={9}>
@@ -858,21 +845,55 @@
             </Row>

     render_preview_button_on: ->
+        tip =
+            Toggles message preview on
+
+
         <Button ref='on' className='smc-big-only' onClick={@button_on_click}>
-            <Icon name='toggle-on'/> Toggle Preview On
+            <Tip title='Toggle Preview On' tip={tip}>
+                <Icon name='toggle-on'/> Preview
+            </Tip>
         </Button>

     render_preview_button_off: ->
+        tip =
+            Toggles message preview off
+
+
         <Button ref='off' className='smc-big-only' onClick={@button_off_click}>
-            <Icon name='toggle-off'/> Toggle Preview Off
+            <Tip title='Toggle Preview Off' tip={tip}>
+                <Icon name='toggle-off'/> Preview
+            </Tip>
+        </Button>
+
+    render_timetravel_button: ->
+        tip =
+            Redirects to the history of the sage-chat
+
+
+        <Button onClick={@show_timetravel} bsStyle='info'>
+            <Tip title='TimeTravel Button' tip={tip}>
+                <Icon name='history'/> TimeTravel
+            </Tip>
+        </Button>
+
+    render_bottom_button: ->
+        tip =
+            Scrolls the chat to the the bottom
+
+
+        <Button onClick={@scroll_to_bottom}>
+            <Tip title='Scroll to Bottom Button' tip={tip}>
+                <Icon name='arrow-down'/> Bottom
+            </Tip>
         </Button>

     render : ->
         if not @props.messages? or not @props.redux?
             return <Loading/>

-        if @props.input.length > 0 and @state.is_preview_on
-            paddingBottom = '75px'
+        if @props.input.length > 0 and @state.is_preview_on and @refs.preview
+            paddingBottom = "#{@_preview_height}px"
         else
             paddingBottom = '0px'

@@ -903,12 +924,8 @@
                     </Col>
                     <Col xs={6} md={6} className="pull-right" style={padding:'2px', textAlign:'right'}>
                         <ButtonGroup>
-                            <Button onClick={@show_timetravel} bsStyle='info'>
-                                <Icon name='history'/> TimeTravel
-                            </Button>
-                            <Button onClick={@scroll_to_bottom}>
-                                <Icon name='arrow-down'/> Scroll to Bottom
-                            </Button>
+                            {@render_timetravel_button()}
+                            {@render_bottom_button()}
                             {@render_preview_button_on() if @state.preview_button}
                             {@render_preview_button_off() if not @state.preview_button}
                         </ButtonGroup>
@@ -926,6 +943,8 @@
                                 file_path    = {if @props.path? then misc.path_split(@props.path).head}
                                 actions      = {@props.actions}
                                 saved_mesg   = {@props.saved_mesg}
+                                focus_end    = {@focus_endpoint}
+                                set_scroll   = {@set_chat_log_state}
                                 show_heads   = true />
                             {@render_preview_message() if @props.input.length > 0 and @state.is_preview_on}
                         </Panel>
@@ -993,6 +1012,7 @@
                                 font_size    = {@props.font_size}
                                 file_path    = {if @props.path? then misc.path_split(@props.path).head}
                                 actions      = {@props.actions}
+                                focus_end    = {@focus_endpoint}
                                 show_heads   = {false} />
                         </Panel>
                     </Col>


hotfix for https://github.com/sagemathinc/smc/issues/1713 -- magenta

--- src/smc-webapp/other-users.cjsx
+++ src/smc-webapp/other-users.cjsx
@@ -158,8 +158,8 @@
     render_letter: ->
         bg = @get_background_color()
         style =
-            backgroundColor : bg
-            color           : if onecolor(bg).magenta() >= 0.4 then 'white' else 'black'
+            backgroundColor : bg   # onecolor doesn't provide magenta in some browsers
+            color           : if (onecolor(bg).magenta?() ? 0) >= 0.4 then 'white' else 'black'
         <span style={misc.merge style, CIRCLE_INNER_STYLE}>
             {@letter()}
         </span>


move x11 button to more logical location

--- src/smc-webapp/project_new.cjsx
+++ src/smc-webapp/project_new.cjsx
@@ -201,6 +201,10 @@
                         tip="Create a command line Linux terminal.  CoCalc includes a full Linux environment.  Run command line software, vim, emacs and more.">
                         <NewFileButton icon='terminal' name='Linux terminal' on_click={@props.create_file} ext='term' />
                     </Tip>
+                    {<Tip title='X11 desktop'   icon='window-restore'
+                        tip='Create an X11 desktop for running graphical applications.  CoCalc lets you collaboratively run any graphical Linux application in your browser.'>
+                        <NewFileButton icon='window-restore' name='X11 desktop' on_click={@props.create_file} ext='x11' />
+                    </Tip> if available.x11}
                 </Col>
             </Row>
             <Row style={row_style}>
@@ -237,10 +241,6 @@
             </Row>
             <Row style={row_style}>
                 <Col sm={12}>
-                    {<Tip title='X11 desktop'   icon='window-restore'
-                        tip='Create an X11 desktop for running graphical applications.  CoCalc lets you collaboratively run any graphical Linux application in your browser.'>
-                        <NewFileButton icon='window-restore' name='X11 desktop' on_click={@props.create_file} ext='x11' />
-                    </Tip> if available.x11}
                    {@props.children}
                 </Col>
             </Row>


Support observer of queue source

Support observer of queue source

--- src/sources/queue.source.coffee
+++ src/sources/queue.source.coffee
@@ -1,10 +1,15 @@
 _ = require "lodash"
 DelayObserver = require "../observers/delay.observer"
+DeadLetterSucceeded = require "../observers/deadLetterSucceeded.observer"
+DidLastRetry = require "../observers/didLastRetry.observer"

 MeliUsersThanNotBelongsToProducteca = process.env.MeliUsersThanNotBelongsToProducteca?.split(",") or []
 MeliUsersThanCanNotRefreshAccessToken = process.env.MeliUsersThanCanNotRefreshAccessToken?.split(",") or []

 IgnoredUsers = _.concat MeliUsersThanNotBelongsToProducteca, MeliUsersThanCanNotRefreshAccessToken
+
+create = (Type) -> ({ redis, app, queue }) ->
+  new Type { redis, app, path: "#{queue}" }

 module.exports =
   newNotification: ({ context: { bindingData : { insertionTime, dequeueCount } }, message }) ->
@@ -15,8 +20,6 @@
   shouldBeIgnored: ({ notification }) ->
     _.includes IgnoredUsers, notification?.message?.user_id?.toString()

-  delayObserver: ({ redis, app, queue }) ->
-    new DelayObserver { redis, app, path: queue }
-
-  deadLetterSucceeded: -> throw new Error "not supported `deadLetterSucceeded`"
-  didLastRetry: -> throw new Error "not supported `didLastRetry`"
+  delayObserver: create DelayObserver
+  deadLetterSucceeded: create DeadLetterSucceeded
+  didLastRetry: create DidLastRetry


require 'Task' uses correct case

require 'Task' uses correct case

--- src/load-snippets-task.coffee
+++ src/load-snippets-task.coffee
@@ -1,4 +1,4 @@
-Task = require 'Task'
+Task = require 'task'
 TextMatePackage = require 'text-mate-package'

 module.exports =


Allow specification of content type in sync

--- src/lib/sync.coffee
+++ src/lib/sync.coffee
@@ -15,7 +15,7 @@

 module.exports = class Sync

-  constructor: (@options) ->
+  constructor: (@options = {}) ->

     @options.parse ?= (response) ->
       response
@@ -40,11 +40,16 @@

       data = JSON.stringify model.toJSON()

+    if data
+      contentType = 'application/json'
+
     network.request url,

       type: methodMap[method]

       data: data
+
+      contentType: contentType

       timeout: options.timeout


Add timer

Add timer

--- code/instance.coffee
+++ code/instance.coffee
@@ -1,15 +1,16 @@
 # Instance interface.
-spawn         = require('child_process').spawn
-fs            = require 'fs'
-net           = require 'net'
-
-_             = require 'underscore'
-async         = require 'async'
+spawn = require('child_process').spawn
+fs = require 'fs'
+net = require 'net'
+
+_ = require 'underscore'
+async = require 'async'
 # https://github.com/jprichardson/node-kexec
-kexec         = require 'kexec'
-
-{Config}      = require 'config'
-settings      = require 'settings'
+kexec = require 'kexec'
+
+{Config} = require 'config'
+settings = require 'settings'
+

 class Instance

@@ -125,19 +126,30 @@

   #### Private methods ####
-
+
+  # If the TIME environment variable is set, show how long hooks take
+  if process.env.TIME?
+    _time_it = (f) -> f()
+  else
+    _time_it = ->
+
   # Runs file if it is of the form DDD_something.r.ext (remotely)
   # or DDD_something.l.ext (locally).
   _call_hook: (hook, callback) =>
     if /^\d+_.+\.r\.\w/.test hook.file
       console.log "  Running remote #{hook.file}"
-      @sh "sh /root/#{hook.file} #{@name}", callback
+      _time_it -> console.time('remote hook')
+      @sh "sh /root/#{hook.file} #{@name}", (args...) ->
+        _time_it -> console.timeEnd('remote hook')
+        callback.apply(args)
     else if /^\d+_.+\.l\.\w/.test hook.file
       console.log "  Running local #{hook.file}"
       hooks_dir="#{settings.config_path}/#{hook.config_name}/hooks"
       oldcwd = process.cwd()
       process.chdir hooks_dir
+      _time_it -> console.time('local hook')
       @_local_sh "#{hooks_dir}/#{hook.file}", [@name], ->
+        _time_it -> console.timeEnd('local hook')
         process.chdir oldcwd
         callback()
     else


wait for instance sshd to be up (kind of hacky)

--- code/instance.coffee
+++ code/instance.coffee
@@ -1,6 +1,7 @@
 #
 # Instance interface.
 spawn         = require('child_process').spawn
+net           = require 'net'

 cf            = require 'config'
 LithiumConfig = require('lithium_config').LithiumConfig
@@ -46,6 +47,7 @@

   # Connect via SSH and execute command
   _ssh: (key, command, callbacks) ->
+    @_wait_for_sshd()
     args = [
       '-o', 'LogLevel=ERROR', '-o', 'UserKnownHostsFile=/dev/null', '-o', 'StrictHostKeyChecking=no', '-i', key, "root@#{@ip_address}"]
     args = args.concat(command.split ' ')
@@ -54,3 +56,14 @@
     ssh.stdout.on 'data', callbacks.stdout
     ssh.stderr.on 'data', callbacks.stderr
     ssh.on 'exit', callbacks.exit
+
+  # Wait until we can connect to port 22 of instance
+  _wait_for_sshd: ->
+    f = =>
+        client = net.connect 22, @ip_address, ->
+          setTimeout ->
+            clearInterval int
+          , 3000
+        client._handle.socket.destroy()
+
+    int = setInterval f, 1000


Fix failing parser error message test

Fix failing parser error message test

--- coffee-script.coffee
+++ coffee-script.coffee
@@ -137,13 +137,13 @@
 parser.yy = require './nodes'

 # Override Jison's default error handling function.
-parser.yy.parseError = (message, {loc, token}) ->
+parser.yy.parseError = (message, {token}) ->
   # Disregard Jison's message, it contains redundant line numer information.
   message = "unexpected #{token}"

-  # FIXME The `loc` received does not correspond to the `token` token, but to
-  # the one before it instead. That's why the test in error_messages.coffee is
-  # failing. There should be a way to get the location data... maybe accessing
-  # the lexer's tokens?
-  {first_line, first_column, last_line, last_column} = loc
+  # The second argument has a `loc` property, which should have the location
+  # data for this token. Unfortunately, Jison seems to send an outdated `loc`
+  # (from the previous token), so we take the location information directly
+  # from the lexer.
+  {first_line, first_column, last_line, last_column} = parser.lexer.yylloc
   throw new CompilerError message, first_line, first_column, last_line, last_column


Increase maximum timeout to 7 seconds.

Increase maximum timeout to 7 seconds.


Spelling comments.

Spelling comments.

--- src/toaster/core/builder.coffee
+++ src/toaster/core/builder.coffee
@@ -389,7 +389,7 @@
     # if cycling is true or @missing is null, initializes empty array
     # for holding missing dependencies
     #
-    # cycling means the redorder method is being called recursively,
+    # cycling means the reorder method is being called recursively,
     # no other methods call it with cycling = true
     @missing = {} if cycling is false


fix error handling

fix error handling

--- src/notebook.coffee
+++ src/notebook.coffee
@@ -331,14 +331,15 @@
     else
       @async ->
         promise = http.get url, dataType: 'text'
-        promise.done (response, status_text, xhr) =>
+        promise.then (response, status_text, xhr) =>
           handle_file @,
             filename: URI(url).filename()
             type: xhr.getResponseHeader 'content-type'
             content: response
           , options
-        promise.fail (response, status_text, error) =>
-          @fns.error status_text
+        promise.fail (response) =>
+          @fns.error response.statusText
+        promise

   cmd 'quiet', 'Hides the input cell', ->
     hide_cell @input_cell


Sidebar: List user workspaces in sidebar.

--- client/Social/Activity/sidebar/activitysidebar.coffee
+++ client/Social/Activity/sidebar/activitysidebar.coffee
@@ -365,16 +365,25 @@
       treeData.push item = new Machine {machine}
       id = item.getId()
       treeData.push
-        id       : "#{id}-workspaces"
-        title    : 'Workspaces'
-        type     : 'title'
-        parentId : id
+        id           : "#{id}-workspaces"
+        title        : 'Workspaces '
+        type         : 'title'
+        parentId     : id
+        machineUId   : machine.uid
+
       treeData.push
-        title    : 'My Workspace'
-        type     : 'workspace'
-        href     : "/IDE/VM/#{machine.uid}"
-        parentId : id
-
+        title        : 'My Workspace'
+        type         : 'workspace'
+        href         : "/IDE/my-workspace"
+        parentId     : id
+
+      KD.userWorkspaces.forEach (workspace) ->
+        treeData.push
+          title      : workspace.name
+          type       : 'workspace'
+          href       : "/IDE/#{workspace.slug}"
+          data       : workspace
+          parentId   : id

     @machineTree.addNode data for data in treeData


Hide if slug is koding

Hide if slug is koding

--- client/landing/app/MainApp/sidebar/groupavatar.coffee
+++ client/landing/app/MainApp/sidebar/groupavatar.coffee
@@ -2,7 +2,7 @@

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

-    options.cssClass = 'group-avatar-drop'
+    options.cssClass = 'group-avatar-drop hidden'
     groupsController = KD.getSingleton 'groupsController'
     super options, groupsController.getCurrentGroupData()

@@ -10,15 +10,15 @@

   render:(slug, group)->
     if group
+
+      @$().css backgroundImage : \
+        "url(#{group.avatar or 'http://lorempixel.com/60/60/?' + @utils.getRandomNumber()})"
+
       @setTooltip
         title : """You are now in #{group.title} group.
-
 Click here to see group's homepage"""
+
 Click here to see group's homepage."""

-      if slug is 'koding'
-        @$().css backgroundImage : "url(images/logos/50.png)"
-      else
-        @$().css backgroundImage : \
-          "url(#{group.avatar or 'http://lorempixel.com/60/60/?' + @utils.getRandomNumber()})"
+      @show() unless slug is 'koding'

   click:->
     super


Routes: handleOAuth: open team login for ouath handler endpoint

with a graceful notification

--- client/landing/site.landing/coffee/core/routes.coffee
+++ client/landing/site.landing/coffee/core/routes.coffee
@@ -89,8 +89,10 @@

   handleOauth = ({ params, query }) ->

-    return kd.singletons.router.openSection 'Login', null, null, (app) ->
+    return kd.singletons.router.openSection 'Team', null, null, (app) ->
+      app.jumpTo 'login'
       if query.provider
+        new kd.NotificationView { title: 'Login in progress...' }
         kd.singletons.oauthController.authCompleted null, query.provider


fix for Opinion Count not being displayed correctly

--- client/app/Applications/Activity.kdapplication/views/tutorialactivityactions.coffee
+++ client/app/Applications/Activity.kdapplication/views/tutorialactivityactions.coffee
@@ -5,16 +5,14 @@

     activity = @getData()

-    @opinionCount?.destroy()
-
     @opinionCountLink  = new ActivityActionLink
       partial     : "Opinions"
       click     : (pubInst, event)=>
         @emit "TutorialActivityLinkClicked"

-    if activity.repliesCount is 0 then @opinionCountLink.hide()
+    if activity.opinionCount is 0 then @opinionCountLink.hide()

-    @opinionCount = new ActivityCommentCount {}, activity
+    @opinionCount = new ActivityOpinionCount {}, activity

     @opinionCount.on "countChanged", (count) =>
       if count > 0 then @opinionCountLink.show()
@@ -43,7 +41,7 @@
   pistachio:->
     """
       {{> @loader}}
-      {{> @opinionCountLink}} {{> @opinionCount}} #{if @getData()?.repliesCount > 0 then " ·" else "" }
+      {{> @opinionCountLink}} {{> @opinionCount}} #{if @getData()?.opinionCount > 0 then " ·" else "" }
       <span class='optional'>
       {{> @shareLink}} ·
       </span>


use chrome latest again

use chrome latest again

--- tests/karma/browserstack.conf.coffee
+++ tests/karma/browserstack.conf.coffee
@@ -2,11 +2,11 @@
 base  = require('./all.conf')

 browsers =
-  'mac-chrome'  : ['OS X', 'Mountain Lion', 'chrome', '30.0 beta']
+  'mac-chrome'  : ['OS X', 'Mountain Lion', 'chrome', 'latest']
   'mac-firefox' : ['OS X', 'Mountain Lion', 'firefox', 'latest']
   'mac-safari'  : ['OS X', 'Mountain Lion', 'safari', 'latest']

-  'windows-8-chrome'  : ['Windows', '8', 'chrome', '30.0 beta']
+  'windows-8-chrome'  : ['Windows', '8', 'chrome', 'latest']
   'windows-8-firefox' : ['Windows', '8', 'firefox', 'latest']
   'windows-8-safari'  : ['Windows', '8', 'safari', 'latest']
   'windows-8-ie-10'   : ['Windows', '8', 'ie', '10.0']


:see_no_evil: Actually push scopeName

:see_no_evil: Actually push scopeName

--- src/token-iterator.coffee
+++ src/token-iterator.coffee
@@ -36,7 +36,7 @@
           if @scopeStarts[@scopeStarts.length - 1] is scopeName
             @scopeStarts.pop()
           else
-            @scopeEnds.push()
+            @scopeEnds.push(scopeName)
           @scopes.pop()
         else
           scope = atom.grammars.scopeForId(tag)


fixed bug with multiple results in input globs

--- src/mist-ninja-builder.coffee
+++ src/mist-ninja-builder.coffee
@@ -88,6 +88,11 @@

     targets = []

+
+    statement.main_inputs =
+      MistGlobber.doAllGlobs statement.main_inputs, @mistDir
+    # TODO error on no inputs
+
     if statement.foreach
       for inp in statement.main_inputs
         targets.push
@@ -101,10 +106,6 @@

     targets.forEach (target)=>
       target.rule = commandHash
-
-      target.main_inputs =
-        MistGlobber.doAllGlobs target.main_inputs, @mistDir
-      # TODO error on no inputs

       build_vars = @compileDict target.main_inputs


Added browser.withOptions.

Added browser.withOptions.

--- src/zombie/browser.coffee
+++ src/zombie/browser.coffee
@@ -49,6 +49,21 @@
     # True to have Zombie report what it's doing.
     @debug = false

+    # ### withOptions(options, fn)
+    #
+    # Changes the browser options, and calls the function with a
+    # callback (reset).  When you're done processing, call the reset
+    # function to bring options back to their previous values.
+    #
+    # See `visit` if you want to see this method in action.
+    @withOptions = (options, fn)->
+      if options
+        restore = {}
+        [restore[k], @[k]] = [@[k], v] for k,v of options
+      fn =>
+        @[k] = v for k,v of restore if restore
+
+    # Sets the browser options.
     if options
       for k,v of options
         @[k] = v if @OPTIONS.indexOf(k) >= 0
@@ -88,7 +103,7 @@
     #
     # Fire a DOM event.  You can use this to simulate a DOM event, e.g. clicking a
     # link.  These events will bubble up and can be cancelled.  With a callback, this
-    # function will call `wait`.
+    # method will call `wait`.
     #
     # * name -- Even name (e.g `click`)
     # * target -- Target element (e.g a link)
@@ -195,18 +210,16 @@
     this.visit = (url, options, callback)->
       if typeof options is "function"
         [callback, options] = [options, null]
-      else if options
-        restore = {}
-        [restore[k], @[k]] = [@[k], v] for k,v of options
-      @on "error", (error)->
-        @removeListener "error", arguments.callee
-        @[k] = v for k,v of restore if restore
-        callback error
-      history._assign url
-      window.document.addEventListener "DOMContentLoaded", =>
-        @removeListener "error", arguments.callee
-        @[k] = v for k,v of restore if restore
-        @wait callback
+      @withOptions options, (reset)=>
+        @on "error", (error)->
+          @removeListener "error", arguments.callee
+          reset()
+          callback error
+        history._assign url
+        window.document.addEventListener "DOMContentLoaded", =>
+          @removeListener "error", arguments.callee
+          reset()
+          @wait callback
       return

     # ### browser.location => Location


project/kucalc: more stats and export for prometheus

--- src/smc-project/kucalc.coffee
+++ src/smc-project/kucalc.coffee
@@ -11,6 +11,14 @@
 # This gets **changed** to true in local_hub.coffee, if a certain
 # command line flag is passed in.
 exports.IN_KUCALC = false
+
+# static values for monitoring and project information
+# uniquely identifies this instance of the local hub
+session_id = misc.uuid()
+# record when this instance started
+start_ts   = (new Date()).getTime()
+# status information
+current_status = {}

 exports.init = (client) ->
     # update project status every 30s
@@ -27,6 +35,7 @@
         (cb) ->
             compute_status (err, s) ->
                 status = s
+                current_status = s
                 cb(err)
         (cb) ->
             client.query
@@ -38,13 +47,17 @@
     )

 exports.compute_status = compute_status = (cb) ->
-    status = {memory:{rss:0}, disk_MB:0}
+    status =
+        memory   : {rss: 0}
+        disk_MB  : 0
+        cpu      : {}
+        start_ts : start_ts
     async.parallel([
         (cb) ->
             compute_status_disk(status, cb)
         (cb) ->
             #compute_status_memory(status, cb)
-            cgroup_memstats(status, cb)
+            cgroup_stats(status, cb)
         (cb) ->
             compute_status_tmp(status, cb)
     ], (err) ->
@@ -76,22 +89,38 @@

 # this grabs the memory stats directly from the sysfs cgroup files
 # the actual usage is the sum of the rss values plus cache, but we leave cache aside
-cgroup_memstats = (status, cb) ->
-    fs.readFile '/sys/fs/cgroup/memory/memory.stat', 'utf8', (err, data) ->
-        if err
-            cb(err)
-            return
-        stats = {}
-        for line in data.split('\n')
-            [key, value] = line.split(' ')
-            try
-                stats[key] = parseInt(value)
-
+cgroup_stats = (status, cb) ->
+    async.parallel({
+        memory : (cb) ->
+            fs.readFile '/sys/fs/cgroup/memory/memory.stat', 'utf8', (err, data) ->
+                if err
+                    cb(err)
+                    return
+                stats = {}
+                for line in data.split('\n')
+                    [key, value] = line.split(' ')
+                    try
+                        stats[key] = parseInt(value)
+                cb(null, stats)
+
+        cpu : (cb) ->
+            fs.readFile '/sys/fs/cgroup/cpu,cpuacct/cpuacct.usage', 'utf8', (err, data) ->
+                if err
+                    cb(err)
+                    return
+                try
+                    cb(null, parseFloat(data) / Math.pow(10, 9))
+                catch
+                    cb(null, 0.0)
+
+    }, (err, res) ->
         kib = 1024 # convert to kibibyte
-        status.memory.rss += (stats.total_rss ? 0 + stats.total_rss_huge ? 0) / kib
-        status.memory.cache = (stats.cache ? 0) / kib
-        status.memory.limit = (stats.hierarchical_memory_limit ? 0) / kib
+        status.memory.rss  += (res.memory.total_rss ? 0 + stats.total_rss_huge ? 0) / kib
+        status.memory.cache = (res.memory.cache ? 0) / kib
+        status.memory.limit = (res.memory.hierarchical_memory_limit ? 0) / kib
+        status.cpu.usage    = res.cpu
         cb()
+    )

 disk_usage = (path, cb) ->
@@ -138,13 +167,30 @@
     setInterval(test_firewall, interval_ms)
     return

+exports.prometheus_metrics = () ->
+    {get_bugs_total} = require('./local_hub')
+    labels = "project_id=\"#{project_id}\",session_id=\"#{session_id}\""
+    """
+    # HELP kucalc_project_bugs_total The total number of caught bugs.
+    # TYPE kucalc_project_bugs_total counter
+    kucalc_project_bugs_total{#{labels}} #{get_bugs_total()}
+    # HELP kucalc_project_start_time when the project/session started
+    # TYPE kucalc_project_start_time counter
+    kucalc_project_start_time{#{labels}} #{start_ts}
+    # HELP kucalc_project_cpu_usage_seconds
+    # TYPE kucalc_project_cpu_usage_seconds counter
+    kucalc_project_start_time{#{labels}} #{current_status.cpu?.usage ? 0.0}
+    # HELP kucalc_project_memory_usage_ki
+    # TYPE kucalc_project_memory_usage_ki gauge
+    kucalc_project_memory_usage_ki #{current_status.memory?.rss ? 0.0}
+    # HELP kucalc_project_memory_limit_ki
+    # TYPE kucalc_project_memory_limit_ki gauge
+    kucalc_project_memory_limit_ki #{current_status.memory?.limit ? 0.0}
+    """
+
 # called inside raw_server
 exports.init_health_metrics = (raw_server, project_id) ->
     return if not exports.IN_KUCALC
-    # uniquely identifies this instance of the local hub
-    session_id = misc.uuid()
-    # record when this instance started
-    start_ts   = (new Date()).getTime()

     # Setup health and metrics (no url base prefix needed)
     raw_server.use '/health', (req, res) ->
@@ -156,13 +202,4 @@
     raw_server.use '/metrics', (req, res) ->
         res.setHeader("Content-Type", "text/plain; version=0.0.4")
         res.setHeader('Cache-Control', 'private, no-cache, must-revalidate')
-        {get_bugs_total} = require('./local_hub')
-        labels = "project_id=\"#{project_id}\",session_id=\"#{session_id}\""
-        res.send("""
-        # HELP kucalc_project_bugs_total The total number of caught bugs.
-        # TYPE kucalc_project_bugs_total counter
-        kucalc_project_bugs_total{#{labels}} #{get_bugs_total()}
-        # HELP kucalc_project_start_time when the project/session started
-        # TYPE kucalc_project_start_time counter
-        kucalc_project_start_time{#{labels}} #{start_ts}
-        """)
+        res.send(exports.prometheus_metrics())


fix issue #723: "Sign out" evidently actually does the same thing as "Sign out everywhere"

--- src/smc-webapp/r_account.cjsx
+++ src/smc-webapp/r_account.cjsx
@@ -403,7 +403,7 @@
         <Well style={marginTop: '15px'}>
             {text}
             <ButtonToolbar style={textAlign: 'center', marginTop: '15px'}>
-                <Button bsStyle="primary" onClick={=>@props.redux.getActions('account').sign_out(everywhere : @props.everywhere)}>
+                <Button bsStyle="primary" onClick={=>@props.redux.getActions('account').sign_out(@props.everywhere)}>
                     <Icon name="external-link" /> Sign out
                 </Button>
                 <Button onClick={=>@props.redux.getActions('account').setState(show_sign_out : false)}} >


remove plain iphone from simulator device types and set the default to 4 inch retina

--- src/steroids/Simulator.coffee
+++ src/steroids/Simulator.coffee
@@ -8,8 +8,8 @@

 class Simulator

-  DEFAULT_DEVICE_TYPE: "iphone"
-  SUPPORTED_DEVICE_TYPES: ["iphone", "ipad", "ipad_retina", "iphone_retina_3_5_inch", "iphone_retina_4_inch"]
+  DEFAULT_DEVICE_TYPE: "iphone_retina_4_inch"
+  SUPPORTED_DEVICE_TYPES: ["ipad", "ipad_retina", "iphone_retina_3_5_inch", "iphone_retina_4_inch"]

   running: false


Uncolor fields on correct answer

Uncolor fields on correct answer

--- resources/static/brunch/src/app/views/home_view.coffee
+++ resources/static/brunch/src/app/views/home_view.coffee
@@ -19,15 +19,18 @@

   checkRules: ->
     callback = (data) ->
+      flds = $('#rules-tree-div input[type="text"]')
       if _.all data, _.identity
-        $("#dialog").html("That's correct!")
-        $("#dialog").dialog
+        dial = $("#dialog")
+        dial.html("That's correct!")
+        dial.dialog
           title: "That's correct!",
           modal: true
           buttons:
-            Ok: => $("#dialog").dialog("close")
+            Ok: => dial.dialog("close")
+        flds.each( -> $(this).css "background-color", "#fff")
       else
-        $('#rules-tree-div input[type="text"]').each(
+        flds.each(
           -> if data.shift()
                $(this).css "background-color", "#afa"
              else


Call git fetch to keep remote data in sync.

--- prompt.coffee
+++ prompt.coffee
@@ -1,6 +1,8 @@
 module.exports = (Impromptu, section) ->
   system = @module.require 'impromptu-system'
   git = @module.require 'impromptu-git'
+
+  git.fetch()

   section 'user',
     content: [system.user, system.shortHost]


remove styled properties from render-html

--- src/render/render_html.coffee
+++ src/render/render_html.coffee
@@ -12,10 +12,6 @@
     tag: tag
     name_tag: name_tag(T, tag_name)
     style: {
-#      border: world.get 'stroke'
-#      border_width: world.get 'stroke-width' if has_border
-#      border_style: 'solid' if has_border
-      background: world.get_local 'fill'
       margin: world.get 'margin'
       padding: world.get 'padding'
       position: world.get_local 'position'


use session helper in Apollos.user

use session helper in Apollos.user

--- tests/mocha/apollos/user.coffee
+++ tests/mocha/apollos/user.coffee
@@ -5,35 +5,28 @@
   describe 'Apollos', ->
     describe 'user', ->

-      beforeEach ->
-        Meteor.flush()
+      if Meteor.isClient

-      before (done) ->
-        if Meteor.isClient
-          Meteor.logout()
-          Meteor.autorun ->
-            user = Apollos.user()
-            done() if Object.keys(user).length is 0
-        else
-          done()
+        beforeEach ->
+          Meteor.flush()

+        before (done) ->
+          SessionHelper.logout(done)

-      it 'should be defined', ->
-        assert typeof Apollos.user is 'function'
-      it 'should return object', ->
-        assert typeof Apollos.user() is 'object'
+        it 'should be defined', ->
+          assert typeof Apollos.user is 'function'
+        it 'should return object', ->
+          assert typeof Apollos.user() is 'object'

-      describe 'when not signed in', ->
+        describe 'when not signed in', ->

-        if Meteor.isClient
           it 'should have no id', ->
             assert.isUndefined Apollos.user()._id
           it 'should have no emails', ->
             assert.isUndefined Apollos.user().emails

-      describe 'when signed in', ->
+        describe 'when signed in', ->

-        if Meteor.isClient
           before (done) ->
             Apollos.user.create 'apollos.user@newspring.cc', 'testPassword'
             Meteor.autorun ->


Convert \n to <br>

Convert \n to <br>

--- src/rich_text/renderer.coffee
+++ src/rich_text/renderer.coffee
@@ -4,11 +4,13 @@
   render: ->
     container = document.createElement("div")
     @text.eachRun (string, attributes) ->
-      element = document.createElement("span")
-      textNode = document.createTextNode(string)
-      element.style["font-weight"] = "bold" if attributes.bold
-      element.style["font-style"] = "italic" if attributes.italic
-      element.style["text-decoration"] = "underline" if attributes.underline
-      element.appendChild(textNode)
-      container.appendChild(element)
+      for substring, index in string.split("\n")
+        container.appendChild(document.createElement("br")) if index > 0
+        element = document.createElement("span")
+        textNode = document.createTextNode(substring)
+        element.style["font-weight"] = "bold" if attributes.bold
+        element.style["font-style"] = "italic" if attributes.italic
+        element.style["text-decoration"] = "underline" if attributes.underline
+        element.appendChild(textNode)
+        container.appendChild(element)
     container


IMPROVED: ArtCanvas now handles mip-map caching management - shares mip-maps across BitmapElements if using the same bitmap, releases them if too many are allocated

--- source/Art/Engine/Elements/Shapes/BitmapElement.coffee
+++ source/Art/Engine/Elements/Shapes/BitmapElement.coffee
@@ -8,73 +8,11 @@
 {isImage, Bitmap, BitmapBase} = require 'art-canvas'
 ShadowableElement = require '../ShadowableElement'

+{SourceToBitmapCache} = require '../../Core'
+
 defineModule module, class BitmapElement extends ShadowableElement

-  class BitmapElement.SourceToBitmapCache extends BaseClass
-    @singletonClass()
-
-    constructor: ->
-      @_cache = {}
-      @_referenceCounts = {}
-      @_loaded = {}
-
-    # OUT: promise.then (bitmap) ->
-    get: (url, initializerPromise) ->
-      return Bitmap.get url unless isString url # non-string sources are not cached
-      @_referenceCounts[url] = (@_referenceCounts[url] || 0) + 1
-      @_cache[url] ||= initializerPromise || @_get url
-      .tap (bitmap) => @_loaded[url] = bitmap
-
-    _get: (url, tryCount = 0) ->
-      Bitmap.get url
-      # .tap =>
-      #   if tryCount > 0
-      #     log sourceToBitmapCacheSuccess: {url, tryCount, refs: @_referenceCounts[url]}
-      .catch (error) =>
-        if @_referenceCounts[url] > 0
-          retryInSeconds = min(30, (2 ** tryCount)) * .9 + .2 * Math.random()
-          # log sourceToBitmapCache1: {retryInSeconds, url, tryCount, refs: @_referenceCounts[url], message: error?.message}
-          timeout 1000 * retryInSeconds, =>
-            # log sourceToBitmapCache2: {retryInSeconds, url, tryCount, refs: @_referenceCounts[url]}
-            if @_referenceCounts[url] > 0
-              @_get url, tryCount + 1
-            else throw error
-        else throw error
-
-    loaded: (url) -> @_loaded[url]
-
-    @getter
-      memoryUsage: ->
-        bytes = 0
-        for k, v of @_loaded
-          bytes += v.byteSize
-        bytes
-
-    temporaryPut: (duration, url, bitmap) ->
-      @get url, Promise.resolve bitmap
-      # log temporaryPut: {bitmap, duration, url}
-      timeout duration, =>
-        # log temporaryPut: release: {bitmap, url}
-        @release url
-
-    # returns true if the bitmap was released
-    # returns false if there are still other references
-    release: (url) ->
-      return unless isString url
-      # log "SourceToBitmapCache#release: #{url}"
-      throw new Error "no references for #{url}" unless isNumber @_referenceCounts[url]
-      if @_referenceCounts[url] == 0 || !isNumber @_referenceCounts[url]
-        return console.error "invalid referenceCount: #{inspect @_referenceCounts[url]} for url: #{url}"
-
-      @_referenceCounts[url]--
-      if @_referenceCounts[url] == 0
-        delete @_cache[url]
-        delete @_loaded[url]
-        true
-      else
-        false
-
-  @bitmapCache: sourceToBitmapCache = BitmapElement.SourceToBitmapCache.singleton
+  @bitmapCache: sourceToBitmapCache = SourceToBitmapCache.singleton

   constructor: (options) ->
     super
@@ -107,8 +45,6 @@
     bitmap:
       default:    null
       validate:   (v) -> !v || v instanceof BitmapBase
-      postSetter: (newV, oldV) ->
-        @_mipmaps = null if newV != oldV

   @virtualProperty
     mode: setter: (mode) ->
@@ -167,14 +103,14 @@

   _prepareDrawOptions: (drawOptions, compositeMode, opacity)->
     super
-    drawOptions.mipmaps     = @_mipmaps ? true
+    drawOptions.mipmap      = true
     drawOptions.focus       = @_focus
     drawOptions.layout      = @getLayout()
     drawOptions.targetSize  = @getSizeForChildren()
     drawOptions.aspectRatio = @getAspectRatio()

   _drawBitmapElement: (target, elementToTargetMatrix, options) ->
-    @_mipmaps = target.drawBitmapWithLayout elementToTargetMatrix, @getCurrentBitmap(), options
+    target.drawBitmapWithLayout elementToTargetMatrix, @getCurrentBitmap(), options

   fillShape: (target, elementToTargetMatrix, options) ->
     @_drawBitmapElement target, elementToTargetMatrix, options


Further limit data and controls shown for public queries

--- src/coffee/cilantro/ui/query/item.coffee
+++ src/coffee/cilantro/ui/query/item.coffee
@@ -69,11 +69,15 @@
                 @ui.shareCount.attr('title', emailHTML)
                 @ui.shareCount.tooltip({animation: false, html: true, placement: 'right'})
             else
-                # Assuming public queries are supported, we don't want to show
-                # any non-owner information on the query if it is public.
-                if c.isSupported('2.2.0') and @model.get('public')
-                    @ui.nonOwner.hide()
+                @ui.owner.hide()

+            # If this is a public query(assuming those are supported), we don't
+            # want the user to see anything outside of the name. If they own
+            # or are a shared user on the query they will see it in their
+            # private list and can edit or delete it there if they have
+            # sufficiaent privileges.
+            if c.isSupported('2.2.0') and @model.get('public')
+                @ui.nonOwner.hide()
                 @ui.owner.hide()

     { EmptyQueryItem, QueryItem }


fix bug

fix bug

--- src/coffee/smart-fullscreenscroll.coffee
+++ src/coffee/smart-fullscreenscroll.coffee
@@ -69,7 +69,7 @@
           @fullscreenscrollCss = 'smart-fullscreenscroll'
           @fullscreenscrollItemCss = 'smart-fullscreenscroll-item'
           @animateFactor = 20
-          @children = []
+          @items = []
           @scrollIsOn = false
           @enterFullscreenscrollPromise = null


small change to avoid bad URLs on instantill alma

--- noddy/service/v2/find.coffee
+++ noddy/service/v2/find.coffee
@@ -278,6 +278,7 @@
   res.find = options.find ? true
   # other possible sources are ['base','dissemin','share','core','openaire','bing','fighsare']
   res.sources = options.sources ? ['oabutton','crossref','epmc','reverse','scrape','oadoi','doaj']
+  ou = if typeof options.url is 'string' then options.url else if _.isArray(options.url) then options.url[0] else undefined
   res.sources.push('bing') if options.plugin in ['widget','oasheet'] or options.from in ['illiad','clio'] or (options.url? and options.url.indexOf('alma.exlibrisgroup.com') isnt -1)
   options.refresh = if options.refresh is 'true' then true else if options.refresh is 'false' then false else options.refresh
   try res.refresh = if options.refresh is false then 30 else if options.refresh is true then 0 else parseInt options.refresh


Update sendPostData.coffee

Update sendPostData.coffee

--- src/routes/sendPostData.coffee
+++ src/routes/sendPostData.coffee
@@ -77,6 +77,7 @@
                 nextHashtag = pageConfig.hashtag + "1"
         catch e
             if e != breakException
+                console.log e
                 console.log "Post result: code 2"
                 res.status(500).json
                     code: 2
@@ -164,4 +165,4 @@
                     err: 1
                     message: i18n.internal_server_error
                 return
-            else console.log "Uncaught error: #{err}"+            else console.log "Uncaught error: #{err}"


Add current timestamp to filename

Add current timestamp to filename

--- cgFileUploadWorker.coffee
+++ cgFileUploadWorker.coffee
@@ -19,6 +19,7 @@
     end = bytes_per_chunk
     size = file.size
     name = file.name.replace /[^a-zA-Z-_.0-9]/g, '_'
+    name = "#{ Date.now() }-#{ name }"

     while start < size
         blobs.push file.slice(start, end)


Removing debug output

Removing debug output

--- src/scripts/eardropping.coffee
+++ src/scripts/eardropping.coffee
@@ -71,9 +71,7 @@
     tasks.sort (a,b) ->
       return if a.order >= b.order then 1 else -1

-    console.log "Tasks: #{tasks}"
     tasksToRun = []
-
     for task in tasks
       if new RegExp(task.key, "i").test(robotHeard)
         tasksToRun.push task


add startTicking to GameInstance

add startTicking to GameInstance

--- src/server/GameInstance.coffee
+++ src/server/GameInstance.coffee
@@ -10,14 +10,17 @@
 class GameInstance

   # Creates a new game and start ticking
-  constructor: (config) ->
+  constructor: (config = {}) ->
+    config.ticksPerSecond ?= 10
+    config.autoStart ?= true
+
     @players = {}
     @sockets = {}
     @inputs = []

     @id = Utils.guid()
     @config = config
-    @setTickInterval(@config.ticksPerSecond)
+    @setTickInterval(@config.ticksPerSecond) if @config.autoStart

   # This methods needs to be implemented by a class which extends the Game
   # It says what happens in a game tick.
@@ -27,10 +30,13 @@
   # Change tick interval of the Game
   #
   # @param [Number] tps
-  setTickInterval: (tps) ->
+  setTickInterval: (tps = 10) ->
     @config.ticksPerSecond = tps
     clearInterval(@tickInterval) if @tickInterval?
     @tickInterval = setInterval @tick, 1000 / @config.ticksPerSecond
+
+  startTicking: ->
+    @setTickInterval(@config.ticksPerSecond)

   stopTicking: ->
     clearInterval(@tickInterval) if @tickInterval?


bugfix: trailing slashes could break subject set selection

--- app/main.cjsx
+++ app/main.cjsx
@@ -34,6 +34,7 @@

   componentWillMount: ->
     subject_set_id = window.location.hash.split('/')[2]
+    subject_set_id = null if subject_set_id == ''
     subject_set_id ?= localStorage.getItem 'subject-set-id'

     @client = Panoptes.apiClient


client: only record snapshot if it succeeds.

--- src/smc-util/syncstring.coffee
+++ src/smc-util/syncstring.coffee
@@ -811,7 +811,7 @@
     # NOTE: no-op if only one user!
     set_cursor_locs: (locs) =>
         if @_users.length <= 2
-            # Don't bother in special case when only one user (plus the project -- for 2 above!)
+            # Don't bother in special case when only one user (plus the project -- for 2 above!)
             # since we never display the user's
             # own cursors - just other user's cursors.  This simple optimization will save tons
             # of bandwidth, since many files are never opened by more than one user.
@@ -924,10 +924,14 @@
             # which leads to serious problems!
             obj.sent = time
         x.snapshot = obj.snapshot  # also set snapshot in the @_patch_list, which helps with optimization
-        @_patches_table.set(obj, 'none')
-        # save the snapshot time in the database
-        @_syncstring_table.set({string_id:@_string_id, project_id:@_project_id, path:@_path, last_snapshot:time})
-        @_last_snapshot = time
+        @_patches_table.set obj, 'none' , (err) =>
+            if not err
+                # Only save the snapshot time in the database after the set in the patches table was confirmed as a
+                # success -- otherwise if the user refreshes their browser (or visits later) they lose all their early work!
+                @_syncstring_table.set({string_id:@_string_id, project_id:@_project_id, path:@_path, last_snapshot:time})
+                @_last_snapshot = time
+            else
+                console.warn("failed to save snapshot -- #{err}")
         return time

     # Have a snapshot every @_snapshot_interval patches, except


Stop timer when reaching total duration, fix #28

--- src/scripts/core/Timer.coffee
+++ src/scripts/core/Timer.coffee
@@ -49,6 +49,11 @@
       if @is_playing
         @time[0] += elapsed

+      if @time[0] >= @totalDuration
+        # Stop timer when reaching the end.
+        @time[0] = @totalDuration
+        @stop()
+
       #if @last_time != @time[0]
       @updated.dispatch(@time[0])


clone detector initial dev

clone detector initial dev


Setup Sprite authority

Setup Sprite authority


Move to express since there's no longer a connect.router

--- src/streammachine/core.coffee
+++ src/streammachine/core.coffee
@@ -1,7 +1,7 @@
 _u = require("underscore")
 url = require('url')
 http = require "http"
-connect = require("connect")
+express = require "express"

 module.exports = class Core
@@ -42,10 +42,8 @@
                 rewind: rewind

         # init our server
-        @server = connect connect.query(),
-            require("connect-assets")(),
-            connect.router (app) => @_router(app)
-
+        @server = express.createServer()
+        @_router(@server)
         @server.listen @options.listen

         #@server = http.createServer (req,res) => @_handle(req,res)


RESTORE THINGS

RESTORE THINGS

--- test/collections/fairs.coffee
+++ test/collections/fairs.coffee
@@ -42,7 +42,10 @@

   describe '#pastYearRoundFairs', ->
     beforeEach ->
-      sinon.useFakeTimers()
+      @clock = sinon.useFakeTimers()
+
+    afterEach ->
+      @clock.restore()

     it 'should not display upcoming fairs', ->
       fairs = new Fairs([


fix missing '$' sign for fetchData$ method test cases

--- workers/social/lib/social/models/computeproviders/credential.test.coffee
+++ workers/social/lib/social/models/computeproviders/credential.test.coffee
@@ -335,16 +335,21 @@
     testFetchDataFailure = (method, done) ->
       withConvertedUserAndCredential ({ client, credential }) ->

+        # adding client to args array, if method is for remote api
+        # a client argument will be passed before callback
+        args  = [client]  if method.slice(-1) is '$'
+        args ?= []
+
         queue = [

           ->
-            credential[method] (err, credData) ->
+            credential[method] args..., (err, credData) ->
               credData.remove (err) ->
                 expect(err).to.not.exist
                 queue.next()

           ->
-            credential[method] (err, data) ->
+            credential[method] args..., (err, data) ->
               expect(err?.message).to.be.equal 'No data found'
               queue.next()

@@ -357,12 +362,17 @@

     testFetchDataSuccess = (method, done) ->
       withConvertedUserAndCredential ({ client, credential }) ->
-        credential[method] (err, credData) ->
+
+        checkCredData = (err, credData) ->
           expect(err).to.not.exist
           expect(credData).to.be.an 'object'
           expect(credData.identifier).to.be.equal credential.identifier
           done()

+        if   method.slice(-1) is '$'
+        then credential[method] client, checkCredData
+        else credential[method] checkCredData
+

     describe 'fetchData()', ->

@@ -386,12 +396,12 @@

       it 'should fail if there is no data', (done) ->

-        testFetchDataFailure 'fetchData', done
+        testFetchDataFailure 'fetchData$', done

       it 'should be able to fetch credential data', (done) ->

-        testFetchDataSuccess 'fetchData', done
+        testFetchDataSuccess 'fetchData$', done

   describe 'update$', ->


Disable SneakerS3 in CI environment

Disable SneakerS3 in CI environment

Signed-off-by: Sonmez Kartal <d2d15c3d37396a5f2a09f8b42cf5f27d43a3fbde@koding.com>

--- workers/social/lib/social/models/computeproviders/credentialstore.coffee
+++ workers/social/lib/social/models/computeproviders/credentialstore.coffee
@@ -10,6 +10,8 @@
   SocialCredential = require '../socialapi/credential'

   @SNEAKER_SUPPORTED = do ->
+
+    return no  if process.env.CI

     for key, val of KONFIG.sneakerS3
       return no  if not val or val is ''


Missing some dependencies.

Missing some dependencies.

--- src/FileManager/FilesCtrl.coffee
+++ src/FileManager/FilesCtrl.coffee
@@ -1,5 +1,5 @@
 angular.module('fileManager').
-controller('FilesCtrl', ($scope, session, fileManager, Clipboard, Selection, $document, User)->
+controller('FilesCtrl', ($scope, session, fileManager, Clipboard, Selection, $document, User, $q, notification)->
   unless session.isConnected()
     return false

@@ -51,7 +51,7 @@
       for name in file.metadata.sharedWith ? []
         if not index[name]?
           index[name] = true
-          $scope.share.push {name: name}
+          $scope.share.push({name: name})

   $scope.closeModalShare = ->
     $scope.shareModal = false


translate menu_darwin.cson into zh-tw

translate menu_darwin.cson into zh-tw

--- def/zh-tw/menu_darwin.cson
+++ def/zh-tw/menu_darwin.cson
@@ -3,312 +3,312 @@
     value: "Atom"
     submenu:
       "About Atom":
-        value: "About Atom"
+        value: "關於 Atom"
       "View License":
-        value: "View License"
+        value: "檢視授權"
       "Preferences…":
-        value: "Preferences..."
+        value: "偏好設定..."
       "Config…":
-        value: "Config..."
+        value: "設定檔..."
       "Init Script…":
-        value: "Init Script..."
+        value: "開啟初始指令..."
       "Keymap…":
-        value: "Keymap..."
+        value: "開啟快捷鍵設定..."
       "Snippets…":
-        value: "Snippets..."
+        value: "開啟程式碼片段..."
       "Stylesheet…":
-        value: "Stylesheet..."
+        value: "開啟 Atom 樣式表..."
       "Install Shell Commands":
-        value: "Install Shell Commands"
+        value: "安裝 Shell 命令列工具"
       "Hide Atom":
-        value: "Hide Atom"
+        value: "隱藏 Atom 視窗"
       "Hide Others":
-        value: "Hide Others"
+        value: "隱藏其他視窗"
       "Show All":
-        value: "Show All"
+        value: "顯示全部視窗"
       Quit:
-        value: "Quit"
+        value: "結束 Atom"
   File:
-    value: "File"
+    value: "檔案"
     submenu:
       "New Window":
-        value: "New Window"
+        value: "開新視窗"
       "New File":
-        value: "New File"
+        value: "新增文件"
       "Open…":
-        value: "Open..."
+        value: "開啟..."
       "Add Project Folder…":
-        value: "Add Project Folder..."
+        value: "新增專案資料夾..."
       "Reopen Last Item":
-        value: "Reopen Last Item"
+        value: "重新開啟關閉的文件"
       Save:
-        value: "Save"
+        value: "儲存"
       "Save As…":
-        value: "Save As..."
+        value: "另存新檔..."
       "Save All":
-        value: "Save All"
+        value: "儲存所有檔案"
       "Close Tab":
-        value: "Close Tab"
+        value: "關閉分頁"
       "Close Pane":
-        value: "Close Pane"
+        value: "關閉窗格"
       "Close Window":
-        value: "Close Window"
+        value: "關閉視窗"
       "Close All Tabs":
-        value: "Close All Tabs"
+        value: "關閉所有分頁"
   Edit:
-    value: "Edit"
+    value: "編輯"
     submenu:
       Undo:
-        value: "Undo"
+        value: "復原"
       Redo:
-        value: "Redo"
+        value: "重作"
       Cut:
-        value: "Cut"
+        value: "剪下"
       Copy:
-        value: "Copy"
+        value: "複製"
       "Copy Path":
-        value: "Copy Path"
+        value: "複製路徑"
       Paste:
-        value: "Paste"
+        value: "貼上"
       "Select All":
-        value: "Select All"
+        value: "全選"
       "Toggle Comments":
-        value: "Toggle Comments"
+        value: "註解/取消註解"
       Lines:
-        value: "Lines"
+        value: "行"
         submenu:
           Indent:
-            value: "Indent"
+            value: "增加縮排"
           Outdent:
-            value: "Outdent"
+            value: "減少縮排"
           "Auto Indent":
-            value: "Auto Indent"
+            value: "自動縮排"
           "Move Line Up":
-            value: "Move Line Up"
+            value: "將此行向上移動"
           "Move Line Down":
-            value: "Move Line Down"
+            value: "將此行向下移動"
           "Duplicate Lines":
-            value: "Duplicate Lines"
+            value: "複製"
           "Delete Line":
-            value: "Delete Line"
+            value: "刪除"
           "Join Lines":
-            value: "Join Lines"
+            value: "多行合併"
       Columns:
-        value: "Columns"
+        value: "欄"
         submenu:
           "Move Selection Left":
-            value: "Move Selection Left"
+            value: "將選取範圍向左移"
           "Move Selection Right":
-            value: "Move Selection Right"
+            value: "將選取範圍向右移"
       Text:
-        value: "Text"
+        value: "文字"
         submenu:
           "Upper Case":
-            value: "Upper Case"
+            value: "轉為大寫"
           "Lower Case":
-            value: "Lower Case"
+            value: "轉為小寫"
           "Delete to End of Word":
-            value: "Delete to End of Word"
+            value: "刪除至下一個詞尾"
           "Delete to Previous Word Boundary":
-            value: "Delete to Previous Word Boundary"
+            value: "刪除至上一個詞語邊界"
           "Delete to Next Word Boundary":
-            value: "Delete to Next Word Boundary"
+            value: "刪除至下一個詞語邊界"
           "Delete Line":
-            value: "Delete Line"
+            value: "刪除行"
           Transpose:
-            value: "Transpose"
+            value: "顛倒"
       Folding:
-        value: "Folding"
+        value: "折疊程式碼"
         submenu:
           Fold:
-            value: "Fold"
+            value: "折疊"
           Unfold:
-            value: "Unfold"
+            value: "展開"
           "Unfold All":
-            value: "Unfold All"
+            value: "展開全部"
           "Fold All":
-            value: "Fold All"
+            value: "折疊全部"
           "Fold Level 1":
-            value: "Fold Level 1"
+            value: "折疊至第 1 層縮排"
           "Fold Level 2":
-            value: "Fold Level 2"
+            value: "折疊至第 2 層縮排"
           "Fold Level 3":
-            value: "Fold Level 3"
+            value: "折疊至第 3 層縮排"
           "Fold Level 4":
-            value: "Fold Level 4"
+            value: "折疊至第 4 層縮排"
           "Fold Level 5":
-            value: "Fold Level 5"
+            value: "折疊至第 5 層縮排"
           "Fold Level 6":
-            value: "Fold Level 6"
+            value: "折疊至第 6 層縮排"
           "Fold Level 7":
-            value: "Fold Level 7"
+            value: "折疊至第 7 層縮排"
           "Fold Level 8":
-            value: "Fold Level 8"
+            value: "折疊至第 8 層縮排"
           "Fold Level 9":
-            value: "Fold Level 9"
+            value: "折疊至第 9 層縮排"
       "Reflow Selection":
-        value: "Reflow Selection"
+        value: "重新編排選取區域"
       Bookmark:
-        value: "Bookmark"
+        value: "書籤"
         submenu:
           "View All":
-            value: "View All"
+            value: "檢視所有書籤"
           "Toggle Bookmark":
-            value: "Toggle Bookmark"
+            value: "切換書籤"
           "Jump to Next Bookmark":
-            value: "Jump to Next Bookmark"
+            value: "移至下一個書籤"
           "Jump to Previous Bookmark":
-            value: "Jump to Previous Bookmark"
+            value: "移至上一個書籤"
       "Select Encoding":
-        value: "Select Encoding"
+        value: "選擇編碼"
       "Go to Line":
-        value: "Go to Line"
+        value: "移動至指定行數"
       "Select Grammar":
-        value: "Select Grammar"
+        value: "切換語法"
   View:
-    value: "View"
+    value: "檢視"
     submenu:
       "Toggle Full Screen":
-        value: "Toggle Full Screen"
+        value: "切換全螢幕"
       Panes:
-        value: "Panes"
+        value: "窗格"
         submenu:
           "Split Up":
-            value: "Split Up"
+            value: "向上分割 ↑"
           "Split Down":
-            value: "Split Down"
+            value: "向下分割 ↓"
           "Split Left":
-            value: "Split Left"
+            value: "向左分割 ←"
           "Split Right":
-            value: "Split Right"
+            value: "向右分割 →"
           "Focus Next Pane":
-            value: "Focus Next Pane"
+            value: "移至下一個窗格"
           "Focus Previous Pane":
-            value: "Focus Previous Pane"
+            value: "移至上一個窗格"
           "Focus Pane Above":
-            value: "Focus Pane Above"
+            value: "移至上方窗格 ↑"
           "Focus Pane Below":
-            value: "Focus Pane Below"
+            value: "移至下方窗格 ↓"
           "Focus Pane On Left":
-            value: "Focus Pane On Left"
+            value: "移至左方窗格 ←"
           "Focus Pane On Right":
-            value: "Focus Pane On Right"
+            value: "移至右方窗格 →"
           "Close Pane":
-            value: "Close Pane"
+            value: "關閉窗格"
       Developer:
-        value: "Developer"
+        value: "開發人員"
         submenu:
           "Open In Dev Mode…":
-            value: "Open In Dev Mode..."
+            value: "在開發人員模式中開啟..."
           "Reload Window":
-            value: "Reload Window"
+            value: "重新載入視窗"
           "Run Package Specs":
-            value: "Run Package Specs"
+            value: "執行擴充套件規格測試"
           "Toggle Developer Tools":
-            value: "Toggle Developer Tools"
+            value: "開發人員工具"
       "Increase Font Size":
-        value: "Increase Font Size"
+        value: "放大字型"
       "Decrease Font Size":
-        value: "Decrease Font Size"
+        value: "縮小字型"
       "Reset Font Size":
-        value: "Reset Font Size"
+        value: "重設字型大小"
       "Toggle Soft Wrap":
-        value: "Toggle Soft Wrap"
+        value: "自動換行"
       "Toggle Command Palette":
-        value: "Toggle Command Palette"
+        value: "切換指令面板"
       "Toggle Tree View":
-        value: "Toggle Tree View"
+        value: "切換 Tree View"
   Selection:
-    value: "Selection"
+    value: "選取區域"
     submenu:
       "Add Selection Above":
-        value: "Add Selection Above"
+        value: "向上新增游標 ↑"
       "Add Selection Below":
-        value: "Add Selection Below"
+        value: "向下新增游標 ↓"
       "Single Selection":
-        value: "Single Selection"
+        value: "單一游標"
       "Split into Lines":
-        value: "Split into Lines"
+        value: "建立多個游標"
       "Select to Top":
-        value: "Select to Top"
+        value: "選取至文件頂端"
       "Select to Bottom":
-        value: "Select to Bottom"
+        value: "選取至文件底端"
       "Select Line":
-        value: "Select Line"
+        value: "選取行"
       "Select Word":
-        value: "Select Word"
+        value: "選取詞"
       "Select to Beginning of Word":
-        value: "Select to Beginning of Word"
+        value: "選取至詞首"
       "Select to Beginning of Line":
-        value: "Select to Beginning of Line"
+        value: "選取至行首"
       "Select to First Character of Line":
-        value: "Select to First Character of Line"
+        value: "選取至行內第一個字元"
       "Select to End of Word":
-        value: "Select to End of Word"
+        value: "選取至詞尾"
       "Select to End of Line":
-        value: "Select to End of Line"
+        value: "選取至行尾"
       "Select Inside Brackets":
-        value: "Select Inside Brackets"
+        value: "選取括號內容"
   Find:
-    value: "Find"
+    value: "尋找"
     submenu:
       "Find in Buffer":
-        value: "Find in Buffer"
+        value: "在文件中尋找"
       "Replace in Buffer":
-        value: "Replace in Buffer"
+        value: "在文件中取代"
       "Select Next":
-        value: "Select Next"
+        value: "選擇下一個項目"
       "Select All":
-        value: "Select All"
+        value: "選擇所有項目"
       "Toggle Find in Buffer":
-        value: "Toggle Find in Buffer"
+        value: "切換文件中尋找面板"
       "Find in Project":
-        value: "Find in Project"
+        value: "在專案資料夾中尋找"
       "Toggle Find in Project":
-        value: "Toggle Find in Project"
+        value: "切換專案資料夾中尋找面板"
       "Find Next":
-        value: "Find Next"
+        value: "尋找下一個項目"
       "Find Previous":
-        value: "Find Previous"
+        value: "尋找上一個項目"
       "Replace Next":
-        value: "Replace Next"
+        value: "取代下一個項目"
       "Replace All":
-        value: "Replace All"
+        value: "全部取代"
       "Clear History":
-        value: "Clear History"
+        value: "清除歷史記錄"
       "Find Buffer":
-        value: "Find Buffer"
+        value: "尋找編輯中文件"
       "Find File":
-        value: "Find File"
+        value: "尋找檔案"
       "Find Modified File":
-        value: "Find Modified File"
+        value: "尋找修改過的檔案"
   Packages:
-    value: "Packages"
+    value: "擴充套件"
   Window:
-    value: "Window"
+    value: "視窗"
     submenu:
       Minimize:
-        value: "Minimize"
+        value: "最小化"
       Zoom:
-        value: "Zoom"
+        value: "縮放"
       "Bring All to Front":
-        value: "Bring All to Front"
+        value: "全部移至最上層"
   Help:
-    value: "Help"
+    value: "說明"
     submenu:
       "Terms of Use":
-        value: "Terms of Use"
+        value: "使用條款"
       Documentation:
-        value: "Documentation"
+        value: "說明文件"
       Roadmap:
-        value: "Roadmap"
+        value: "發展藍圖"
       "Frequently Asked Questions":
-        value: "Frequently Asked Questions"
+        value: "常見問題"
       "Community Discussions":
-        value: "Community Discussions"
+        value: "社群討論區"
       "Report Issue":
-        value: "Report Issue"
+        value: "回報問題"
       "Search Issues":
-        value: "Search Issues"
+        value: "搜尋問題"
       "Welcome Guide":
-        value: "Welcome Guide"
+        value: "歡迎畫面與指南"


Tests: Activity: PostWith: Added an extra assertion.

--- tests/src/activity/postwith.coffee
+++ tests/src/activity/postwith.coffee
@@ -33,6 +33,7 @@
     selector = activitySelector + ' .activity-content-wrapper .link-embed-box img'

     browser
+      .waitForElementVisible  '.activity-sidebar .followed.topics', 50000
       .click                  '[testpath="public-feed-link/Activity/Topic/public"]'
       .waitForElementVisible  '[testpath=ActivityInputView]', 25000
       .click                  '[testpath="ActivityTabHandle-/Activity/Public/Recent"]'


directのアクセストークンを環境変数から取得するように変更。 refs #4285

--- src/adapters/direct.coffee
+++ src/adapters/direct.coffee
@@ -1,6 +1,7 @@

 # customize
 endpoint = process.env.HUBOT_DIRECT_ENDPOINT ? "wss://api.direct4b.com/albero-app-server/api"
+accessToken = process.env.HUBOT_DIRECT_TOKEN

 # Hubot dependencies
 Robot                                                = require '../robot'
@@ -32,6 +33,7 @@
      host:     url.parse(endpoint).host
      endpoint: endpoint
      name:     @robot.name
+     access_token: accessToken

    bot = DirectAPI.getInstance();
    bot.setOptions options


tweaks

tweaks

--- scripts/joyent-create-instances.coffee
+++ scripts/joyent-create-instances.coffee
@@ -21,7 +21,7 @@

 setupJoyent = (team, next) ->
   # skip empty teams
-  return next('Empty team') if team.peopleIds.length is 0
+  return next("Error: #{team} team empty") if team.peopleIds.length is 0

   createMachine = (next) ->
     console.log "Creating machine for #{team}"
@@ -32,21 +32,23 @@
     , next

   waitUntilRunning = (machine, next) ->
+    secs = 15
+    console.log("Waiting until #{machine.name} is running")
     i = 0
-    secs = 15
-    joyent.getMachine machine, (err, res) ->
-      return next(err) if err
+    do check = ->
+      joyent.getMachine machine, (err, res) ->
+        return next(err) if err

-      switch res.state
-        when 'initializing', 'provisioning'
-          console.log("#{machine.name} #{res.state} (#{i * res}secs)...")
-          setTimeout (-> waitForMachine(machine, next)), secs * 1000
-          i += 1
-        when 'running'
-          console.log("#{machine.name} running!")
-          return next(null, machine)
-        else
-          return next("#{machine.name} unexpected state: '#{res.state}'")
+        switch res.state
+          when 'initializing', 'provisioning'
+            console.log("#{machine.name} #{res.state} (#{i * secs}s)...")
+            setTimeout check, secs * 1000
+            i += 1
+          when 'running'
+            console.log("#{machine.name} is running!")
+            return next(null, machine)
+          else
+            return next("Error: #{machine.name} in unexpected state: '#{res.state}'")

   logMachine = (machine, next) ->
     console.dir(machine)


fix call of map on non array field

fix call of map on non array field

--- src/utils/type_checking.coffee
+++ src/utils/type_checking.coffee
@@ -68,7 +68,11 @@
         throw WrongShemaError 'empty array' unless type[0]
         value = _default value, defaultValue, []
         arrayType = type[0]
-        return value.map (item) -> castValue item, arrayType
+        result = []
+        if value? and typeof value is 'string'
+            for item in value
+                result.push castValue item, arrayType
+        return result

     # support for (x) -> x use everywhere in cozy apps
     else if typeof type is 'function'


Modified in browser at pixie.strd6.com

Modified in browser at pixie.strd6.com

--- test/number_extensions.coffee
+++ test/number_extensions.coffee
@@ -118,6 +118,6 @@
   ]

   4.circularPoints (p, i) ->
-    equals p.x.toFixed(5), points[i].x.toFixed(5), "#{p.x} == #{points[i]}"
+    equals p.x.toFixed(5), points[i].x.toFixed(5), "#{p.x} == #{points[i].}"

 module undefined


Compare site id using site from original event

Previously the site was being looked for in a copy of the event where
only the range and text keys were picked and so it was always missing.

--- src/app/text-buffer.coffee
+++ src/app/text-buffer.coffee
@@ -73,14 +73,13 @@
   ### Internal ###

   handleTextChange: (event) =>
-    event = _.pick(event, 'oldRange', 'newRange', 'oldText', 'newText')
     @cachedMemoryContents = null
     @conflict = false if @conflict and !@isModified()
-    marker.handleBufferChange(event) for marker in @getMarkers()
-    @trigger 'changed', event
+    bufferChangeEvent = _.pick(event, 'oldRange', 'newRange', 'oldText', 'newText')
+    marker.handleBufferChange(bufferChangeEvent) for marker in @getMarkers()
+    @trigger 'changed', bufferChangeEvent
     @scheduleModifiedEvents()
-    if @state.site.id isnt event.site
-      @trigger 'markers-updated'
+    @trigger 'markers-updated' if @state.site.id isnt event.site

   destroy: ->
     unless @destroyed


Fix RSS feed (broken in ad2f229)

Fix RSS feed (broken in ad2f229)

--- router.coffee
+++ router.coffee
@@ -1,4 +1,6 @@
-Router.onBeforeAction 'loading'
+if Meteor.isClient
+  Router.onBeforeAction 'loading'
+
 Router.map ->

   #


Router options to work with the ms-seo package

Adds SEO information to routes for [ms-seo](https://github.com/DerMambo/ms-seo) package. Description works with `Blog.config()` on the client side for the `blogIndex` template. For blog posts, description is either based on an optional `metaDesc` set during blog creation or the excerpt. I have not yet created the corresponding template fields for `metaDesc`.

--- router.coffee
+++ router.coffee
@@ -40,6 +40,22 @@
   data: ->
     posts: Post.where {},
       sort: publishedAt: -1
+  onAfterAction: ->
+    if not Meteor.isClient
+      return
+    if SEO
+      title = 'Blog'
+      if Blog.settings.title
+        title += " | #{Blog.settings.title}"
+      description = if Blog.settings.description then Blog.settings.description else ''
+      SEO.set
+        title: title
+        meta:
+          description: description
+        og:
+          title: title
+          description: description
+      return

 # BLOG TAG

@@ -52,10 +68,28 @@
   ]
   fastRender: true
   data: ->
+    tag: @params.tag
     posts: Post.where
       tags: @params.tag
     ,
       sort: publishedAt: -1
+  onAfterAction: ->
+    if not Meteor.isClient
+      return
+    if SEO
+      tag = @data().tag
+      title = "Blog: tagged \"#{tag}\""
+      if Blog.settings.title
+        title += " | #{Blog.settings.title}"
+      description = if Blog.settings.description then Blog.settings.description else ''
+      SEO.set
+        title: title
+        meta:
+          description: description
+        og:
+          title: title
+          description: description
+      return

 # SHOW BLOG

@@ -91,6 +125,23 @@
   ]
   data: ->
     Post.first slug: @params.slug
+  onAfterAction: ->
+    if not Meteor.isClient
+      return
+    if SEO
+      title = @data().title
+      if Blog.settings.title
+        title += " | #{Blog.settings.title}"
+      description = if @data().metaDesc then @data().metaDesc else @data().excerpt
+      slug = @data().slug
+      SEO.set
+        title: title
+        meta:
+          description: description
+        og:
+          title: title
+          description: description
+      return

 # BLOG ADMIN INDEX


removed comment

removed comment

--- routes.coffee
+++ routes.coffee
@@ -40,7 +40,6 @@
 		y = parseInt opts.y
 		z = parseInt opts.zoom
 		x = parseInt opts.x
-		#y = (1 << z) - 1 - y
 		if opts.format == "grid.json"
 			layer.getGrid z,x,y,(err, grid)->
 				callback(err) if err
@@ -59,7 +58,6 @@

 Tiles::getTileJson = (opts, callback) ->
-#	console.log "getting tile json"
 	if opts.layer of config.layers
 		layer = config.layers[opts.layer]
 		data = layer.info


Fix UT

Fix UT

--- test/specs/finder.spec.coffee
+++ test/specs/finder.spec.coffee
@@ -7,63 +7,36 @@

 describe 'Finder:', ->
   describe 'getBaseDir:', ->
+    mockCwd = ''
+    before ->
+      mockCwd = process.cwd().match(/.*?(?=[/\\])/)[0] + '/test/path'
     beforeEach ->
       @getBaseDir = finder.__get__ 'getBaseDir'

-    describe 'when on Windows:', ->
-      it 'should be able to handle inner directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> 'X:\\test\\path'
-        @getBaseDir('inner').should.equal 'X:/test/path/inner/'
-        revert()
+    it 'should be able to handle inner directories', ->
+      revert = finder.__set__ 'process',
+        cwd: -> mockCwd
+      @getBaseDir('inner').should.match /^([A-Za-z]:\/|\/)test\/path\/inner\/$/
+      revert()

-      it 'should be able to handle current directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> 'X:\\test\\path'
-        @getBaseDir('.').should.equal 'X:/test/path/'
-        @getBaseDir('').should.equal 'X:/test/path/'
-        @getBaseDir(null).should.equal 'X:/test/path/'
-        @getBaseDir().should.equal 'X:/test/path/'
-        revert()
+    it 'should be able to handle current directories', ->
+      revert = finder.__set__ 'process',
+        cwd: -> mockCwd
+      @getBaseDir('.').should.match /^([A-Za-z]:\/|\/)test\/path\/$/
+      @getBaseDir('').should.match /^([A-Za-z]:\/|\/)test\/path\/$/
+      @getBaseDir(null).should.match /^([A-Za-z]:\/|\/)test\/path\/$/
+      @getBaseDir().should.match /^([A-Za-z]:\/|\/)test\/path\/$/
+      revert()

-      it 'should be able to handle upper directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> 'X:\\test\\path'
-        @getBaseDir('..').should.equal 'X:/test/'
-        @getBaseDir('../asdf').should.equal 'X:/test/asdf/'
-        revert()
+    it 'should be able to handle upper directories', ->
+      revert = finder.__set__ 'process',
+        cwd: -> mockCwd
+      @getBaseDir('..').should.match /^([A-Za-z]:\/|\/)test\/$/
+      @getBaseDir('../asdf').should.match /^([A-Za-z]:\/|\/)test\/asdf\/$/
+      revert()

-      it 'should be able to handle the root directory', ->
-        revert = finder.__set__ 'process',
-          cwd: -> 'X:\\test\\path'
-        @getBaseDir('/').should.equal 'X:/'
-        revert()
-
-    describe 'when on Linux / Unix', ->
-      it 'should be able to handle inner directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> '/test/path'
-        @getBaseDir('inner').should.equal '/test/path/inner/'
-        revert()
-
-      it 'should be able to handle current directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> '/test/path'
-        @getBaseDir('.').should.equal '/test/path/'
-        @getBaseDir('').should.equal '/test/path/'
-        @getBaseDir(null).should.equal '/test/path/'
-        @getBaseDir().should.equal '/test/path/'
-        revert()
-
-      it 'should be able to handle upper directories', ->
-        revert = finder.__set__ 'process',
-          cwd: -> '/test/path'
-        @getBaseDir('..').should.equal '/test/'
-        @getBaseDir('../asdf').should.equal '/test/asdf/'
-        revert()
-
-      it 'should be able to handle the root directory', ->
-        revert = finder.__set__ 'process',
-          cwd: -> '/test/path'
-        @getBaseDir('/').should.equal '/'
-        revert()
+    it 'should be able to handle the root directory', ->
+      revert = finder.__set__ 'process',
+        cwd: -> mockCwd
+      @getBaseDir('/').should.match /^([A-Za-z]:\/|\/)$/
+      revert()


Add tests for punycode algorithm

Add tests for punycode algorithm

--- test/string-codec-test.coffee
+++ test/string-codec-test.coffee
@@ -53,6 +53,8 @@
     'abcDEF':'abcdef'
   'upper':
     'abcDEF':'ABCDEF'
+  'punycode':
+    'ドメイン名例.jp':'xn--eckwd4c7cu47r2wf.jp'
   'md4':
     'abc':'a448017aaf21d8525fc10ae87aa6729d'
   'md5':
@@ -109,6 +111,8 @@
   # depends on local timezone
   # 'unixtime':
   #   '1424580000':'Sun Feb 22 2015 13:40:00 GMT+0900 (JST)'
+  'punycode':
+    'xn--eckwd4c7cu47r2wf.jp':'ドメイン名例.jp'
   # require internet connection
   # 'md5':
   #   '5d41402abc4b2a76b9719d911017c592':'hello'


Has test fix

Has test fix

--- test/tests/VersionTest.coffee
+++ test/tests/VersionTest.coffee
@@ -1,17 +1,15 @@
 describe "Version", ->

-	describe "Version", ->
+	it "should have a branch", ->
+		(typeof Framer.Version.branch).should.equal "string"
+		Framer.Version.branch.length.should.be.above(1)

-		it "should have a branch", ->
-			(typeof Framer.Version.branch).should.equal "string"
-			Framer.Version.branch.length.should.be.above(1)
+	it "should have a hash", ->
+		(typeof Framer.Version.hash).should.equal "string"
+		Framer.Version.hash.length.should.be.above(5)

-		it "should have a hash", ->
-			(typeof Framer.Version.hash).should.equal "string"
-			Framer.Version.branch.length.should.be.above(5)
+	it "should have a build", ->
+		(typeof Framer.Version.build).should.equal "number"

-		it "should have a build", ->
-			(typeof Framer.Version.build).should.equal "number"
-
-		it "should have a date", ->
-			(typeof Framer.Version.date).should.equal "number"+	it "should have a date", ->
+		(typeof Framer.Version.date).should.equal "number"


added tests for using options.this as *this*

--- test/test-chain-builder.coffee
+++ test/test-chain-builder.coffee
@@ -377,3 +377,38 @@
         result = fn ctx
         assert.equal result, true
         assert.equal ctx.found, true
+
+  describe 'test optional *this* on function', ->
+
+    describe 'test for chain', ->
+
+      it 'should make *this* equal to object in options.this', ->
+        thiss = accessed:false
+        options = this: thiss
+        ctx = available:false
+        com1 = (sharedContext) ->
+          this.accessed = true
+          sharedContext.available = true
+        com1.options = options
+        fn = builder.chain [ com1 ]
+        result = fn ctx
+        assert.equal result, true
+        assert.equal ctx.available, true
+        assert.equal thiss.accessed, true
+
+    describe 'test for pipeline', ->
+
+      it 'should make *this* equal to object in options.this', ->
+        thiss = accessed:false
+        options = this: thiss
+        ctx = available:false
+        com1 = (next, sharedContext) ->
+          this.accessed = true
+          sharedContext.available = true
+          next sharedContext
+        com1.options = options
+        fn = builder.pipeline [ com1 ]
+        result = fn ctx
+        assert.equal ctx.available, true, 'ctx.available should be true'
+        assert.equal thiss.accessed, true, 'thiss.accessed should be true'
+        assert.equal result, true


Add failing test for task list updates on links

--- test/unit/test_updates.coffee
+++ test/unit/test_updates.coffee
@@ -440,3 +440,69 @@
   , 20

   item2Checkbox.click()
+
+asyncTest "update ignores items that look like Task List items but are links", ->
+  expect 3
+
+  $('#qunit-fixture').empty()
+
+  container = $ '<div>', class: 'js-task-list-container'
+
+  list = $ '<ul>', class: 'task-list'
+
+  item1 = $ '<li>', class: 'task-list-item'
+  item1Checkbox = $ '<input>',
+    type: 'checkbox'
+    class: 'task-list-item-checkbox'
+    disabled: true
+    checked: false
+
+  item2 = $ '<li>', class: 'task-list-item'
+  item2Checkbox = $ '<input>',
+    type: 'checkbox'
+    class: 'task-list-item-checkbox'
+    disabled: true
+    checked: false
+
+  field = $ '<textarea>', class: 'js-task-list-field', text: """
+    - [ ] (link)
+    - [ ] [reference]
+    - [ ] () collapsed
+    - [ ] [] collapsed reference
+    - [ ] \(escaped item)
+    - [ ] item
+  """
+
+  changes = """
+    - [ ] (link)
+    - [ ] [reference]
+    - [ ] () collapsed
+    - [ ] [] collapsed reference
+    - [ ] \(escaped item)
+    - [x] item
+  """
+
+  item1.append item2Checkbox
+  list.append item1
+  item1.expectedIndex = 1
+
+  item2.append item2Checkbox
+  list.append item2
+  item2.expectedIndex = 2
+
+  container.append list
+  container.append field
+
+  $('#qunit-fixture').append(container)
+  container.taskList()
+
+  field.on 'tasklist:changed', (event, index, checked) =>
+    ok checked
+    equal index, item2.expectedIndex
+    equal field.val(), changes
+
+  setTimeout ->
+    start()
+  , 20
+
+  item2Checkbox.click()


calbackWrapper updated

calbackWrapper updated

--- src/callbackWrapper.coffee
+++ src/callbackWrapper.coffee
@@ -12,7 +12,7 @@
       while true
         try
           match = (expression.exec fn.toString())[1]
-          result.push match if match != undefined
+          result.push match if match?
         catch e then break

       result


Fix action logging in random spec

Fix action logging in random spec

--- spec/text-editor-presenter-spec.coffee
+++ spec/text-editor-presenter-spec.coffee
@@ -1722,8 +1722,8 @@
           referencePresenter = new TextEditorPresenter(presenterParams)
           expect(presenter.state).toEqual referencePresenter.state

+          actions = []
           _.times 30, ->
-            actions = []
             performRandomAction (action) -> actions.push(action)
             actualState = presenter.state
             expectedState = new TextEditorPresenter(presenterParams).state


remove using of deprecated SyncController

--- client/ace/lib/ace.coffee
+++ client/ace/lib/ace.coffee
@@ -203,28 +203,10 @@
       if @getDelegate().parent.active
         @notify 'Nothing to save!'
       return
-    file = @getData()
-    {localSync} = kd.singletons
-    # update the localStorage each time user requested save.
-    if remote.isConnected()
-      @askedForSave = yes
-      @emit 'ace.requests.save', contents
-      # if file is saved, remove it from localStorage
-      localSync.removeFromSaveArray file
-    else
-      # add to list of files that need to be synced.
-      localSync.updateFileContentOnLocalStorage file, contents
-      localSync.addToSaveArray file
-      @prepareSyncListeners()
-
-  prepareSyncListeners: ->
-    {localSync} = kd.singletons
-
-    localSync.on 'LocalContentSynced', (file) =>
-      @notify 'File synced to remote...', null, null, 5000
-
-    localSync.on 'LocalContentCouldntSynced', (file) =>
-      @notify 'File coudn\'t be synced to remote please try again...', null, null, 5000
+
+    @askedForSave = yes
+    @emit 'ace.requests.save', contents
+

   requestSaveAs: ->
     @emit 'ace.requests.saveAs', @getContents()


Optimizing subscriptions.

Optimizing subscriptions.

Subscribing with an empty array still makes a subscription.

--- client/publication.coffee
+++ client/publication.coffee
@@ -131,13 +131,10 @@

 Deps.autorun ->
   # Subscribe to people and group data that appear in permissions for new annotations
-  Meteor.subscribe 'persons-by-ids-or-slugs', getNewAnnotationReadPersons()
-  Meteor.subscribe 'persons-by-ids-or-slugs', getNewAnnotationMaintainerPersons()
-  Meteor.subscribe 'persons-by-ids-or-slugs', getNewAnnotationAdminPersons()
-  Meteor.subscribe 'groups-by-ids', getNewAnnotationReadGroups()
-  Meteor.subscribe 'groups-by-ids', getNewAnnotationMaintainerGroups()
-  Meteor.subscribe 'groups-by-ids', getNewAnnotationAdminGroups()
-  Meteor.subscribe 'groups-by-ids', getNewAnnotationWorkInsideGroups()
+  for persons in [getNewAnnotationReadPersons(), getNewAnnotationMaintainerPersons(), getNewAnnotationAdminPersons()] when persons.length
+    Meteor.subscribe 'persons-by-ids-or-slugs', persons
+  for groups in [getNewAnnotationReadGroups(), getNewAnnotationMaintainerGroups(), getNewAnnotationAdminGroups(), getNewAnnotationWorkInsideGroups()] when groups.length
+    Meteor.subscribe 'groups-by-ids', groups

 class @Publication extends Publication
   @Meta


Better shrinking of oversized stanzas

Better shrinking of oversized stanzas

The guy who committed eb843d79 must be a weirdo anyway.

--- src/xmpp/pubsub_server.coffee
+++ src/xmpp/pubsub_server.coffee
@@ -47,9 +47,9 @@
                     # Retry with smaller result set
                     logger.warn "MaxStanzaSizeExceeded: #{results.length} items"
                     if results.length >= 20
+                        newLength = Math.floor(results.length / 2)
+                    else
                         newLength = results.length - 1
-                    else
-                        newLength = results.length - 10
                     smallerResults = results?.slice(0, newLength)
                     smallerResults.rsm ?= results?.rsm
                     @callback err, smallerResults


pubsub_server: DiscoInfoHandler.reply()

pubsub_server: DiscoInfoHandler.reply()

--- src/xmpp/pubsub_server.coffee
+++ src/xmpp/pubsub_server.coffee
@@ -11,7 +11,7 @@
 # </iq>
 class DiscoInfoHandler extends IqHandler.Handler
     constructor: (stanza) ->
-        super stanza
+        super

         @discoInfoEl = @iq.getChild("query", NS.DISCO_INFO)
         @node = @discoInfoEl && @discoInfoEl.attrs.node
@@ -22,29 +22,43 @@

     run: () ->
         console.log 'run DiscoInfoHandler'
-        queryEl = new xmpp.Element("query", xmlns: NS.DISCO_INFO)
-        if @node
-            queryEl.attrs.node = @node
         features = []
         unless @node
             features.push NS.DISCO_ITEMS, NS.REGISTER
-        features.forEach (feature) ->
-            queryEl.c "feature", var: feature
-        for x in [1..1]
-    	    # Didn't request info about specific node, hence no need
-    	    # to get node config but respond immediately.
-            queryEl.c "identity",
+
+        console.log 'replying'
+        @reply
+            node: @node
+            features: features
+            identities: [
                 category: "pubsub"
                 type: "service"
-                name: "Channels service"
-
-            queryEl.c "identity",
+                name: "Channels service",
                 category: "pubsub"
                 type: "channels"
                 name: "Channels service"
-
-            console.log 'replying'
-            @reply queryEl
+            ]
+
+    reply: (result) ->
+        queryEl = new xmpp.Element("query", xmlns: NS.DISCO_INFO)
+        if result.node?
+            queryEl.attrs.node = result.node
+
+        if result.identities?
+            for identity in result.identities
+                queryEl.c "identity",
+                    category: identity.category
+                    type: identity.type
+                    name: identity.name
+
+        if result.features?
+            for feature in result.features
+                queryEl.c "feature",
+                    var: feature
+
+        # TODO: result.forms
+
+        super queryEl

 exports.handler =