activity: add reload capability to activity pane

--- client/Social/Activity/views/activitypane.coffee
+++ client/Social/Activity/views/activitypane.coffee
@@ -53,6 +53,27 @@
     return panes[path]

+  refreshContent: ->
+
+    options = @getActiveContentOptions()
+
+    @refreshContentPane options
+
+
+  refreshContentPane: (options) ->
+
+    { pane, name } = options
+    { listController } = pane
+
+    listController.removeAllItems()
+    listController.showLazyLoader()
+
+    fetchOptions = {}
+    fetchOptions[name] = yes
+
+    @fetch fetchOptions, @createContentSetter name
+
+
   bindLazyLoader: ->

     @scrollView.wrapper.on 'LazyLoadThresholdReached', =>


TrollMode: Use KD.getOwnerMessage for fetching user

--- client/Social/Activity/views/settingsview.coffee
+++ client/Social/Activity/views/settingsview.coffee
@@ -60,10 +60,8 @@

     {activityController} = KD.singletons

-    {constructorName, _id} = post.account
-    KD.remote.cacheable constructorName, _id, (err, owner) =>
+    KD.getMessageOwner post, (err, owner) =>
       return KD.showErrorNotification err  if err
-      return KD.showNotification "Account not found"  unless owner

       if owner.isExempt
         @addMenuItem 'Unmark User as Troll', ->


Update topicswidget.coffee

Update topicswidget.coffee

--- client/Social/Activity/views/topicswidget.coffee
+++ client/Social/Activity/views/topicswidget.coffee
@@ -20,7 +20,7 @@
             height        : 40
       noItemFoundWidget   : new KDView
         cssClass          : "no-item-found"
-        partial           : "There is no topics."
+        partial           : "There are no topics."

     @addSubView @listController.getView()


fix windowing logic

fix windowing logic

--- chat.coffee
+++ chat.coffee
@@ -83,7 +83,7 @@
 			win.message '', '(disconnected)', type:'system'

 	onJoined: (conn, chan) ->
-		unless chan of conn.windows
+		unless win = conn.windows[chan]
 			win = @makeWin conn, chan
 		win.message '', '(You joined the channel.)', type:'system'
 	onParted: (conn, chan) ->
@@ -152,11 +152,13 @@
 	commands = {
 		join: (chan) ->
 			@currentWindow.conn.irc.send 'JOIN', chan
+			win = @makeWin @currentWindow.conn, chan
+			@switchToWindow win
 		win: (num) ->
 			num = parseInt(num)
 			@switchToWindow @winList[num] if num < @winList.length
 		say: (text...) ->
-			if target = @currentWindow.target and conn = @currentWindow.conn
+			if (target = @currentWindow.target) and (conn = @currentWindow.conn)
 				msg = text.join(' ')
 				@onMessage conn, target, 'privmsg', conn.irc.nick, msg
 				conn.irc.send 'PRIVMSG', target, msg


Remove empty PING handler in chat.coffee, since it's handled in irc.coffee.

--- chat.coffee
+++ chat.coffee
@@ -123,9 +123,6 @@
 				win.message '', "#{from.nick} #{m[1]}", type:'privmsg action'
 			else
 				win.message from.nick, msg, type:'privmsg'
-
-		PING: -> # server handles these for us.
-		# TODO: maybe don't even forward pings to client?
 	}

 	send: (msg...) ->


add tooltips to action_dock

add tooltips to action_dock

--- client/vue/components/action_dock/action_dock.coffee
+++ client/vue/components/action_dock/action_dock.coffee
@@ -10,14 +10,17 @@
       >

-          <v-btn icon :class="`md-button--tiny action-dock__button--${action.name}`" v-if="action.name != 'react'" @click="action.perform()">
-

-            <v-icon>{{action.icon}}</v-icon>
-
-          </v-btn>
+          <v-tooltip bottom>
+            <v-btn slot="activator" icon :class="`md-button--tiny action-dock__button--${action.name}`" v-if="action.name != 'react'" @click="action.perform()">
+

+              <v-icon>{{action.icon}}</v-icon>
+

+
+
+

+            </v-btn>
+

+          </v-tooltip>

     """


Remove commensts in git.coffee

Remove commensts in git.coffee

--- lib/git.coffee
+++ lib/git.coffee
@@ -40,19 +40,9 @@
     else
       reject "no current file"

-module.exports = git = {
-  # Public: Execute a git command.
-  #
-  # args    - An {Array} containing the arguments for the command.
-  # options - An {Object} with the following keys:
-  #   :cwd  - Current working directory as {String}.
-  #   :options - The {Object} with options used by child_process
-  #
-  # Returns a {Promise}.
-  cmd: (args, {cwd, options}={}) ->
+module.exports = git =
+  cmd: (args, options={}) ->
     new Promise (resolve, reject) ->
-      options ?= {}
-      options.cwd ?= cwd
       output = ''
       try
         new BufferedProcess
@@ -83,8 +73,6 @@
         repo.refreshStatus()
         git.cmd ['add', '--refresh', '--', '.'], cwd: repo.getWorkingDirectory()

-  # returns filepath relativized for either a submodule or repository
-  #   otherwise just a full path
   relativize: (path) ->
     git.getSubmodule(path)?.relativize(path) ? atom.project.getRepositories()[0]?.relativize(path) ? path

@@ -124,8 +112,6 @@
         notifier.addSuccess "Added #{file ? 'all files'}"
         true

-  # Public: Get the repository of the current file or project if no current file
-  # Returns a {Promise} that resolves to a repository like object
   getRepo: ->
     new Promise (resolve, reject) ->
       getRepoForCurrentFile().then (repo) -> resolve(repo)
@@ -138,22 +124,15 @@
         else
           resolve(repos[0])

-  # returns submodule for given file or undefined
   getSubmodule: (path) ->
     path ?= atom.workspace.getActiveTextEditor()?.getPath()
     atom.project.getRepositories().filter((r) ->
       r?.repo.submoduleForPath path
     )[0]?.repo?.submoduleForPath path

-  # Returns the working directory for a git repo.
-  # Will search for submodule first if currently
-  #   in one or the project root
-  #
-  # @param andSubmodules boolean determining whether to account for submodules
   dir: (andSubmodules=true) ->
     new Promise (resolve, reject) =>
       if andSubmodules and submodule = git.getSubmodule()
         resolve(submodule.getWorkingDirectory())
       else
         git.getRepo().then (repo) -> resolve(repo.getWorkingDirectory())
-}


chore: disable exit event

chore: disable exit event

--- lib/i2c.coffee
+++ lib/i2c.coffee
@@ -18,7 +18,7 @@
       ).context.wire = @
       process.stdin.emit 'data', '' # trigger repl

-    process.on 'exit', => @close()
+    # process.on 'exit', => @close()

     @on 'data', (data) =>
       @history.push data


Small updates

Small updates

--- components/progressbar/KDProgressBarView.coffee
+++ components/progressbar/KDProgressBarView.coffee
@@ -21,8 +21,10 @@
       cssClass    : "bar spinner hidden"
     @addSubView @darkLabel = new KDCustomHTMLView
       tagName     : "span"
+      cssClass    : 'dark-label'
     @bar.addSubView @lightLabel = new KDCustomHTMLView
       tagName     : "span"
+      cssClass    : 'light-label'

   updateBar:(value, unit, label)->
     {determinate} = @getOptions()


fix coffeescript bug

fix coffeescript bug

--- sharejs-server.coffee
+++ sharejs-server.coffee
@@ -1,4 +1,5 @@
 Meteor.startShareJS = ->
+
   # Creates a (persistent) ShareJS server
   # Roughly copied from https://github.com/share/ShareJS/wiki/Getting-started


Finish removing bookmarks routes

Finish removing bookmarks routes

--- server/route_bindings.coffee
+++ server/route_bindings.coffee
@@ -7,7 +7,6 @@

 dashboardRoutes = require('./routes/dashboard.coffee')
 indicatorRoutes = require('./routes/indicators.coffee')
-bookmarkRoutes  = require('./routes/bookmarks.coffee')
 reportRoutes    = require('./routes/reports.coffee')
 userRoutes      = require('./routes/users.coffee')
 testRoutes      = require('./routes/tests.coffee')
@@ -31,7 +30,6 @@
   app.get "/dashboard", ensureAuthenticated, dashboardRoutes.index
   app.get "/indicators", ensureAuthenticated, indicatorRoutes.index
   app.get "/reports", ensureAuthenticated, reportRoutes.index
-  app.get "/bookmarks", ensureAuthenticated, bookmarkRoutes.index

   app.get "/indicators/:id", ensureAuthenticated, indicatorRoutes.show


test: Add notes; Add some tests

test: Add notes; Add some tests

--- src/test/shared/data.coffee
+++ src/test/shared/data.coffee
@@ -7,7 +7,7 @@
 	{id: 'A5555550', cardType: 'hkid', expect: true}
 	{id: 'C5678909', cardType: 'hkid', expect: true} # modify from Caring and Sharing Scheme sample (2019)
 	{id: 'AB9876543', cardType: 'hkid', expect: true}
-	{id: 'WX1234569', cardType: 'hkid', expect: true}
+	{id: 'WX1234569', cardType: 'hkid', expect: true} # from repo PR5

 	# fail test: length
 	{id: 'A123456', cardType: 'hkid', expect: false}
@@ -20,8 +20,10 @@
 	{id: 'A12345670', cardType: 'hkid', expect: false}

 	# fail test: checksum
-	{id: 'C668668E', cardType: 'hkid', expect: false}
+	{id: 'C668668E', cardType: 'hkid', expect: false} # src: SCMP
 	{id: 'C5678901', cardType: 'hkid', expect: false} # copy from Caring and Sharing Scheme sample (2019)
+	{id: 'Z6833655', cardType: 'hkid', expect: false} # src: Wikipedia
+	{id: 'Z6833672', cardType: 'hkid', expect: false} # src: immd.gov.hk

 	# false test: other impossible ID, e.g. according to practice
 	{id: 'O1234561', cardType: 'hkid', expect: false} # no ID starting with "O"
@@ -43,13 +45,13 @@
 	{id: 'AXXXXXXXX9', cardType: 'twid', expect: false}
 	{id: '0123456789', cardType: 'twid', expect: false}

-	# fail test: gender
-	{id: 'D012345678', cardType: 'twid', expect: false} # only 1 or 2 in 2nd character
-	{id: 'C987654321', cardType: 'twid', expect: false} # only 1 or 2 in 2nd character
-	{id: 'E345678901', cardType: 'twid', expect: false} # only 1 or 2 in 2nd character
+	# fail test: gender (2nd character has to be 1 or 2)
+	{id: 'D012345678', cardType: 'twid', expect: false}
+	{id: 'C987654321', cardType: 'twid', expect: false}
+	{id: 'E345678901', cardType: 'twid', expect: false}

 	# fail test: checksum
-	{id: 'A234567890', cardType: 'twid', expect: false}
+	{id: 'A234567890', cardType: 'twid', expect: false} # src: Apple Daily TW

 	# Taiwan Resident Certificate
@@ -63,12 +65,12 @@
 	# Legacy UID format before 2021 but still valid
 	{id: 'AB12345677', cardType: 'twrc', expect: true}
 	{id: 'AC12345679', cardType: 'twrc', expect: true}
-	{id: 'AB00207171', cardType: 'twrc', expect: true}
-	{id: 'AC03095424', cardType: 'twrc', expect: true}
-	{id: 'BD01300667', cardType: 'twrc', expect: true}
-	{id: 'CC00151114', cardType: 'twrc', expect: true}
-	{id: 'HD02717288', cardType: 'twrc', expect: true}
-	{id: 'TD00251124', cardType: 'twrc', expect: true}
+	{id: 'AB00207171', cardType: 'twrc', expect: true} # from repo PR4
+	{id: 'AC03095424', cardType: 'twrc', expect: true} # from repo PR4
+	{id: 'BD01300667', cardType: 'twrc', expect: true} # from repo PR4
+	{id: 'CC00151114', cardType: 'twrc', expect: true} # src: Wikimedia Commons
+	{id: 'HD02717288', cardType: 'twrc', expect: true} # from repo PR4
+	{id: 'TD00251124', cardType: 'twrc', expect: true} # from repo PR4

 	# fail test: length
 	{id: 'AB1234566', cardType: 'twrc', expect: false}
@@ -79,11 +81,14 @@
 	{id: 'AB1234567X', cardType: 'twrc', expect: false}
 	{id: 'ABC1234567', cardType: 'twrc', expect: false}

-	# fail test: gender
-	{id: 'AE12345673', cardType: 'twrc', expect: false} # only [A-D] in 2nd character
+	# fail test: gender (2nd character has to be [A-D])
+	{id: 'AE12345673', cardType: 'twrc', expect: false}
+	{id: 'YZ50000001', cardType: 'twrc', expect: false} # src: Taiwan News

 	# fail test: checksum
 	{id: 'AB12345670', cardType: 'twrc', expect: false}
+	{id: 'AD12345678', cardType: 'twrc', expect: false} # src: Wikipedia
+	{id: 'HC12345678', cardType: 'twrc', expect: false} # src: Taiwan News

 	# China Resident Identity Card
@@ -92,6 +97,8 @@
 	{id: '11010120170210193X', cardType: 'cnid', expect: true}
 	{id: '120103198806018241', cardType: 'cnid', expect: true}
 	{id: '310101200001013948', cardType: 'cnid', expect: true}
+	{id: '810000199408230021', cardType: 'cnid', expect: true} # src: Wikipedia
+	{id: '830000199201300022', cardType: 'cnid', expect: true} # src: Wikipedia

 	# fail test: length
 	{id: '98765432101234567', cardType: 'cnid', expect: false}


adding picture provider

adding picture provider

--- server/startup/avatar.coffee
+++ server/startup/avatar.coffee
@@ -45,7 +45,12 @@
 		if not file?

 			if Meteor.settings.public.sandstorm
-				console.log "Should redirect to: ", RocketChat.models.Users.findOneByUsername(params.username.replace('.jpg', '')).services.sandstorm.picture
+				if sandstormPictureUrl = RocketChat.models.Users.findOneByUsername(params.username.replace('.jpg', '')).services.sandstorm.picture?
+					console.log "Should redirect to: ", sandstormPictureUrl
+					res.setHeader 'Location', sandstormPictureUrl
+					res.writeHead 302
+					res.end()
+					return

 			res.setHeader 'Content-Type', 'image/svg+xml'
 			res.setHeader 'Cache-Control', 'public, max-age=0'


protecting against empty strings in events

--- src/alert_distributor.coffee
+++ src/alert_distributor.coffee
@@ -48,6 +48,7 @@
   # Parse out event data from StatsD packet
   parsePacket: (packet) ->
     for event in packet.toString().split('\n') or []
+      continue if !event
       [name, data] = event.split ':'
       [metric, type] = data.split '|'
       metric = Number metric if _.isNumbery metric


Created CompositeView from TopChannelView

--- core/app/backbone/views/top_channel_view.coffee
+++ core/app/backbone/views/top_channel_view.coffee
@@ -1,3 +1,28 @@
-class window.TopChannelView extends Backbone.Marionette.ItemView
+class window.TopChannelItemView extends Backbone.Marionette.ItemView
   template: 'channels/topchannel'
   tagName: 'li'
+
+class window.TopChannelView extends Backbone.Marionette.CompositeView
+  template: "users/profile/top_channels"
+  className: "top-channel-container"
+  itemView: TopChannelItemView
+  itemViewContainer: ".top-channels ol"
+  events:
+    "click a.show-more": "showMoreOn"
+    "click a.show-less": "showMoreOff"
+
+  showMoreOn:  ->
+    console.info @$el
+    @$el.addClass 'showMore'
+  showMoreOff: -> @$el.removeClass 'showMore'
+
+  showEmptyView: ->
+      @$(".top-channels").hide()
+      @$(".no-channels").show()
+
+  closeEmptyView: ->
+      @$(".no-channels").hide()
+      @$(".top-channels").show()
+
+  onRender: ->
+    @$(".top-channels .show-more").hide() if @collection.length < 6


start tailing only after model and collection are fetched

--- SingularityUI/app/views/tail.coffee
+++ SingularityUI/app/views/tail.coffee
@@ -17,9 +17,12 @@
     initialize: ({@taskId, @path, firstRequest, @offset}) ->
         @filename = _.last @path.split '/'

+        fetchDone = _.after 2, => @afterInitialData()
+        @model.once 'sync', => fetchDone()
+        @listenTo @collection, 'initialdata', fetchDone()
+
         @listenTo @collection, 'reset',       @dumpContents
         @listenTo @collection, 'sync',        @renderLines
-        @listenTo @collection, 'initialdata', @afterInitialData
         @listenTo @collection, 'initialOffsetData', @afterInitialOffsetData

         @listenTo @collection.state, 'change:moreToFetch', @showOrHideMoreToFetchSpinners


Validate email admin addresses

Validate email admin addresses

--- coffees/src/controllers/league_controller.coffee
+++ coffees/src/controllers/league_controller.coffee
@@ -2,6 +2,14 @@
 models  = require '../../../models'
 authMiddleware = require '../authentication_middleware'
 Sequelize = require 'sequelize'
+
+EMAIL_REGEX = /// ^ (
+  [^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+
+  |\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+
+  |\x22([^\x0d\x22\x5c\x80-\xff]|\x5c[\x00-\x7f])*\x22))*\x40([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+
+  |\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d)(\x2e([^\x00-\x20\x22\x28\x29\x2c\x2e\x3a-\x3c\x3e\x40\x5b-\x5d\x7f-\xff]+
+  |\x5b([^\x0d\x5b-\x5d\x80-\xff]|\x5c[\x00-\x7f])*\x5d))*$
+  ///

 module.exports = ->
   router = express.Router()
@@ -69,9 +77,12 @@
       .catch (error) -> response.status(500).json(error: error)

   router.post '/leagues/:id/admins', (request, response) ->
-    models.Admin.create(request.body)
-      .then (admin) -> response.status(201).json(admin)
-      .catch (error) -> response.status(500).json(error: error)
+    if EMAIL_REGEX.test(request.body.UserEmail)
+      models.Admin.create(request.body)
+        .then (admin) -> response.status(201).json(admin)
+        .catch (error) -> response.status(500).json(error: error)
+    else
+      response.status(400).json(error: "Invalid email")

   router.delete '/leagues/:id/admins/:adminId', (request, response) ->
     request.league.getAdmins().then (admins) ->


ext/KineticPanel: Fix issue with layout.

--- Oghma/Ext/KineticPanel.coffee
+++ Oghma/Ext/KineticPanel.coffee
@@ -31,12 +31,14 @@
   initStage: ( width, height ) ->
     @stage = new Kinetic.Stage(
       container: @body
-      width:     width
-      height:    height
     )
+    @stage.setSize( width, height )

   # See ExtJS.
   initComponent: ->
+    Ext.apply( this,
+      layout: 'fit'
+    )
     @on( 'boxready', ( it, width, height ) =>
       @initStage( width, height )


Config cleanup

Config cleanup

--- VersionOne.Requestor.NET.Backbone/config.coffee
+++ VersionOne.Requestor.NET.Backbone/config.coffee
@@ -11,7 +11,7 @@

   host = "https://www12.v1host.com"
   service = "https://www12.v1host.com/ProofpointAPITest/rest-1.v1/Data/"
-  versionOneAuth = "bstussy:Proofpoint!"
+  versionOneAuth = "userName:password"
   serviceGateway = false

   #var serviceGateway = 'http://localhost/v1requestor/Setup';


passport config in local with mongo

passport config in local with mongo

--- app/server.coffee
+++ app/server.coffee
@@ -7,6 +7,8 @@
 # LocalStrategy = require('passport-local').Strategy
 mongoose = require 'mongoose'
 stylus = require 'stylus'
+passport = require 'passport'
+Account = require('./models/account').Account

 app = express()

@@ -32,11 +34,10 @@
 app.configure "production", ->
   app.use express.errorHandler()

-# passport config
-# Account = require("../models/account")
-# passport.use new LocalStrategy(Account.authenticate())
-# passport.serializeUser Account.serializeUser()
-# passport.deserializeUser Account.deserializeUser()
+passport.use Account.createStrategy()
+
+passport.serializeUser Account.serializeUser()
+passport.deserializeUser Account.deserializeUser()

 # mongoose
 # mongoose.connect "mongodb://localhost/passport_local_mongoose"


Fixed usage if no translations has been loaded till now.

--- app/assets/javascripts/app/lib/app_post/i18n.js.coffee
+++ app/assets/javascripts/app/lib/app_post/i18n.js.coffee
@@ -31,6 +31,7 @@

   constructor: ->
     @map = {}
+    @timestampFormat = 'yyyy-mm-dd HH:MM'

     # observe if text has been translated
     $('body')
@@ -80,7 +81,6 @@

   set: ( locale ) ->
     @locale = locale
-    @timestampFormat = 'yyyy-mm-dd HH:MM'
     @map = {}
     App.Com.ajax(
       id:    'i18n-set-' + locale,


reverted some stuff

reverted some stuff

--- app/assets/javascripts/backbone/models/track.js.coffee
+++ app/assets/javascripts/backbone/models/track.js.coffee
@@ -15,10 +15,7 @@
   model: Molamp.Models.Track

   initialize: ->
-    ### Hack to prevent removing the selection on a track if the user clicks it twice.
-    Player.play already handles removing the selection on all tracks.
-    ###
-    #@on 'change:isSelected', @onSelectedChanged
+    @on 'change:isSelected', @onSelectedChanged

   onSelectedChanged: ->
     @each (model) ->


Dropup triggers OPENED/CLOSED classes elements closes #367

--- wzk/ui/dropup/Dropup.coffee
+++ wzk/ui/dropup/Dropup.coffee
@@ -7,18 +7,30 @@
   ###
   @CSS:
     BUTTON_TAG: 'li'
+
+  ###
+    @enum {string}
+  ###
+  @CLS:
     BUTTON_CLASS: 'dropup-button'
-    TARGET_ATTRIBUTE: 'target'
     CARET: 'caret-marker' # do not set to 'caret', as it is would add another arrow
     ARROW_UP: 'fa-angle-up'
     ARROW_LEFT: 'fa-angle-left'
     COLLAPSE: 'collapse'
+    OPENED: 'opened'
+    CLOSED: 'closed'

   ###
     @enum {number}
   ###
   @ANIMATION:
     DEFAULT_DURATION: 500
+
+  ###
+    @enum {string}
+  ###
+  @DATA:
+    TARGET: 'target'

   ###*
     @param {wzk.dom.Dom} dom
@@ -31,30 +43,30 @@
   ###
   decorate: (@dropupButton) ->
     # arrow span, allowing change of icon according to state
-    @caretElement = @dropupButton.querySelector ".#{wzk.ui.dropup.Dropup.CSS.CARET}"
+    @caretElement = @dom.cls(wzk.ui.dropup.Dropup.CLS.CARET, @dropupButton)

     # id of tag to dropup
-    dropupSelector = goog.dom.dataset.get @dropupButton, wzk.ui.dropup.Dropup.CSS.TARGET_ATTRIBUTE
+    dropupSelector = goog.dom.dataset.get(@dropupButton, wzk.ui.dropup.Dropup.DATA.TARGET)
     unless dropupSelector?
-      throw new Error "Dropup button element has no '#{wzk.ui.dropup.Dropup.CSS.TARGET_ATTRIBUTE}' attribute!"
+      throw new Error "Dropup button element has no '#{wzk.ui.dropup.Dropup.DATA.TARGET}' attribute!"

     # find element with id or class
     @dropupElement = @dom.one "##{dropupSelector}, .#{dropupSelector}", @dom.getDocument()
     unless @dropupElement?
-      console.warn "Dropup element with specified id or class  '#{dropupSelector}', does not exist!"
+      console.warn("Dropup element with specified id or class  '#{dropupSelector}', does not exist!")
       return

     # hide dropupElement right after registering
-    goog.style.setElementShown @dropupElement, false
+    goog.style.setElementShown(@dropupElement, false)

     # set overflow property
-    @dropupElement.style.overflow = 'hidden'
+    goog.style.setStyle(@dropupElement, 'overflow', 'hidden')

     # when animation is running, lock is set to true
     @animationLock = false

     # register handler for button
-    goog.events.listen @dropupButton, goog.events.EventType.CLICK, @toggle
+    goog.events.listen(@dropupButton, goog.events.EventType.CLICK, @toggle)

   ###*
     @protected
@@ -62,7 +74,7 @@
   ###
   toggle: (event) =>
     event.preventDefault()
-    if (goog.style.isElementShown @dropupElement) and (not @animationLock)
+    if goog.style.isElementShown(@dropupElement) and not @animationLock
       @close()
     else
       @open()
@@ -72,11 +84,16 @@
   ###
   open: ->
     @height ?= @getHeight()
-    goog.style.setElementShown @dropupElement, true
+    goog.style.setElementShown(@dropupElement, true)
     animation = @getAnimation(0, @height)
+    CLS = wzk.ui.dropup.Dropup.CLS
+
+    animation.listen goog.fx.Animation.EventType.BEGIN, (event) =>
+      for el in @getAllElements()
+        goog.dom.classes.addRemove(el, CLS.CLOSED, CLS.OPENED)

     animation.listen goog.fx.Animation.EventType.FINISH, (event) =>
-      goog.dom.classes.remove el, wzk.ui.dropup.Dropup.CSS.COLLAPSE for el in [@dropupElement, @dropupButton]
+      goog.dom.classes.remove(@dropupElement, CLS.COLLAPSE)
       @swapClasses()

     animation.play()
@@ -86,25 +103,36 @@
   ###
   close: ->
     animation = @getAnimation(@height, 0)
+    CLS = wzk.ui.dropup.Dropup.CLS

     animation.listen goog.fx.Animation.EventType.FINISH, (event) =>
-      goog.style.setElementShown @dropupElement, false
-      goog.dom.classes.add el, wzk.ui.dropup.Dropup.CSS.COLLAPSE for el in [@dropupElement, @dropupButton]
+      for el in @getAllElements()
+        goog.dom.classes.addRemove(el, CLS.OPENED, CLS.CLOSED)
+
+    animation.listen goog.fx.Animation.EventType.FINISH, (event) =>
+      goog.style.setElementShown(@dropupElement, false)
+      goog.dom.classes.add(@dropupElement, CLS.COLLAPSE)
       @swapClasses()

     animation.play()

   ###*
+    @return {Array.<Element>}
+  ###
+  getAllElements: =>
+    [@dropupElement, @dropupButton]
+
+  ###*
     @protected
   ###
   swapClasses: ->
-    C = wzk.ui.dropup.Dropup.CSS
+    C = wzk.ui.dropup.Dropup.CLS

     if @caretElement?
       if goog.dom.classes.has @caretElement, C.ARROW_LEFT
-        goog.dom.classes.addRemove @caretElement, C.ARROW_LEFT, C.ARROW_UP
+        goog.dom.classes.addRemove(@caretElement, C.ARROW_LEFT, C.ARROW_UP)
       else
-        goog.dom.classes.addRemove @caretElement, C.ARROW_UP, C.ARROW_LEFT
+        goog.dom.classes.addRemove(@caretElement, C.ARROW_UP, C.ARROW_LEFT)

   ###*
     Gets animation object and set's starting height and ending height
@@ -132,6 +160,7 @@
     animation

   ###*
+    @protected
     @return {number} actual computed height of element
   ###
   getHeight: ->


Fix infinite scroll logic

Fix infinite scroll logic

--- app/assets/javascripts/videos/infinitescroll.js.coffee
+++ app/assets/javascripts/videos/infinitescroll.js.coffee
@@ -1,6 +1,7 @@
 $ ->
-	# Don't run on non-video pages
-	return if $('.videos')
+  # Don't run on non-video pages
+  return if not $('.videos').length
+
   $(window).sausage({page: '.video'
   , content: (i, page) ->
    '' + (i + 1) + ". " + page.find('.date').html() + ''


reload state after 404 on current_user

reload state after 404 on current_user

--- src/app/app.js.coffee
+++ src/app/app.js.coffee
@@ -99,14 +99,25 @@

 .controller "AppCtrl",
-  ($scope, $location, currentUser, $stateParams) ->
+  ($scope, $location, currentUser, $state, $stateParams) ->

+    # handle 401
+    $scope.$on 'event:auth-loginRequired', (e, data)->
+      currentUser.login_prompt()
+
     # force login
     currentUser.get().then(
       (resp)->
         return resp
       ,(err)->
-        currentUser.login_prompt()
+        # handle 404 on get user
+        currentUser.login_prompt().then ()->
+          # reload
+          $state.transitionTo(
+            $state.current,
+            $stateParams,
+            { reload: true, inherit: true, notify: true }
+          )
     )

     # FIXME - this needs to handle urls that are already 'full'
@@ -114,8 +125,7 @@
       base = currentUser.server() || ""
       base + path

-    $scope.$on 'event:auth-loginRequired', (e, data)->
-      currentUser.login_prompt()
+

     $scope.$on "$stateChangeSuccess", (ev, tState, tParams, fState, fParams) ->


Fix cursor autoscrolling when position doesn't change

When assigning the cursor's position, if we decide we want to autoscroll,
we should do so even if the cursor's position doesn't change.

The previous approach relied on a comparison of the position being
assigned, but this is before it has been clipped. Now we rely on a
boolean return value from the marker position assignment methods, which
tells us if the position has changed. If not, we need to emit our own
event to force autoscrolling.

--- src/app/cursor.coffee
+++ src/app/cursor.coffee
@@ -39,28 +39,25 @@
     @trigger 'destroyed'

   setScreenPosition: (screenPosition, options={}) ->
-    @goalColumn = null
-    @clearSelection()
-    @needsAutoscroll = (options.autoscroll ? true) and @isLastCursor()
-    if @getScreenPosition().isEqual(screenPosition)
-      @trigger 'autoscrolled' if @needsAutoscroll
-    else
+    @changePosition options, =>
       @editSession.setMarkerHeadScreenPosition(@marker, screenPosition, options)

   getScreenPosition: ->
     @editSession.getMarkerHeadScreenPosition(@marker)

   setBufferPosition: (bufferPosition, options={}) ->
+    @changePosition options, =>
+      @editSession.setMarkerHeadBufferPosition(@marker, bufferPosition, options)
+
+  getBufferPosition: ->
+    @editSession.getMarkerHeadBufferPosition(@marker)
+
+  changePosition: (options, fn) ->
     @goalColumn = null
     @clearSelection()
     @needsAutoscroll = options.autoscroll ? @isLastCursor()
-    if @getBufferPosition().isEqual(bufferPosition)
+    unless fn()
       @trigger 'autoscrolled' if @needsAutoscroll
-    else
-      @editSession.setMarkerHeadBufferPosition(@marker, bufferPosition, options)
-
-  getBufferPosition: ->
-    @editSession.getMarkerHeadBufferPosition(@marker)

   setVisible: (visible) ->
     if @visible != visible