:lipstick:

:lipstick:

--- spec/app/native-spec.coffee
+++ spec/app/native-spec.coffee
@@ -1,7 +1,6 @@
 describe 'Native', ->
-
   describe '$native.getPlatform()', ->
     it 'returns a non-empty value', ->
       platform = $native.getPlatform()
       expect(platform).not.toBe ''
-      expect(platform.length).toBeGreaterThan(0)+      expect(platform.length).toBeGreaterThan(0)


check before

check before

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/netguru/reminders.git/'


famly generator

famly generator

error: pathspec 'famgen.coffee' did not match any file(s) known to git

--- famgen.coffee
+++ famgen.coffee
@@ -0,0 +1,44 @@
+fs = require 'fs'
+
+GENERATIONS = 3
+MAX_CHILDREN = 10
+MIN_CHILDREN = 0
+INITIAL_PEOPLE = 10
+STARTING_YEAR = 1900
+globalID = 0
+
+console.log "Generating family..."
+
+generatePeople = (total) ->
+  people = []
+  for index in [0..total]
+    person = generatePerson()
+    person.ID = globalID += 1
+    people.push(person)
+  return people
+
+generatePerson = () ->
+  person =
+    name: "Ben Garvey" + Math.random()
+    gender: 'M'
+    birthDate: '1900'
+    deathDate: '1950'
+
+generateMarriages = (singles) ->
+  complete = []
+  for person, index in singles
+    rand = getRandomInt(index + 1, singles.length - 1)
+    console.log rand
+    complete.push(singles[index])
+    complete.push(singles[rand])
+    singles = singles.slice(rand-1, rand)
+    singles = singles.slice(index-1, index)
+  console.log singles
+
+getRandomInt = (min, max) ->
+  return Math.ceil((Math.random() * (max - min)) + min)
+
+people = generatePeople(INITIAL_PEOPLE)
+generateMarriages(people)
+
+console.log "Complete!"


Client: NotificationController: check group slug for social event

--- client/app/lib/notificationcontroller.coffee
+++ client/app/lib/notificationcontroller.coffee
@@ -54,7 +54,6 @@
         # event: "ChannelUpdateHappened"

         # filter notifications according to group slug
-
         return  unless notification?.context is getGroup().slug

         @emit 'NotificationHasArrived', notification
@@ -70,6 +69,9 @@
         @emit event, contents  if event

       @notificationChannel.on 'social', (notification) =>
+        # filter notifications according to group slug
+        return  unless notification?.context is getGroup().slug
+
         { contents, context, event } = notification

         @emit event, contents  if event


Smooth scroll; handle chrome bug and refactor.

--- content_scripts/scroller.coffee
+++ content_scripts/scroller.coffee
@@ -36,40 +36,51 @@
     amount

 # Test whether element should be scrolled.
-isScrollable = (element, direction) ->
+isScrollAllowed = (element, direction) ->
   # Elements with `overflow: hidden` should not be scrolled.
-  overflow = window.getComputedStyle(element).getPropertyValue("overflow-#{direction}")
-  return false if overflow == "hidden"
-  return true
+  window.getComputedStyle(element).getPropertyValue("overflow-#{direction}") != "hidden"

-# Chrome does not report scrollHeight accurately for nodes with pseudo-elements of height 0 (bug 110149).
-# Therefore we cannot figure out if we have scrolled to the bottom of an element by testing if scrollTop +
-# clientHeight == scrollHeight. So just try to increase scrollTop blindly -- if it fails we know we have
-# reached the end of the content.
-ensureScrollChange = (direction, changeFn) ->
+# Test whether element actually scrolls in the direction required when asked to do so.
+# Due to chrome bug 110149, scrollHeight and clientHeight cannot be used to reliably determine whether an
+# element will scroll.  Instead, we scroll the element by 1 or -1 and see if it moved.
+isScrollPossible = (element, direction, amount, factor) ->
   axisName = scrollProperties[direction].axisName
-  element = activatedElement
-  progress = 0
-  loop
-    oldScrollValue = element[axisName]
-    changeFn(element, axisName) if isScrollable element, direction
-    progress += element[axisName] - oldScrollValue
-    break unless element[axisName] == oldScrollValue && element != document.body
-    # we may have an orphaned element. if so, just scroll the body element.
-    element = element.parentElement || document.body
+  # delta, here, is treated as a relative amount, which is correct for relative scrolls. For absolute scrolls
+  # (only gg, G, and friends), amount can be either 'max' or zero. In the former case, we're definitely
+  # scrolling forwards, so any positive value will do for delta.  In the latter case, we're definitely
+  # scrolling backwards, so a delta of -1 will do.
+  delta = factor * getDimension(element, direction, amount) || -1
+  delta = delta / Math.abs delta # 1 or -1
+  before = element[axisName]
+  element[axisName] += delta
+  after = element[axisName]
+  element[axisName] = before
+  before != after

-  # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
-  # position will not provide any more visual feedback to the user. therefore we deactivate it so that
-  # subsequent scrolls only move the parent element.
-  rect = activatedElement.getBoundingClientRect()
-  if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
-    activatedElement = element
+# Find the element we should and can scroll.
+findScrollableElement = (element, direction, amount, factor = 1) ->
+  axisName = scrollProperties[direction].axisName
+  while element != document.body and
+    not (isScrollPossible(element, direction, amount, factor) and isScrollAllowed(element, direction))
+      element = element.parentElement || document.body
+  element
+
+performScroll = (element, axisName, amount, checkVisibility = true) ->
+  before = element[axisName]
+  element[axisName] += amount
+
+  if checkVisibility
+    # if the activated element has been scrolled completely offscreen, subsequent changes in its scroll
+    # position will not provide any more visual feedback to the user. therefore we deactivate it so that
+    # subsequent scrolls only move the parent element.
+    rect = activatedElement.getBoundingClientRect()
+    if (rect.bottom < 0 || rect.top > window.innerHeight || rect.right < 0 || rect.left > window.innerWidth)
+      activatedElement = element
+
   # Return the amount by which the scroll position has changed.
-  return progress
+  element[axisName] - before

-# Scroll by a relative amount in some direction, possibly smoothly.
-# The constants below seem to roughly match chrome's scroll speeds for both short and long scrolls.
-# TODO(smblott) For very-long scrolls, chrome implements a soft landing; we don't.
+# Scroll by a relative amount (a number) in some direction, possibly smoothly.
 doScrollBy = do ->
   interval = 10 # Update interval (in ms).
   duration = 120 # This must be a multiple of interval (also in ms).
@@ -87,30 +98,27 @@
     # Ensure we have a multiple of interval.
     return interval * Math.round (extra / interval)

-  scroller = (direction,amount) ->
-    return ensureScrollChange direction, (element, axisName) -> element[axisName] += amount
-
-  (direction,amount,wantSmooth) ->
+  (element, direction, amount, wantSmooth) ->
+    axisName = scrollProperties[direction].axisName
     clearTimer()

     unless wantSmooth and settings.get "smoothScroll"
-      scroller direction, amount
-      return
+      return performScroll element, axisName, amount

     requiredTicks = (duration + calculateExtraDuration amount) / interval
-    # Round away from 0, so that we don't leave any requested scroll amount unscrolled.
-    rounder = (if 0 <= amount then Math.ceil else Math.floor)
-    delta = rounder(amount / requiredTicks)
+    # Round away from 0, so that we don't leave any scroll amount unscrolled.
+    delta = (if 0 <= amount then Math.ceil else Math.floor)(amount / requiredTicks)

-    ticks = 0
-    ticker = ->
-      # If we haven't scrolled by the expected amount, then we've hit the top, bottom or side of the activated
-      # element, so stop scrolling.
-      if scroller(direction, delta) != delta or ++ticks == requiredTicks
-        clearTimer()
+    if delta
+      ticks = 0
+      ticker = ->
+        if performScroll(element, axisName, delta, false) != delta or ++ticks == requiredTicks
+          # One final call of performScroll to check the visibility of the activated element.
+          performScroll(element, axisName, 0, true)
+          clearTimer()

-    timer = setInterval ticker, interval
-    ticker()
+      timer = setInterval ticker, interval
+      ticker()

 # scroll the active element in :direction by :amount * :factor.
 # :factor is needed because :amount can take on string values, which scrollBy converts to element dimensions.
@@ -126,28 +134,19 @@
   if (!activatedElement || !isRendered(activatedElement))
     activatedElement = document.body

-  elementAmount = getDimension activatedElement, direction, amount
-  elementAmount *= factor
+  element = findScrollableElement activatedElement, direction, amount, factor
+  elementAmount = factor * getDimension element, direction, amount
+  doScrollBy element, direction, elementAmount, true

-  doScrollBy direction, elementAmount, true
-
-root.scrollTo = (direction, pos, wantSmooth=false) ->
+root.scrollTo = (direction, pos, wantSmooth = false) ->
   return unless document.body

   if (!activatedElement || !isRendered(activatedElement))
     activatedElement = document.body

-  # Find the deepest scrollable element which would move if we scrolled it.  This is the element which
-  # ensureScrollChange will scroll.
-  # TODO(smblott) We're pretty much copying what ensureScrollChange does here.  Refactor.
-  element = activatedElement
-  axisName = scrollProperties[direction].axisName
-  while element != document.body and
-    (getDimension(element, direction, pos) == element[axisName] or not isScrollable element, direction)
-      element = element.parentElement || document.body
-
-  amount = getDimension(element,direction,pos) - element[axisName]
-  doScrollBy direction, amount, wantSmooth
+  element = findScrollableElement activatedElement, direction, pos
+  amount = getDimension(element,direction,pos) - element[scrollProperties[direction].axisName]
+  doScrollBy element, direction, amount, wantSmooth

 # TODO refactor and put this together with the code in getVisibleClientRect
 isRendered = (element) ->


createTexture -> createTexture2D

createTexture -> createTexture2D

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/rfw/basilisk.git/'


:lipstick:

:lipstick:

Keep the bind commands close together

--- src/app/config-panel.coffee
+++ src/app/config-panel.coffee
@@ -30,17 +30,6 @@
             value = @parseValue(type, value)
           config.set(name, value)

-  parseValue: (type, value) ->
-    switch type
-      when 'int'
-        intValue = parseInt(value)
-        value = intValue unless isNaN(intValue)
-      when 'float'
-        floatValue = parseFloat(value)
-        value = floatValue unless isNaN(floatValue)
-    value = undefined if value == ''
-    value
-
   bindEditors: ->
     for editor in @find('.editor[id]').views()
       do (editor) =>
@@ -55,3 +44,14 @@
         editor.getBuffer().one 'contents-modified', =>
           editor.getBuffer().on 'contents-modified', =>
             config.set(name, @parseValue(type, editor.getText()))
+
+  parseValue: (type, value) ->
+    switch type
+      when 'int'
+        intValue = parseInt(value)
+        value = intValue unless isNaN(intValue)
+      when 'float'
+        floatValue = parseFloat(value)
+        value = floatValue unless isNaN(floatValue)
+    value = undefined if value == ''
+    value


Always use ~/github/atom as the resource path when running specs

--- src/atom-application.coffee
+++ src/atom-application.coffee
@@ -261,7 +261,7 @@
   runSpecs: (exitWhenDone) ->
     specWindow = new AtomWindow
       bootstrapScript: 'spec-bootstrap'
-      resourcePath: @resourcePath
+      resourcePath: path.join(@getHomeDir(), 'github', 'atom')
       exitWhenDone: exitWhenDone
       isSpec: true


Temporary display. Displays team information for group management.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/picoCTF/picoCTF-web.git/'


Only try selector based listeners if target has .webkitMatchesSelector

--- src/command-registry.coffee
+++ src/command-registry.coffee
@@ -210,7 +210,7 @@

     loop
       listeners = @inlineListenersByCommandName[originalEvent.type]?.get(currentTarget) ? []
-      unless currentTarget is window or currentTarget is document
+      if currentTarget.webkitMatchesSelector?
         selectorBasedListeners =
           (@selectorBasedListenersByCommandName[originalEvent.type] ? [])
             .filter (listener) -> currentTarget.webkitMatchesSelector(listener.selector)


allow custom apply filters passed

allow custom apply filters passed

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/companytools/ember-cli-dunlop.git/'


Don't add bufferChange to DisplayBuffer change events

--- src/app/display-buffer.coffee
+++ src/app/display-buffer.coffee
@@ -223,11 +223,9 @@
       @handleBufferChange(bufferChange)
       bufferDelta = bufferChange.newRange.end.row - bufferChange.oldRange.end.row

-
     tokenizedBufferStart = @bufferRowForScreenRow(@screenRowForBufferRow(tokenizedBufferChange.start))
     tokenizedBufferEnd = tokenizedBufferChange.end
     tokenizedBufferDelta = tokenizedBufferChange.delta
-

     start = @screenRowForBufferRow(tokenizedBufferStart)
     end = @lastScreenRowForBufferRow(tokenizedBufferEnd)
@@ -235,7 +233,7 @@
     @lineMap.replaceScreenRows(start, end, newScreenLines)
     screenDelta = @lastScreenRowForBufferRow(tokenizedBufferEnd + tokenizedBufferDelta) - end

-    @triggerChanged({ start, end, screenDelta, bufferDelta, bufferChange })
+    @triggerChanged({ start, end, screenDelta, bufferDelta })

   buildLineForBufferRow: (bufferRow) ->
     @buildLinesForBufferRows(bufferRow, bufferRow)


Fix file encoding

Fix file encoding

--- packages/rocketchat-irc/irc.server.coffee
+++ packages/rocketchat-irc/irc.server.coffee
@@ -1,389 +1,389 @@
-net = Npm.require('net')
-Lru = Npm.require('lru-cache')
-
-MESSAGE_CACHE_SIZE = 200
-IRC_PORT = 6667
-IRC_HOST = 'irc.freenode.net'
-
-ircClientMap = {}
-ircReceiveMessageCache = Lru MESSAGE_CACHE_SIZE
-ircSendMessageCache = Lru MESSAGE_CACHE_SIZE
-
-bind = (f) ->
-	g = Meteor.bindEnvironment (self, args...) -> f.apply(self, args)
-	(args...) -> g @, args...
-
-async = (f, args...) ->
-	Meteor.wrapAsync(f)(args...)
-
-class IrcClient
-	constructor: (@loginReq) ->
-		@user = @loginReq.user
-		@user.username = @user.name
-		ircClientMap[@user._id] = this
-		@ircPort = IRC_PORT
-		@ircHost = IRC_HOST
-		@msgBuf = []
-
-		@isConnected = false
-		@isDistroyed = false
-		@socket = new net.Socket
-		@socket.setNoDelay
-		@socket.setEncoding 'utf-8'
-		@socket.setKeepAlive true
-		@onConnect = bind @onConnect
-		@onClose = bind @onClose
-		@onTimeout = bind @onTimeout
-		@onError = bind @onError
-		@onReceiveRawMessage = bind @onReceiveRawMessage
-		@socket.on 'data', @onReceiveRawMessage
-		@socket.on 'close', @onClose
-		@socket.on 'timeout', @onTimeout
-		@socket.on 'error', @onError
-
-		@isJoiningRoom = false
-		@receiveMemberListBuf = {}
-		@pendingJoinRoomBuf = []
-
-		@successLoginMessageRegex = /Welcome to the freenode Internet Relay Chat Network/
-		@failedLoginMessageRegex = /You have not registered/
-		@receiveMessageRegex = /^:(\S+)!~\S+ PRIVMSG (\S+) :(.+)$/
-		@receiveMemberListRegex = /^:\S+ \d+ \S+ = #(\S+) :(.*)$/
-		@endMemberListRegex = /^.+#(\S+) :End of \/NAMES list.$/
-		@addMemberToRoomRegex = /^:(\S+)!~\S+ JOIN #(\S+)$/
-		@removeMemberFromRoomRegex = /^:(\S+)!~\S+ PART #(\S+)$/
-		@quiteMemberRegex = /^:(\S+)!~\S+ QUIT .*$/
-
-	connect: (@loginCb) =>
-		@socket.connect @ircPort, @ircHost, @onConnect
-		@initRoomList()
-
-	disconnect: () ->
-		@isDistroyed = true
-		@socket.destroy()
-
-	onConnect: () =>
-		console.log '[irc] onConnect -> '.yellow, @user.username, 'connect success.'
-		@socket.write "NICK #{@user.username}\r\n"
-		@socket.write "USER #{@user.username} 0 * :Real Name\r\n"
-		# message order could not make sure here
-		@isConnected = true
-		@socket.write msg for msg in @msgBuf
-
-	onClose: (data) =>
-		console.log '[irc] onClose -> '.yellow, @user.username, 'connection close.'
-		@isConnected = false
-		if @isDistroyed
-			delete ircClientMap[@user._id]
-		else
-			@connect()
-
-	onTimeout: () =>
-		console.log '[irc] onTimeout -> '.yellow, @user.username, 'connection timeout.', arguments
-
-	onError: () =>
-		console.log '[irc] onError -> '.yellow, @user.username, 'connection error.', arguments
-
-	onReceiveRawMessage: (data) =>
-		data = data.toString().split('\n')
-		for line in data
-			line = line.trim()
-			console.log "[#{@ircHost}:#{@ircPort}]:", line
-			# Send heartbeat package to irc server
-			if line.indexOf('PING') == 0
-				@socket.write line.replace('PING :', 'PONG ')
-				continue
-
-			matchResult = @receiveMessageRegex.exec line
-			if matchResult
-				@onReceiveMessage matchResult[1], matchResult[2], matchResult[3]
-				continue
-
-			matchResult = @receiveMemberListRegex.exec line
-			if matchResult
-				@onReceiveMemberList matchResult[1], matchResult[2].split ' '
-				continue
-
-			matchResult = @endMemberListRegex.exec line
-			if matchResult
-				@onEndMemberList matchResult[1]
-				continue
-
-			matchResult = @addMemberToRoomRegex.exec line
-			if matchResult
-				@onAddMemberToRoom matchResult[1], matchResult[2]
-				continue
-
-			matchResult = @removeMemberFromRoomRegex.exec line
-			if matchResult
-				@onRemoveMemberFromRoom matchResult[1], matchResult[2]
-				continue
-
-			matchResult = @quiteMemberRegex.exec line
-			if matchResult
-				@onQuiteMember matchResult[1]
-				continue
-
-			matchResult = @successLoginMessageRegex.exec line
-			if matchResult
-				@onSuccessLoginMessage()
-				continue
-
-			matchResult = @failedLoginMessageRegex.exec line
-			if matchResult
-				@onFailedLoginMessage()
-				continue
-
-	onSuccessLoginMessage: () ->
-		console.log '[irc] onSuccessLoginMessage -> '.yellow
-		if @loginCb
-			@loginCb null, @loginReq
-
-	onFailedLoginMessage: () ->
-		console.log '[irc] onFailedLoginMessage -> '.yellow
-		@loginReq.allowed = false
-		@disconnect()
-		if @loginCb
-			@loginCb null, @loginReq
-
-	onReceiveMessage: (source, target, content) ->
-		now = new Date
-		timestamp = now.getTime()
-
-		cacheKey = [source, target, content].join ','
-		console.log '[irc] ircSendMessageCache.get -> '.yellow, 'key:', cacheKey, 'value:', ircSendMessageCache.get(cacheKey), 'ts:', (timestamp - 1000)
-		if ircSendMessageCache.get(cacheKey) > (timestamp - 1000)
-			return
-		else
-			ircSendMessageCache.set cacheKey, timestamp
-
-		console.log '[irc] onReceiveMessage -> '.yellow, 'source:', source, 'target:', target, 'content:', content
-		source = @createUserWhenNotExist source
-		if target[0] == '#'
-			room = RocketChat.models.Rooms.findOneByName target.substring(1)
-		else
-			room = @createDirectRoomWhenNotExist(source, @user)
-
-		message =
-			msg: content
-			ts: now
-		cacheKey = "#{source.username}#{timestamp}"
-		ircReceiveMessageCache.set cacheKey, true
-		console.log '[irc] ircReceiveMessageCache.set -> '.yellow, 'key:', cacheKey
-		RocketChat.sendMessage source, message, room
-
-	onReceiveMemberList: (roomName, members) ->
-		@receiveMemberListBuf[roomName] = @receiveMemberListBuf[roomName].concat members
-
-	onEndMemberList: (roomName) ->
-		newMembers = @receiveMemberListBuf[roomName]
-		console.log '[irc] onEndMemberList -> '.yellow, 'room:', roomName, 'members:', newMembers.join ','
-		room = RocketChat.models.Rooms.findOneByNameAndType roomName, 'c'
-		unless room
-			return
-
-		oldMembers = room.usernames
-		appendMembers = _.difference newMembers, oldMembers
-		removeMembers = _.difference oldMembers, newMembers
-
-		for member in appendMembers
-			@createUserWhenNotExist member
-
-		RocketChat.models.Rooms.removeUsernamesById room._id, removeMembers
-		RocketChat.models.Rooms.addUsernamesById room._id, appendMembers
-
-		@isJoiningRoom = false
-		roomName = @pendingJoinRoomBuf.shift()
-		if roomName
-			@joinRoom
-				t: 'c'
-				name: roomName
-
-	sendRawMessage: (msg) ->
-		console.log '[irc] sendRawMessage -> '.yellow, msg.slice(0, -2)
-		if @isConnected
-			@socket.write msg
-		else
-			@msgBuf.push msg
-
-	sendMessage: (room, message) ->
-		console.log '[irc] sendMessage -> '.yellow, 'userName:', message.u.username
-		target = ''
-		if room.t == 'c'
-			target = "##{room.name}"
-		else if room.t == 'd'
-			for name in room.usernames
-				if message.u.username != name
-					target = name
-					break
-
-		cacheKey = [@user.username, target, message.msg].join ','
-		console.log '[irc] ircSendMessageCache.set -> '.yellow, 'key:', cacheKey, 'ts:', message.ts.getTime()
-		ircSendMessageCache.set cacheKey, message.ts.getTime()
-		msg = "PRIVMSG #{target} :#{message.msg}\r\n"
-		@sendRawMessage msg
-
-	initRoomList: ->
-		roomsCursor = RocketChat.models.Rooms.findByTypeContainigUsername 'c', @user.username,
-			fields:
-				name: 1
-				t: 1
-
-		rooms = roomsCursor.fetch()
-		for room in rooms
-			@joinRoom(room)
-
-	joinRoom: (room) ->
-		if room.t isnt 'c' or room.name == 'general'
-			return
-
-		if @isJoiningRoom
-			@pendingJoinRoomBuf.push room.name
-		else
-			console.log '[irc] joinRoom -> '.yellow, 'roomName:', room.name, 'pendingJoinRoomBuf:', @pendingJoinRoomBuf.join ','
-			msg = "JOIN ##{room.name}\r\n"
-			@receiveMemberListBuf[room.name] = []
-			@sendRawMessage msg
-			@isJoiningRoom = true
-
-	leaveRoom: (room) ->
-		if room.t isnt 'c'
-			return
-		msg = "PART ##{room.name}\r\n"
-		@sendRawMessage msg
-
-	getMemberList: (room) ->
-		if room.t isnt 'c'
-			return
-		msg = "NAMES ##{room.name}\r\n"
-		@receiveMemberListBuf[room.name] = []
-		@sendRawMessage msg
-
-	onAddMemberToRoom: (member, roomName) ->
-		if @user.username == member
-			return
-
-		console.log '[irc] onAddMemberToRoom -> '.yellow, 'roomName:', roomName, 'member:', member
-		@createUserWhenNotExist member
-
-		RocketChat.models.Rooms.addUsernameByName roomName, member
-
-	onRemoveMemberFromRoom: (member, roomName)->
-		console.log '[irc] onRemoveMemberFromRoom -> '.yellow, 'roomName:', roomName, 'member:', member
-		RocketChat.models.Rooms.removeUsernameByName roomName, member
-
-	onQuiteMember: (member) ->
-		console.log '[irc] onQuiteMember ->'.yellow, 'username:', member
-		RocketChat.models.Rooms.removeUsernameFromAll member
-
-		Meteor.users.update {name: member},
-			$set:
-				status: 'offline'
-
-	createUserWhenNotExist: (name) ->
-		user = Meteor.users.findOne {name: name}
-		unless user
-			console.log '[irc] createNotExistUser ->'.yellow, 'userName:', name
-			Meteor.call 'registerUser',
-				email: "#{name}@rocketchat.org"
-				pass: 'rocketchat'
-				name: name
-			Meteor.users.update {name: name},
-				$set:
-					status: 'online'
-					username: name
-			user = Meteor.users.findOne {name: name}
-		return user
-
-
-	createDirectRoomWhenNotExist: (source, target) ->
-		console.log '[irc] createDirectRoomWhenNotExist -> '.yellow, 'source:', source, 'target:', target
-		rid = [source._id, target._id].sort().join('')
-		now = new Date()
-		RocketChat.models.Rooms.upsert
-			_id: rid
-		,
-			$set:
-				usernames: [source.username, target.username]
-			$setOnInsert:
-				t: 'd'
-				msgs: 0
-				ts: now
-
-		RocketChat.models.Subscriptions.upsert
-			rid: rid
-			$and: [{'u._id': target._id}]
-		,
-			$setOnInsert:
-				name: source.username
-				t: 'd'
-				open: false
-				alert: false
-				unread: 0
-				u:
-					_id: target._id
-					username: target.username
-		return {
-			t: 'd'
-			_id: rid
-		}
-
-IrcClient.getByUid = (uid) ->
-	return ircClientMap[uid]
-
-IrcClient.create = (login) ->
-	unless login.user?
-		return login
-	unless login.user._id of ircClientMap
-		ircClient = new IrcClient login
-		return async ircClient.connect
-
-	return login
-
-
-class IrcLoginer
-	constructor: (login) ->
-		console.log '[irc] validateLogin -> '.yellow, login
-		return IrcClient.create login
-
-
-class IrcSender
-	constructor: (message) ->
-		name = message.u.username
-		timestamp = message.ts.getTime()
-		cacheKey = "#{name}#{timestamp}"
-		if ircReceiveMessageCache.get cacheKey
-			return message
-
-		room = RocketChat.models.Rooms.findOneById message.rid, { fields: { name: 1, usernames: 1, t: 1 } }
-		ircClient = IrcClient.getByUid message.u._id
-		ircClient.sendMessage room, message
-		return message
-
-
-class IrcRoomJoiner
-	constructor: (user, room) ->
-		ircClient = IrcClient.getByUid user._id
-		ircClient.joinRoom room
-		return room
-
-
-class IrcRoomLeaver
-	constructor: (user, room) ->
-		ircClient = IrcClient.getByUid user._id
-		ircClient.leaveRoom room
-		return room
-
-
-class IrcLogoutCleanUper
-	constructor: (user) ->
-		ircClient = IrcClient.getByUid user._id
-		ircClient.disconnect()
-		return user
-
-RocketChat.callbacks.add 'beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW
-RocketChat.callbacks.add 'beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW
-RocketChat.callbacks.add 'beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW
-RocketChat.callbacks.add 'beforeCreateChannel', IrcRoomJoiner, RocketChat.callbacks.priority.LOW
-RocketChat.callbacks.add 'beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW
-RocketChat.callbacks.add 'afterLogoutCleanUp', IrcLogoutCleanUper, RocketChat.callbacks.priority.LOW
+net = Npm.require('net')
+Lru = Npm.require('lru-cache')
+
+MESSAGE_CACHE_SIZE = 200
+IRC_PORT = 6667
+IRC_HOST = 'irc.freenode.net'
+
+ircClientMap = {}
+ircReceiveMessageCache = Lru MESSAGE_CACHE_SIZE
+ircSendMessageCache = Lru MESSAGE_CACHE_SIZE
+
+bind = (f) ->
+	g = Meteor.bindEnvironment (self, args...) -> f.apply(self, args)
+	(args...) -> g @, args...
+
+async = (f, args...) ->
+	Meteor.wrapAsync(f)(args...)
+
+class IrcClient
+	constructor: (@loginReq) ->
+		@user = @loginReq.user
+		@user.username = @user.name
+		ircClientMap[@user._id] = this
+		@ircPort = IRC_PORT
+		@ircHost = IRC_HOST
+		@msgBuf = []
+
+		@isConnected = false
+		@isDistroyed = false
+		@socket = new net.Socket
+		@socket.setNoDelay
+		@socket.setEncoding 'utf-8'
+		@socket.setKeepAlive true
+		@onConnect = bind @onConnect
+		@onClose = bind @onClose
+		@onTimeout = bind @onTimeout
+		@onError = bind @onError
+		@onReceiveRawMessage = bind @onReceiveRawMessage
+		@socket.on 'data', @onReceiveRawMessage
+		@socket.on 'close', @onClose
+		@socket.on 'timeout', @onTimeout
+		@socket.on 'error', @onError
+
+		@isJoiningRoom = false
+		@receiveMemberListBuf = {}
+		@pendingJoinRoomBuf = []
+
+		@successLoginMessageRegex = /Welcome to the freenode Internet Relay Chat Network/
+		@failedLoginMessageRegex = /You have not registered/
+		@receiveMessageRegex = /^:(\S+)!~\S+ PRIVMSG (\S+) :(.+)$/
+		@receiveMemberListRegex = /^:\S+ \d+ \S+ = #(\S+) :(.*)$/
+		@endMemberListRegex = /^.+#(\S+) :End of \/NAMES list.$/
+		@addMemberToRoomRegex = /^:(\S+)!~\S+ JOIN #(\S+)$/
+		@removeMemberFromRoomRegex = /^:(\S+)!~\S+ PART #(\S+)$/
+		@quiteMemberRegex = /^:(\S+)!~\S+ QUIT .*$/
+
+	connect: (@loginCb) =>
+		@socket.connect @ircPort, @ircHost, @onConnect
+		@initRoomList()
+
+	disconnect: () ->
+		@isDistroyed = true
+		@socket.destroy()
+
+	onConnect: () =>
+		console.log '[irc] onConnect -> '.yellow, @user.username, 'connect success.'
+		@socket.write "NICK #{@user.username}\r\n"
+		@socket.write "USER #{@user.username} 0 * :Real Name\r\n"
+		# message order could not make sure here
+		@isConnected = true
+		@socket.write msg for msg in @msgBuf
+
+	onClose: (data) =>
+		console.log '[irc] onClose -> '.yellow, @user.username, 'connection close.'
+		@isConnected = false
+		if @isDistroyed
+			delete ircClientMap[@user._id]
+		else
+			@connect()
+
+	onTimeout: () =>
+		console.log '[irc] onTimeout -> '.yellow, @user.username, 'connection timeout.', arguments
+
+	onError: () =>
+		console.log '[irc] onError -> '.yellow, @user.username, 'connection error.', arguments
+
+	onReceiveRawMessage: (data) =>
+		data = data.toString().split('\n')
+		for line in data
+			line = line.trim()
+			console.log "[#{@ircHost}:#{@ircPort}]:", line
+			# Send heartbeat package to irc server
+			if line.indexOf('PING') == 0
+				@socket.write line.replace('PING :', 'PONG ')
+				continue
+
+			matchResult = @receiveMessageRegex.exec line
+			if matchResult
+				@onReceiveMessage matchResult[1], matchResult[2], matchResult[3]
+				continue
+
+			matchResult = @receiveMemberListRegex.exec line
+			if matchResult
+				@onReceiveMemberList matchResult[1], matchResult[2].split ' '
+				continue
+
+			matchResult = @endMemberListRegex.exec line
+			if matchResult
+				@onEndMemberList matchResult[1]
+				continue
+
+			matchResult = @addMemberToRoomRegex.exec line
+			if matchResult
+				@onAddMemberToRoom matchResult[1], matchResult[2]
+				continue
+
+			matchResult = @removeMemberFromRoomRegex.exec line
+			if matchResult
+				@onRemoveMemberFromRoom matchResult[1], matchResult[2]
+				continue
+
+			matchResult = @quiteMemberRegex.exec line
+			if matchResult
+				@onQuiteMember matchResult[1]
+				continue
+
+			matchResult = @successLoginMessageRegex.exec line
+			if matchResult
+				@onSuccessLoginMessage()
+				continue
+
+			matchResult = @failedLoginMessageRegex.exec line
+			if matchResult
+				@onFailedLoginMessage()
+				continue
+
+	onSuccessLoginMessage: () ->
+		console.log '[irc] onSuccessLoginMessage -> '.yellow
+		if @loginCb
+			@loginCb null, @loginReq
+
+	onFailedLoginMessage: () ->
+		console.log '[irc] onFailedLoginMessage -> '.yellow
+		@loginReq.allowed = false
+		@disconnect()
+		if @loginCb
+			@loginCb null, @loginReq
+
+	onReceiveMessage: (source, target, content) ->
+		now = new Date
+		timestamp = now.getTime()
+
+		cacheKey = [source, target, content].join ','
+		console.log '[irc] ircSendMessageCache.get -> '.yellow, 'key:', cacheKey, 'value:', ircSendMessageCache.get(cacheKey), 'ts:', (timestamp - 1000)
+		if ircSendMessageCache.get(cacheKey) > (timestamp - 1000)
+			return
+		else
+			ircSendMessageCache.set cacheKey, timestamp
+
+		console.log '[irc] onReceiveMessage -> '.yellow, 'source:', source, 'target:', target, 'content:', content
+		source = @createUserWhenNotExist source
+		if target[0] == '#'
+			room = RocketChat.models.Rooms.findOneByName target.substring(1)
+		else
+			room = @createDirectRoomWhenNotExist(source, @user)
+
+		message =
+			msg: content
+			ts: now
+		cacheKey = "#{source.username}#{timestamp}"
+		ircReceiveMessageCache.set cacheKey, true
+		console.log '[irc] ircReceiveMessageCache.set -> '.yellow, 'key:', cacheKey
+		RocketChat.sendMessage source, message, room
+
+	onReceiveMemberList: (roomName, members) ->
+		@receiveMemberListBuf[roomName] = @receiveMemberListBuf[roomName].concat members
+
+	onEndMemberList: (roomName) ->
+		newMembers = @receiveMemberListBuf[roomName]
+		console.log '[irc] onEndMemberList -> '.yellow, 'room:', roomName, 'members:', newMembers.join ','
+		room = RocketChat.models.Rooms.findOneByNameAndType roomName, 'c'
+		unless room
+			return
+
+		oldMembers = room.usernames
+		appendMembers = _.difference newMembers, oldMembers
+		removeMembers = _.difference oldMembers, newMembers
+
+		for member in appendMembers
+			@createUserWhenNotExist member
+
+		RocketChat.models.Rooms.removeUsernamesById room._id, removeMembers
+		RocketChat.models.Rooms.addUsernamesById room._id, appendMembers
+
+		@isJoiningRoom = false
+		roomName = @pendingJoinRoomBuf.shift()
+		if roomName
+			@joinRoom
+				t: 'c'
+				name: roomName
+
+	sendRawMessage: (msg) ->
+		console.log '[irc] sendRawMessage -> '.yellow, msg.slice(0, -2)
+		if @isConnected
+			@socket.write msg
+		else
+			@msgBuf.push msg
+
+	sendMessage: (room, message) ->
+		console.log '[irc] sendMessage -> '.yellow, 'userName:', message.u.username
+		target = ''
+		if room.t == 'c'
+			target = "##{room.name}"
+		else if room.t == 'd'
+			for name in room.usernames
+				if message.u.username != name
+					target = name
+					break
+
+		cacheKey = [@user.username, target, message.msg].join ','
+		console.log '[irc] ircSendMessageCache.set -> '.yellow, 'key:', cacheKey, 'ts:', message.ts.getTime()
+		ircSendMessageCache.set cacheKey, message.ts.getTime()
+		msg = "PRIVMSG #{target} :#{message.msg}\r\n"
+		@sendRawMessage msg
+
+	initRoomList: ->
+		roomsCursor = RocketChat.models.Rooms.findByTypeContainigUsername 'c', @user.username,
+			fields:
+				name: 1
+				t: 1
+
+		rooms = roomsCursor.fetch()
+		for room in rooms
+			@joinRoom(room)
+
+	joinRoom: (room) ->
+		if room.t isnt 'c' or room.name == 'general'
+			return
+
+		if @isJoiningRoom
+			@pendingJoinRoomBuf.push room.name
+		else
+			console.log '[irc] joinRoom -> '.yellow, 'roomName:', room.name, 'pendingJoinRoomBuf:', @pendingJoinRoomBuf.join ','
+			msg = "JOIN ##{room.name}\r\n"
+			@receiveMemberListBuf[room.name] = []
+			@sendRawMessage msg
+			@isJoiningRoom = true
+
+	leaveRoom: (room) ->
+		if room.t isnt 'c'
+			return
+		msg = "PART ##{room.name}\r\n"
+		@sendRawMessage msg
+
+	getMemberList: (room) ->
+		if room.t isnt 'c'
+			return
+		msg = "NAMES ##{room.name}\r\n"
+		@receiveMemberListBuf[room.name] = []
+		@sendRawMessage msg
+
+	onAddMemberToRoom: (member, roomName) ->
+		if @user.username == member
+			return
+
+		console.log '[irc] onAddMemberToRoom -> '.yellow, 'roomName:', roomName, 'member:', member
+		@createUserWhenNotExist member
+
+		RocketChat.models.Rooms.addUsernameByName roomName, member
+
+	onRemoveMemberFromRoom: (member, roomName)->
+		console.log '[irc] onRemoveMemberFromRoom -> '.yellow, 'roomName:', roomName, 'member:', member
+		RocketChat.models.Rooms.removeUsernameByName roomName, member
+
+	onQuiteMember: (member) ->
+		console.log '[irc] onQuiteMember ->'.yellow, 'username:', member
+		RocketChat.models.Rooms.removeUsernameFromAll member
+
+		Meteor.users.update {name: member},
+			$set:
+				status: 'offline'
+
+	createUserWhenNotExist: (name) ->
+		user = Meteor.users.findOne {name: name}
+		unless user
+			console.log '[irc] createNotExistUser ->'.yellow, 'userName:', name
+			Meteor.call 'registerUser',
+				email: "#{name}@rocketchat.org"
+				pass: 'rocketchat'
+				name: name
+			Meteor.users.update {name: name},
+				$set:
+					status: 'online'
+					username: name
+			user = Meteor.users.findOne {name: name}
+		return user
+
+
+	createDirectRoomWhenNotExist: (source, target) ->
+		console.log '[irc] createDirectRoomWhenNotExist -> '.yellow, 'source:', source, 'target:', target
+		rid = [source._id, target._id].sort().join('')
+		now = new Date()
+		RocketChat.models.Rooms.upsert
+			_id: rid
+		,
+			$set:
+				usernames: [source.username, target.username]
+			$setOnInsert:
+				t: 'd'
+				msgs: 0
+				ts: now
+
+		RocketChat.models.Subscriptions.upsert
+			rid: rid
+			$and: [{'u._id': target._id}]
+		,
+			$setOnInsert:
+				name: source.username
+				t: 'd'
+				open: false
+				alert: false
+				unread: 0
+				u:
+					_id: target._id
+					username: target.username
+		return {
+			t: 'd'
+			_id: rid
+		}
+
+IrcClient.getByUid = (uid) ->
+	return ircClientMap[uid]
+
+IrcClient.create = (login) ->
+	unless login.user?
+		return login
+	unless login.user._id of ircClientMap
+		ircClient = new IrcClient login
+		return async ircClient.connect
+
+	return login
+
+
+class IrcLoginer
+	constructor: (login) ->
+		console.log '[irc] validateLogin -> '.yellow, login
+		return IrcClient.create login
+
+
+class IrcSender
+	constructor: (message) ->
+		name = message.u.username
+		timestamp = message.ts.getTime()
+		cacheKey = "#{name}#{timestamp}"
+		if ircReceiveMessageCache.get cacheKey
+			return message
+
+		room = RocketChat.models.Rooms.findOneById message.rid, { fields: { name: 1, usernames: 1, t: 1 } }
+		ircClient = IrcClient.getByUid message.u._id
+		ircClient.sendMessage room, message
+		return message
+
+
+class IrcRoomJoiner
+	constructor: (user, room) ->
+		ircClient = IrcClient.getByUid user._id
+		ircClient.joinRoom room
+		return room
+
+
+class IrcRoomLeaver
+	constructor: (user, room) ->
+		ircClient = IrcClient.getByUid user._id
+		ircClient.leaveRoom room
+		return room
+
+
+class IrcLogoutCleanUper
+	constructor: (user) ->
+		ircClient = IrcClient.getByUid user._id
+		ircClient.disconnect()
+		return user
+
+RocketChat.callbacks.add 'beforeValidateLogin', IrcLoginer, RocketChat.callbacks.priority.LOW
+RocketChat.callbacks.add 'beforeSaveMessage', IrcSender, RocketChat.callbacks.priority.LOW
+RocketChat.callbacks.add 'beforeJoinRoom', IrcRoomJoiner, RocketChat.callbacks.priority.LOW
+RocketChat.callbacks.add 'beforeCreateChannel', IrcRoomJoiner, RocketChat.callbacks.priority.LOW
+RocketChat.callbacks.add 'beforeLeaveRoom', IrcRoomLeaver, RocketChat.callbacks.priority.LOW
+RocketChat.callbacks.add 'afterLogoutCleanUp', IrcLogoutCleanUper, RocketChat.callbacks.priority.LOW


Minor touch ups.

Minor touch ups.

--- content_scripts/link_hints.coffee
+++ content_scripts/link_hints.coffee
@@ -48,9 +48,6 @@
   constructor: (mode = OPEN_IN_CURRENT_TAB) ->
     # we need documentElement to be ready in order to append links
     return unless document.documentElement
-
-    if @isActive
-      return
     @isActive = true

     elements = @getVisibleClickableElements()
@@ -84,10 +81,9 @@
     @setOpenLinkMode mode

     # Note(philc): Append these markers as top level children instead of as child nodes to the link itself,
-    # because some clickable elements cannot contain children, e.g. submit buttons. This has the caveat
-    # that if you scroll the page and the link has position=fixed, the marker will not stay fixed.
-    @hintMarkerContainingDiv = DomUtils.addElementList(hintMarkers,
-      { id: "vimiumHintMarkerContainer", className: "vimiumReset" })
+    # because some clickable elements cannot contain children, e.g. submit buttons.
+    @hintMarkerContainingDiv = DomUtils.addElementList hintMarkers,
+      id: "vimiumHintMarkerContainer", className: "vimiumReset"

   setOpenLinkMode: (@mode) ->
     if @mode is OPEN_IN_NEW_BG_TAB or @mode is OPEN_IN_NEW_FG_TAB or @mode is OPEN_WITH_QUEUE
@@ -327,18 +323,16 @@
   updateVisibleMarkers: (hintMarkers, activateActiveHint = false, tabCount = 0) ->
     keyResult = @markerMatcher.getMatchingHints hintMarkers, tabCount
     linksMatched = keyResult.linksMatched
-    if (linksMatched.length == 0)
+    if linksMatched.length == 0
       @deactivateMode()
     else if activateActiveHint
       tabCount = ((linksMatched.length * Math.abs tabCount) + tabCount) % linksMatched.length
-      @activateLink(linksMatched[tabCount], 0)
-    else if (linksMatched.length == 1)
-      @activateLink(linksMatched[0], keyResult.delay ? 0)
+      @activateLink linksMatched[tabCount], 0
+    else if linksMatched.length == 1
+      @activateLink linksMatched[0], keyResult.delay ? 0
     else
-      for marker in hintMarkers
-        @hideMarker(marker)
-      for matched in linksMatched
-        @showMarker(matched, @markerMatcher.hintKeystrokeQueue.length)
+      @hideMarker marker for marker in hintMarkers
+      @showMarker matched, @markerMatcher.hintKeystrokeQueue.length for matched in linksMatched

   #
   # When only one link hint remains, this function activates it in the appropriate way.


Refresh scrollbars when no stylesheet is specified

--- src/editor-component.coffee
+++ src/editor-component.coffee
@@ -712,7 +712,7 @@
     return unless @performedInitialMeasurement
     return unless atom.themes.isInitialLoadComplete()

-    @refreshScrollbars() if @containsScrollbarSelector(stylesheet)
+    @refreshScrollbars() if not stylesheet or @containsScrollbarSelector(stylesheet)
     @sampleFontStyling()
     @sampleBackgroundColors()
     @remeasureCharacterWidths()


Update template path

Update template path

--- src/daemon/proxy/index.coffee
+++ src/daemon/proxy/index.coffee
@@ -24,13 +24,13 @@
       if app
         proxy.web req, res, target: "http://localhost:#{app.env.PORT}"
       else
-        res.end render 'app_not_found.html.eco'
+        res.end render 'html/app_not_found.html.eco'

     proxy.on 'error', (err, req, res) ->
       if err.code is 'ECONNREFUSED'
         name = getName req
         app  = apps.findByName name
-        res.end render 'econnrefused.html.eco', app: app
+        res.end render 'html/econnrefused.html.eco', app: app
       else
         res.end "Unknown error: #{err.code}"


I'm not entirely sure why I need this now.

It seems there is no editor unless I show the first editSession

--- spec/pane-spec.coffee
+++ spec/pane-spec.coffee
@@ -236,6 +236,7 @@

     describe "when the item is a model", ->
       it "removes the associated view only when all items that require it have been removed", ->
+        pane.showItem(editSession1)
         pane.showItem(editSession2)
         pane.removeItem(editSession2)
         expect(pane.itemViews.find('.editor')).toExist()


Updated grunt

Updated grunt

New Gruntfile.coffee (grunt v0.4.1 required)

error: pathspec 'Gruntfile.coffee' did not match any file(s) known to git

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -0,0 +1,33 @@
+module.exports = (grunt) ->
+  grunt.initConfig
+    pkg: grunt.file.readJSON "package/component.json"
+
+    meta:
+      file: 'quo'
+      banner: '/* <%= pkg.name %> v<%= pkg.version %> - <%= grunt.template.today("yyyy/m/d") %>\n' +
+              '   <%= pkg.homepage %>\n' +
+              '   Copyright (c) <%= grunt.template.today("yyyy") %> <%= pkg.author.name %>' +
+              ' - Licensed <%= _.pluck(pkg.license, "type").join(", ") %> */\n'
+
+    resources:
+      app: ['src/*.coffee']
+
+    coffee:
+      app: files: 'package/<%= meta.file %>.debug.js': ['<%= resources.app %>']
+
+    uglify:
+      options: compress: false, banner: "<%= meta.banner %>"
+      app: files: 'package/<%= meta.file %>.js': 'package/<%= meta.file %>.debug.js'
+
+    watch:
+      coffee:
+        files: ['<%= resources.app %>']
+        tasks: ["coffee"]
+
+  grunt.loadNpmTasks "grunt-contrib-coffee"
+  grunt.loadNpmTasks "grunt-contrib-uglify"
+  grunt.loadNpmTasks "grunt-contrib-watch"
+
+  # Default task.
+  grunt.registerTask 'default', ["coffee", "uglify"]
+


Added some comments to Gruntfile

Added some comments to Gruntfile

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,4 +1,6 @@
 module.exports = (grunt) ->
+
+  # load all grunt plugins defined in package.json
   require('load-grunt-tasks') grunt

   grunt.initConfig
@@ -173,6 +175,7 @@
         spawn: false
         livereload: true

+  # Start server in development mode
   grunt.registerTask "default", [
     "coffeelint"
     "clean"
@@ -180,6 +183,8 @@
     "connect:dev"
     "watch"
   ]
+
+  # Start server in preview mode
   grunt.registerTask "preview", [
     "clean:release"
     "clean:tmp"
@@ -188,6 +193,8 @@
     "clean:tmp"
     "connect:release"
   ]
+
+  # Build optimized files
   grunt.registerTask "build", [
     "clean:release"
     "clean:tmp"
@@ -195,6 +202,8 @@
     "concurrent:optimize"
     "clean:tmp"
   ]
+
+  # Deploy to GitHub Pages
   grunt.registerTask "shipit", [
     "clean:release"
     "clean:tmp"


Improve test coverage on InPorts collection

error: pathspec 'spec/Ports.coffee' did not match any file(s) known to git

--- spec/Ports.coffee
+++ spec/Ports.coffee
@@ -0,0 +1,45 @@
+if typeof process isnt 'undefined' and process.execPath and process.execPath.match /node|iojs/
+  chai = require 'chai' unless chai
+  noflo = require '../src/lib/NoFlo.coffee'
+else
+  noflo = require 'noflo'
+
+describe 'Ports collection', ->
+  describe 'InPorts', ->
+    describe 'without initial ports', ->
+      p = new noflo.InPorts
+      it 'should initially contain no ports', ->
+        chai.expect(p.ports).to.eql {}
+      it 'should allow adding a port', ->
+        p.add 'foo',
+          datatype: 'string'
+        chai.expect(p.ports['foo']).to.be.an 'object'
+        chai.expect(p.ports['foo'].getDataType()).to.equal 'string'
+      it 'should allow overriding  a port', ->
+        p.add 'foo',
+          datatype: 'boolean'
+        chai.expect(p.ports['foo']).to.be.an 'object'
+        chai.expect(p.ports['foo'].getDataType()).to.equal 'boolean'
+      it 'should throw if trying to add an \'add\' port', ->
+        chai.expect(-> p.add 'add').to.throw()
+      it 'should throw if trying to add an \'remove\' port', ->
+        chai.expect(-> p.add 'remove').to.throw()
+      it 'should throw if trying to add a port with invalid characters', ->
+        chai.expect(-> p.add 'hello world!').to.throw()
+      it 'should throw if trying to remove a port that doesn\'t exist', ->
+        chai.expect(-> p.remove 'bar').to.throw()
+      it 'should throw if trying to subscribe to a port that doesn\'t exist', ->
+        chai.expect(-> p.once 'bar', 'ip', ->).to.throw()
+        chai.expect(-> p.on 'bar', 'ip', ->).to.throw()
+      it 'should allow subscribing to an existing port', (done) ->
+        received = 0
+        p.once 'foo', 'ip', (packet) ->
+          received++
+          return done() if received is 2
+        p.on 'foo', 'ip', (packet) ->
+          received++
+          return done() if received is 2
+        p.foo.handleIP new noflo.IP 'data', null
+      it 'should be able to remove a port', ->
+        p.remove 'foo'
+        chai.expect(p.ports).to.eql {}


made an execAndPipe function and started on path to having local commands

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/genericdave/noprob.git/'


Reviewed code, almost ready for release

Reviewed code, almost ready for release

--- server/routes/mail.coffee
+++ server/routes/mail.coffee
@@ -9,9 +9,20 @@
 Level = require '../levels/Level'
 log = require 'winston'
 sendwithus = require '../sendwithus'
-if config.isProduction or config.redis.host isnt "localhost"
+if config.isProduction or config.redis.host isnt "localhost" #TODO: Ask Nick and Scott to change their environment variables and change the deploy ones
   lockManager = require '../commons/LockManager'
-
+#TODO: Ask Nick about email unsubscriptions
+createMailTask = (req, res) -> #TODO: Ask Nick whether he thinks it is a good idea or not to hardcode the mail tasks
+  unless req.user?.isAdmin() then return errors.forbidden(res)
+  unless req.body.url and req.body.frequency then return errors.badInput(res)
+  console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}"
+  newMailTask = new MailTask {}
+  newMailTask.set("url",req.body.url)
+  newMailTask.set("frequency",req.body.frequency)
+  newMailTask.save (err) ->
+    if err? then return errors.serverError(res, err)
+    res.send("Created mail task!")
+    res.end()

 module.exports.setup = (app) ->
   app.all config.mail.mailchimpWebhook, handleMailchimpWebHook
@@ -22,28 +33,52 @@

 setupScheduledEmails = ->
   testForLockManager()
-  mailTaskMap =
-    "test_mail_task": employerNewCandidatesAvailableTask
+  mailTaskMap = #TODO: Edit this to include additional emails
+    "test_mail_task": candidateUpdateProfileTask

-  MailTask.find({}).lean().exec (err, mailTasks) ->
+  MailTask.find({}).lean().exec (err, mailTasks) -> #TODO: Ask Nick whether or not to remove this
     if err? then throw "Failed to schedule mailTasks! #{err}"
     for mailTask in mailTasks
-      setInterval mailTaskMap[mailTask.url], mailTask.frequency*2
+      setInterval mailTaskMap[mailTask.url], mailTask.frequency*2 #TODO: Have some random offset to prevent lock contention

 testForLockManager = -> unless lockManager then throw "The system isn't configured to do distributed locking!"

 ### Candidate Update Reminder Task ###
-emailTimeRange = (timeRange, finalCallback) ->
+
+candidateUpdateProfileTask = ->
+  mailTaskName = "candidateUpdateProfileTask"
+  lockDurationMs = 20000 #TODO: Change these to something appropriate for the mail frequency (ideally longer than the task but shorter than frequency)
+  currentDate = new Date()
+  timeRanges = []
+  for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']]
+    timeRanges.push
+      start: generateWeekOffset currentDate, weekPair[0]
+      end: generateWeekOffset currentDate, weekPair[1]
+      name: weekPair[2]
+  lockManager.setLock mailTaskName, lockDurationMs, (err) ->
+    if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!"
+    async.each timeRanges, emailTimeRange.bind({mailTaskName: mailTaskName}), (err) ->
+      if err
+        log.error "There was an error sending the candidate profile update reminder emails: #{err}"
+      else
+        log.info "Completed mail task #{mailTaskName}"
+      lockManager.releaseLock mailTaskName, (err) ->
+        if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
+
+generateWeekOffset = (originalDate, numberOfWeeks) ->
+  return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString()
+
+emailTimeRange = (timeRange, emailTimeRangeCallback) ->
   waterfallContext =
     "timeRange": timeRange
     "mailTaskName": @mailTaskName
   async.waterfall [
     findAllCandidatesWithinTimeRange.bind(waterfallContext)
-    (unfilteredCandidates, cb) -> #now filter the candidates to see if they are eligible
-      async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), (filtered) -> cb null, filtered
-    (filteredCandidates, cb) -> #Now send emails to the eligible candidates and record.
+    (unfilteredCandidates, cb) ->
+      async.reject unfilteredCandidates, candidateFilter.bind(waterfallContext), cb.bind(null, null)
+    (filteredCandidates, cb) ->
       async.each filteredCandidates, sendReminderEmailToCandidate.bind(waterfallContext), cb
-  ], finalCallback
+  ], emailTimeRangeCallback

 findAllCandidatesWithinTimeRange = (cb) ->
   findParameters =
@@ -61,8 +96,11 @@
     "metadata.timeRangeName": @timeRange.name
     "metadata.updated": candidate.jobProfile.updated
   MailSent.find(findParameters).lean().exec (err, sentMail) ->
-    if err? then return errors.serverError("Error fetching sent mail in email task")
-    sentEmailFilterCallback Boolean(sentMail.length)
+    if err?
+      log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
+      sentEmailFilterCallback true
+    else
+      sentEmailFilterCallback Boolean(sentMail.length)

 findEmployersSignedUpAfterDate = (dateObject, cb) ->
   countParameters =
@@ -73,14 +111,18 @@

 sendReminderEmailToCandidate = (candidate, sendEmailCallback) ->
   findEmployersSignedUpAfterDate new Date(candidate.jobProfile.updated), (err, employersAfterCount) =>
+    if err?
+      log.error "There was an error finding employers who signed up after #{candidate.jobProfile.updated}: #{err}"
+      return sendEmailCallback err
     context =
       email_id: "tem_CtTLsKQufxrxoPMn7upKiL"
       recipient:
         address: candidate.email
         name: candidate.jobProfile.name
       email_data:
-        profile_updated: candidate.jobProfile.updated #format nicely
         new_company: employersAfterCount
+        company_name: "CodeCombat"
+        user_profile: "http://codecombat.com/account/profile/#{candidate._id}"
     log.info "Sending #{@timeRange.name} update reminder to #{context.recipient.name}(#{context.recipient.address})"
     newSentMail =
       mailTask: @mailTaskName
@@ -93,30 +135,22 @@
       sendwithus.api.send context, (err, result) ->
         log.error "Error sending candidate update reminder email: #{err} with result #{result}" if err
         sendEmailCallback null
-
-generateWeekOffset = (originalDate, numberOfWeeks) ->
-  return (new Date(originalDate.getTime() - numberOfWeeks * 7 * 24 * 60 * 60 * 1000)).toISOString()
-
-candidateUpdateProfileTask = ->
-  mailTaskName = "candidateUpdateProfileTask"
-  lockDurationMs = 6000
-  currentDate = new Date()
-  timeRanges = []
-  for weekPair in [[4, 2,'two weeks'], [8, 4, 'four weeks'], [8, 52, 'eight weeks']]
-    timeRanges.push
-      start: generateWeekOffset currentDate, weekPair[0]
-      end: generateWeekOffset currentDate, weekPair[1]
-      name: weekPair[2]
-  lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
-    if err? then return log.error "Error getting a task lock!"
-    async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) ->
-      if err then log.error JSON.stringify err else log.info "Sent candidate update reminders!"
-      lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
-
 ### End Candidate Update Reminder Task ###
 ### Internal Candidate Update Reminder Email ###
-
-emailInternalCandidateUpdateReminder = (cb) ->
+internalCandidateUpdateTask = ->
+  mailTaskName = "internalCandidateUpdateTask"
+  lockDurationMs = 6000 #TODO: Change lock duration
+  lockManager.setLock mailTaskName, lockDurationMs, (err) ->
+    if err? then return log.error "Error getting a distributed lock for task #{mailTaskName}!"
+    emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) ->
+      if err
+        log.error "There was an error sending the internal candidate update reminder.: #{err}"
+      else
+        log.info "Sent internal candidate update reminder email!"
+      lockManager.releaseLock mailTaskName, (err) ->
+        if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
+
+emailInternalCandidateUpdateReminder = (internalCandidateUpdateReminderCallback) ->
   currentTime = new Date()
   beginningOfUTCDay = new Date()
   beginningOfUTCDay.setUTCHours(0,0,0,0)
@@ -124,40 +158,43 @@
     "beginningOfUTCDay": beginningOfUTCDay
     "currentTime": currentTime
     "mailTaskName": @mailTaskName
-
   async.waterfall [
     findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
     (unfilteredCandidates, cb) ->
       async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null)
     (filteredCandidates, cb) ->
       async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb
-  ], cb
+  ], internalCandidateUpdateReminderCallback

 findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) ->
   findParameters =
     "jobProfile.updated":
       $lte: @currentTime.toISOString()
       gt: @beginningOfUTCDay.toISOString()
-    "jobProfileApproved": false
+    "jobProfileApproved": false
   User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb

 candidatesUpdatedTodayFilter = (candidate, cb) ->
   findParameters =
     "user": candidate._id
     "mailTask": @mailTaskName
-    "metadata.beginningOfUTCDay": @beginningOfUTCDay
+    "metadata.beginningOfUTCDay": @beginningOfUTCDay
   MailSent.find(findParameters).lean().exec (err, sentMail) ->
-    if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}")
-    cb Boolean(sentMail.length)
+    if err?
+      log.error "Error finding mail sent for task #{@mailTaskName} and user #{candidate._id}!"
+      cb true
+    else
+      cb Boolean(sentMail.length)

 sendInternalCandidateUpdateReminder = (candidate, cb) ->
   context =
     email_id: "tem_Ac7nhgKqatTHBCgDgjF5pE"
-    recipient:
+    recipient:
       address: "team@codecombat.com" #Change to whatever email address is necessary
       name: "The CodeCombat Team"
+    email_data:
+      new_candidate_profile: "https://codecombat.com/account/profile/#{candidate._id}"
   log.info "Sending candidate updated reminder for #{candidate.jobProfile.name}"
-
   newSentMail =
     mailTask: @mailTaskName
     user: candidate._id
@@ -169,20 +206,23 @@
     sendwithus.api.send context, (err, result) ->
       log.error "Error sending interal candidate update email: #{err} with result #{result}" if err
       cb null
-
-internalCandidateUpdateTask = ->
-  mailTaskName = "internalCandidateUpdateTask"
-  lockDurationMs = 6000
-  lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
-    if err? then return log.error "Error getting a task lock!"
-    emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) ->
-      if err? then log.error "There was an error sending the internal candidate update reminder."
-      lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
+
 ### End Internal Candidate Update Reminder Email ###
-
 ### Employer New Candidates Available Email ###
-
-emailEmployerNewCandidatesAvailableEmail = (cb) ->
+employerNewCandidatesAvailableTask = ->
+  mailTaskName = "employerNewCandidatesAvailableTask"
+  lockDurationMs = 6000 #TODO: Update this lock duration
+  lockManager.setLock mailTaskName, lockDurationMs, (err) ->
+    if err? then return log.error "There was an error getting a task lock!"
+    emailEmployerNewCandidatesAvailable.apply {"mailTaskName":mailTaskName}, (err) ->
+      if err
+        log.error "There was an error completing the new candidates available task: #{err}"
+      else
+        log.info "Completed the employer new candidates available task!"
+      lockManager.releaseLock mailTaskName, (err) ->
+        if err? then return log.error "There was an error releasing the distributed lock for task #{mailTaskName}: #{err}"
+
+emailEmployerNewCandidatesAvailable = (emailEmployerNewCandidatesAvailableCallback) ->
   currentTime = new Date()
   asyncContext =
     "currentTime": currentTime
@@ -192,22 +232,20 @@
     findAllEmployers
     makeEmployerNamesEasilyAccessible
     (allEmployers, cb) ->
-      console.log "Found #{allEmployers.length} employers to email about new candidates available"
       async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null)
     (employersToEmail, cb) ->
       async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb
-  ], cb
+  ], emailEmployerNewCandidatesAvailableCallback

 findAllEmployers = (cb) ->
   findParameters =
     "employerAt":
-      $exists: true
+      $exists: true
     permissions: "employer"
   selection = "_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName activity dateCreated"
   User.find(findParameters).select(selection).lean().exec cb

 makeEmployerNamesEasilyAccessible = (allEmployers, cb) ->
-  #Make names easily accessible
   for employer, index in allEmployers
     if employer.signedEmployerAgreement?.data?.firstName
       employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName
@@ -222,15 +260,16 @@
     "sent":
       $gt: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000)
   MailSent.find(findParameters).lean().exec (err, sentMail) ->
-    if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}")
-    cb Boolean(sentMail.length)
+    if err?
+      log.error "Error finding mail sent for task #{@mailTaskName} and employer #employer._id}!"
+      cb true
+    else
+      cb Boolean(sentMail.length)

 sendEmployerNewCandidatesAvailableEmail = (employer, cb) ->
-  lastLoginDate = employer.activity?.login?.last ? employer.dateCreated
+  lastLoginDate = employer.activity?.login?.last ? employer.dateCreated
   countParameters =
     "jobProfileApproved": true
-    "jobProfile":
-      $exists: true
     $or: [
         jobProfileApprovedDate:
           $gt: lastLoginDate.toISOString()
@@ -246,12 +285,12 @@
       email_id: "tem_CCcHKr95Nvu5bT7c7iHCtm"
       recipient:
         address: employer.email
-        name: employer.name
       email_data:
         new_candidates: numberOfCandidatesSinceLogin
         employer_company_name: employer.employerAt
         company_name: "CodeCombat"
-
+    if employer.name
+      context.recipient.name = employer.name
     log.info "Sending available candidates update reminder to #{context.recipient.name}(#{context.recipient.address})"
     newSentMail =
       mailTask: @mailTaskName
@@ -261,17 +300,6 @@
       sendwithus.api.send context, (err, result) ->
         log.error "Error sending employer candidates available email: #{err} with result #{result}" if err
         cb null
-
-employerNewCandidatesAvailableTask = ->
-  #initialize featuredDate to job profile updated
-  mailTaskName = "employerNewCandidatesAvailableTask"
-  lockDurationMs = 6000
-  lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
-    if err? then return log.error "There was an error getting a task lock!"
-    emailEmployerNewCandidatesAvailableEmail.apply {"mailTaskName":mailTaskName}, (err) ->
-      if err? then return log.error "There was an error performing the #{mailTaskName} email task."
-      lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
-

 ### End Employer New Candidates Available Email ###

@@ -313,17 +341,7 @@
     return false
   return true

-createMailTask = (req, res) ->
-  #unless req.user?.isAdmin() then return errors.forbidden(res)
-  unless req.body.url and req.body.frequency then return errors.badInput(res)
-  console.log "Creating mail task with url #{req.body.url} and frequency #{req.body.frequency}"
-  newMailTask = new MailTask {}
-  newMailTask.set("url",req.body.url)
-  newMailTask.set("frequency",req.body.frequency)
-  newMailTask.save (err) ->
-    if err? then return errors.serverError(res, err)
-    res.send("Created mail task!")
-    res.end()
+

 handleLadderUpdate = (req, res) ->
   log.info('Going to see about sending ladder update emails.')


Progress on employer new candidates available email

--- server/routes/mail.coffee
+++ server/routes/mail.coffee
@@ -23,7 +23,7 @@
 setupScheduledEmails = ->
   testForLockManager()
   mailTaskMap =
-    "test_mail_task": candidateUpdateProfileTask
+    "test_mail_task": employerNewCandidatesAvailableTask

   MailTask.find({}).lean().exec (err, mailTasks) ->
     if err? then throw "Failed to schedule mailTasks! #{err}"
@@ -50,6 +50,7 @@
     "jobProfile.updated":
       $gt: @timeRange.start
       $lte: @timeRange.end
+    "jobProfileApproved": true
   selection =  "_id email jobProfile.name jobProfile.updated"
   User.find(findParameters).select(selection).lean().exec cb

@@ -109,7 +110,7 @@
   lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
     if err? then return log.error "Error getting a task lock!"
     async.each timeRanges, emailTimeRange.bind(mailTaskName: mailTaskName), (err) ->
-      if err then return log.error JSON.stringify err else log.info "Sent candidate update reminders!"
+      if err then log.error JSON.stringify err else log.info "Sent candidate update reminders!"
       lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err

 ### End Candidate Update Reminder Task ###
@@ -125,18 +126,19 @@
     "mailTaskName": @mailTaskName

   async.waterfall [
-    findCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
+    findNonApprovedCandidatesWhoUpdatedJobProfileToday.bind(asyncContext)
     (unfilteredCandidates, cb) ->
       async.reject unfilteredCandidates, candidatesUpdatedTodayFilter.bind(asyncContext), cb.bind(null,null)
     (filteredCandidates, cb) ->
       async.each filteredCandidates, sendInternalCandidateUpdateReminder.bind(asyncContext), cb
   ], cb

-findCandidatesWhoUpdatedJobProfileToday = (cb) ->
+findNonApprovedCandidatesWhoUpdatedJobProfileToday = (cb) ->
   findParameters =
     "jobProfile.updated":
       $lte: @currentTime.toISOString()
       gt: @beginningOfUTCDay.toISOString()
+    "jobProfileApproved": false
   User.find(findParameters).select("_id jobProfile.name jobProfile.updated").lean().exec cb

 candidatesUpdatedTodayFilter = (candidate, cb) ->
@@ -174,17 +176,69 @@
   lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
     if err? then return log.error "Error getting a task lock!"
     emailInternalCandidateUpdateReminder.apply {"mailTaskName":mailTaskName}, (err) ->
-      if err? then return log.error "There was an error sending the internal candidate update reminder."
+      if err? then log.error "There was an error sending the internal candidate update reminder."
       lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
 ### End Internal Candidate Update Reminder Email ###

 ### Employer New Candidates Available Email ###
+
+emailEmployerNewCandidatesAvailableEmail = (cb) ->
+  currentTime = new Date()
+  asyncContext =
+    "currentTime": currentTime
+    "mailTaskName": @mailTaskName
+
+  async.waterfall [
+    findAllEmployers
+    makeEmployerNamesEasilyAccessible
+    (allEmployers, cb) ->
+      console.log "Found #{allEmployers.length} employers to email about new candidates available"
+      async.reject allEmployers, employersEmailedDigestMoreThanWeekAgoFilter.bind(asyncContext), cb.bind(null,null)
+    (employersToEmail, cb) ->
+      async.each employersToEmail, sendEmployerNewCandidatesAvailableEmail.bind(asyncContext), cb
+  ], cb
+
+findAllEmployers = (cb) ->
+  findParameters =
+    "employerAt":
+      $exists: true
+    permissions: "employer"
+  User.find(findParameters).select("_id email employerAt signedEmployerAgreement.data.firstName signedEmployerAgreement.data.lastName").lean().exec cb
+
+makeEmployerNamesEasilyAccessible = (allEmployers, cb) ->
+  #Make names easily accessible
+  for employer, index in allEmployers
+    if employer.signedEmployerAgreement?.data?.firstName
+      employer.name = employer.signedEmployerAgreement.data.firstName + " " + employer.signedEmployerAgreement.data.lastName
+      delete employer.signedEmployerAgreement
+    allEmployers[index] = employer
+  cb null, allEmployers
+
+employersEmailedDigestMoreThanWeekAgoFilter = (employer, cb) ->
+  findParameters =
+    "user": employer._id
+    "mailTask": @mailTaskName
+    "sent":
+      $lte: new Date(@currentTime.getTime() - 7 * 24 * 60 * 60 * 1000)
+  MailSent.find(findParameters).lean().exec (err, sentMail) ->
+    if err? then return errors.serverError("Error fetching sent mail in #{@mailTaskName}")
+    cb Boolean(sentMail.length)
+
+sendEmployerNewCandidatesAvailableEmail = (employer, cb) ->
+  cb null
+
+
 employerNewCandidatesAvailableTask = ->
   #  tem_CCcHKr95Nvu5bT7c7iHCtm
   #initialize featuredDate to job profile updated
   mailTaskName = "employerNewCandidatesAvailableTask"
   lockDurationMs = 6000
   lockManager.setLock mailTaskName, lockDurationMs, (err, lockResult) ->
+    if err? then return log.error "There was an error getting a task lock!"
+    emailEmployerNewCandidatesAvailableEmail.apply {"mailTaskName":mailTaskName}, (err) ->
+      if err? then return log.error "There was an error performing the #{mailTaskName} email task."
+      lockManager.releaseLock mailTaskName, (err, result) -> if err? then return log.error err
+

 ### End Employer New Candidates Available Email ###


pass in app data into layout

pass in app data into layout

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/SNASW/rendr.git/'


Varaible fix

Varaible fix

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/SwingDev/pdf-generator-service.git/'


Adding f-ing weather.

Adding f-ing weather.

error: pathspec 'src/scripts/f-ing-weather.coffee' did not match any file(s) known to git

--- src/scripts/f-ing-weather.coffee
+++ src/scripts/f-ing-weather.coffee
@@ -0,0 +1,31 @@
+# Description:
+#   Returns the weather from thefuckingweather.com
+#
+# Dependencies:
+#   None
+#
+# Configuration:
+#   None
+#
+# Commands:
+#   hubot what's the weather for <city> - Get the weather for a location
+#   hubot what's the weather for <zip> - Get the weather for a zipcode
+#
+# Author:
+#   aaronott
+
+weather = (msg, query, cb) ->
+  msg.http('http://thefuckingweather.com/')
+    .query(where: query)
+    .header('User-Agent', 'Mozilla/5.0')
+    .get() (err, res, body) ->
+      temp = body.match(/(\d+)/)?[1] || ""
+      remark = body.match(/
(.*)</)?[1] || "remark not found"
+      flavor = body.match(/
(.*)</)?[1] || "flavor not found"
+      cb(temp, remark, flavor)
+
+module.exports = (robot) ->
+  robot.respond /(what's|what is) the weather for (.*)/i, (msg) ->
+    weather msg, msg.match[2], (temp, remark, flavor) ->
+      out = temp + " degrees " + remark + " " + flavor
+      msg.send out


Fix velocity scrolling upward

Fix velocity scrolling upward

There’s no need to check that the mouseWheelScreenRow is on screen
before preserving the corresponding line.

--- src/text-editor-presenter.coffee
+++ src/text-editor-presenter.coffee
@@ -160,10 +160,9 @@
         @buildLineState(row, line)
       row++

-    if @mouseWheelScreenRow? and not startRow <= @mouseWheelScreenRow < endRow
+    if @mouseWheelScreenRow?
       preservedLine = @model.tokenizedLineForScreenRow(@mouseWheelScreenRow)
       visibleLineIds[preservedLine.id] = true
-      @updateLineState(@mouseWheelScreenRow, preservedLine)

     for id, line of @state.content.lines
       unless visibleLineIds.hasOwnProperty(id)
@@ -260,7 +259,7 @@
       @state.gutter.lineNumbers[id] = {screenRow, bufferRow, softWrapped, top, decorationClasses, foldable}
       visibleLineNumberIds[id] = true

-    if @mouseWheelScreenRow? and not startRow <= @mouseWheelScreenRow < endRow
+    if @mouseWheelScreenRow?
       screenRow = @mouseWheelScreenRow
       top = screenRow * @lineHeight
       bufferRow = @model.bufferRowForScreenRow(screenRow)


Fix assignment

Fix assignment

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/sstephenson/eve.git/'


Encapsulates the responsive tables plugin to a jquery plugin

error: pathspec 'coffee/responsiveTables.coffee' did not match any file(s) known to git

--- coffee/responsiveTables.coffee
+++ coffee/responsiveTables.coffee
@@ -0,0 +1,86 @@
+(($, window, document) ->
+  "use strict"
+
+  pluginName = "responsiveTables"
+
+  defaults =
+    widthToCollapse: 768
+
+  class ResponsiveTables
+    constructor: (@el, a, options) ->
+      #merges options
+      @options = $.extend {}, defaults, options
+      #jquery representation
+      @$el = $(@el)
+
+      @switched = false
+
+      @_initResponsiveTables()
+
+    _initResponsiveTables: =>
+      $(window).on 'load', @_updateTables
+      $(window).on 'redraw', =>
+        @switched = false
+        @_updateTables()
+      $(window).on 'resize', @_updateTables
+
+    _updateTables: =>
+      console.log @options.widthToCollapse
+      if $(window).width() <= @options.widthToCollapse and not @switched
+        @switched = true
+        @$el.each (i, element) =>
+          @_splitTable($(element))
+        true
+      else if @switched and $(window).width() > @options.widthToCollapse
+        @switched = false
+        @$el.each (i, element) =>
+          @_unsplitTable($(element))
+
+    _splitTable: (table) =>
+      table.wrap "
"
+
+      tableClone = table.clone()
+
+      tableClone.find("td:not(:first-child), th:not(:first-child)").css("display", "none")
+      tableClone.removeClass("responsive")
+
+      table.closest(".table-wrapper").append(tableClone)
+
+      tableClone.wrap("
")
+      table.wrap("
")
+
+      @_setCellHeights(table, tableClone)
+
+    _unsplitTable: (table) =>
+      table.closest(".table-wrapper").find(".pinned").remove()
+      table.unwrap()
+      table.unwrap()
+
+    _setCellHeights: (table, tableClone) =>
+      tableRows = table.find('tr')
+      tableRowsCopy = tableClone.find 'tr'
+      heights = []
+
+      tableRows.each (index) ->
+        self = $(this)
+        tableHeadersAndData = self.find 'th, td'
+
+        tableHeadersAndData.each ->
+          height = $(this).outerHeight(true)
+          heights[index] = heights[index] || 0
+          heights[index] = height if height > heights[index]
+
+      tableRowsCopy.each (index) ->
+        $(this).height heights[index]
+
+
+  $.fn[pluginName] = (a, options) ->
+    [_, args...] = arguments
+    @each ->
+      plugin = $.data @, "plugin_#{pluginName}"
+
+      unless plugin
+        $.data @, "plugin_#{pluginName}", new ResponsiveTables(@, a, options)
+      else if plugin[_]? and $.type(plugin[_]) == 'function'
+        plugin[_].apply plugin, args
+) $, window, document


Controls menu

Controls menu

error: pathspec 'imports/ui/menus/InstanceControls.cjsx' did not match any file(s) known to git

--- imports/ui/menus/InstanceControls.cjsx
+++ imports/ui/menus/InstanceControls.cjsx
@@ -0,0 +1,44 @@
+React         = require 'react'
+DetailPage    = require '../DetailPage.cjsx'
+
+{ Menu, Button, Icons, Layer, Form, Header, Heading, FormFields, Paragraph, Footer, Button } = require 'grommet'
+
+
+module.exports = React.createClass
+  displayName: 'InstanceControls'
+
+  getInitialState: ->
+    showStopInstanceDialog: false
+
+  onClick: (eventType) -> =>
+    @props.emit eventType, @props.instanceName
+
+  stopInstance: ->
+    @setState showStopInstanceDialog: false
+    @props.emit 'stop instance', @props.instanceName
+
+  showCloseLayer:  ->
+    @setState showStopInstanceDialog: true
+
+  hideCloseLayer: ->
+    @setState showStopInstanceDialog: false
+
+  render: ->
+
+      <Menu pad='medium'>
+        <Button onClick={@showCloseLayer} align='start' plain=true label='Stop' icon={<Icons.Base.Power />}></Button>
+        <Button onClick={@onClick 'show instance logs'} align='start' plain=true label='Logs' icon={<Icons.Base.Notes />}></Button>
+        <Button onClick={@onClick 'delete instance'} align='start' plain=true label='Remove' icon={<Icons.Base.Trash />}></Button>
+      </Menu>
+      <Layer onClose={@hideCloseLayer} align='right' closer={true} hidden={not @state.showStopInstanceDialog}>
+       <Form compact=true>
+        <Heading>Stop</Heading>
+        <FormFields>
+          <Paragraph>Are you sure you want to stop {@props.instanceName}?</Paragraph>
+        </FormFields>
+
+          <Button onClick={@stopInstance} secondary=true label='Yes, stop it' />
+
+       </Form>
+      </Layer>
+


Clone defaults in config constructor so they aren't mutated.

This fixes failures caused pollution of state between specs

--- src/app/config.coffee
+++ src/app/config.coffee
@@ -18,8 +18,8 @@

   constructor: ->
     @settings =
-      core: require('root-view').configDefaults
-      editor: require('editor').configDefaults
+      core: _.clone(require('root-view').configDefaults)
+      editor: _.clone(require('editor').configDefaults)

   load: ->
     @loadUserConfig()


Add missing public API comment

Add missing public API comment

--- lib/assets/javascripts/turbolinks.js.coffee
+++ lib/assets/javascripts/turbolinks.js.coffee
@@ -639,6 +639,7 @@

 # Public API
 #   Turbolinks.visit(url)
+#   Turbolinks.replace(html)
 #   Turbolinks.pagesCached()
 #   Turbolinks.pagesCached(20)
 #   Turbolinks.cacheCurrentPage()


avatar debug

avatar debug

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/GregDesplaces/classe-1914.git/'


Indentation fix.

Indentation fix.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/deamonsoft/salo.git/'


:memo: update

:memo: update

--- src/config.coffee
+++ src/config.coffee
@@ -322,8 +322,6 @@
   # * `keyPath` {String} name of the key to observe
   # * `callback` {Function} to call when the value of the key changes.
   #   * `value` the new value of the key
-  #   * `event` {Object}
-  #     * `previous` the prior value of the key.
   #
   # Returns a {Disposable} with the following keys on which you can call
   # `.dispose()` to unsubscribe.
@@ -340,13 +338,15 @@
     @emitter.on 'did-change', (event) =>
       callback(event.newValue) if keyPath? and keyPath.indexOf(event?.keyPath) is 0

-  # Essential: Add a listener for changes to a given key path.
-  #
-  # * `keyPath` {String} name of the key to observe
+  # Essential: Add a listener for changes to a given key path. If `keyPath` is
+  # not specified, your callback will be called on changes to any key.
+  #
+  # * `keyPath` (optional) {String} name of the key to observe
   # * `callback` {Function} to call when the value of the key changes.
-  #   * `value` the new value of the key
   #   * `event` {Object}
-  #     * `previous` the prior value of the key.
+  #     * `newValue` the new value of the key
+  #     * `oldValue` the prior value of the key.
+  #     * `keyPath` the keyPath of the changed key
   #
   # Returns a {Disposable} with the following keys on which you can call
   # `.dispose()` to unsubscribe.


bug fix

bug fix

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/bubblegroup/bubblebot.git/'


swig template can output context in JSON

 On branch master

	modified:   nodejs_server/middleware/swig_template.coffee

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/FireworksProject/platform.y.git/'


Reset version.

Reset version.

--- lib/shared/lib/spendflow.coffee
+++ lib/shared/lib/spendflow.coffee
@@ -1,4 +1,4 @@
-@SPENDFLOW_VERSION = '1.7.16'
+@SPENDFLOW_VERSION = '1.7.16.dev'

 @spendflowPrecision = 2


MailChimp webhook fix.

MailChimp webhook fix.

--- server/routes/mail.coffee
+++ server/routes/mail.coffee
@@ -55,11 +55,9 @@
   lname = post.data.merges.LNAME
   user.set('lastName', lname) if lname

-  mailChimpInfo = user.get 'mailChimp'
-  mailChimpInfo.email = post.data.email
-  mailChimpInfo.euid = post.data.id
-  user.set 'mailChimp', mailChimpInfo
-
+  user.set 'mailChimp.email', post.data.email
+  user.set 'mailChimp.euid', post.data.id
+
   badLog("Updating user object to: #{JSON.stringify(user.toObject(), null, '\t')}")

 handleUnsubscribe = (user) ->


Add comment shortcut setting

Add comment shortcut setting

error: pathspec 'settings/mathematica.cson' did not match any file(s) known to git

--- settings/mathematica.cson
+++ settings/mathematica.cson
@@ -0,0 +1,4 @@
+'.source.mathematica':
+  'editor':
+    'commentStart': '(*'
+    'commentEnd': '*)'


Allow commands.coffee to require deps from build

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/clochix/cozy-home.git/'


Refactor controller and extract pushstate control to pushstate.js.coffee

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/lonelyplanet/rizzo.git/'


Made the CocoCollection more strict about setting the model property.

--- app/collections/CocoCollection.coffee
+++ app/collections/CocoCollection.coffee
@@ -1,7 +1,12 @@
+CocoModel = require 'models/CocoModel'
+
 module.exports = class CocoCollection extends Backbone.Collection
   loaded: false
+  model: null

   initialize: ->
+    if not @model
+      console.error @constructor.name, 'does not have a model defined. This will not do!'
     super()
     @once 'sync', =>
       @loaded = true


Fix template test

Fix template test

--- components/article/test/view.coffee
+++ components/article/test/view.coffee
@@ -203,6 +203,6 @@
   describe '#renderCalloutSections', ->

     it 'renders callouts', ->
-      articles = [_.extend(fixtures.article, { thumbnail_title: 'callout article' })]
+      articles = [_.extend({}, fixtures.article, { thumbnail_title: 'callout article' })]
       Backbone.sync.args[0][2].success results: articles
       @view.$el.html().should.containEql 'callout article'


now allows to pass multiple arguments. test pass! why didn't I run into this before?

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/Industrial/stream-flow.git/'


add script for filtering the front page

add script for filtering the front page

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/tom-szczarkowski/hn-stuff.git/'


commit

commit

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/Romaboy/awesomeadmin.git/'


Proxy TextEditorView::find calls to the root inside the shadow DOM

--- src/text-editor-view.coffee
+++ src/text-editor-view.coffee
@@ -73,16 +73,15 @@
   setModel: (@model) ->
     @editor = @model

-    $root = $(@element.rootElement)
-
-    @scrollView = $root.find('.scroll-view')
-    @underlayer = $root.find('.highlights').addClass('underlayer')
-    @overlayer = $root.find('.lines').addClass('overlayer')
-    @hiddenInput = $root.find('.hidden-input')
-    @lines = $root.find('.lines')
+    @root = $(@element.rootElement)
+
+    @scrollView = @root.find('.scroll-view')
+    @underlayer = @root.find('.highlights').addClass('underlayer')
+    @overlayer = @root.find('.lines').addClass('overlayer')
+    @hiddenInput = @root.find('.hidden-input')

     @subscribe atom.config.observe 'editor.showLineNumbers', =>
-      @gutter = $root.find('.gutter')
+      @gutter = @root.find('.gutter')

       @gutter.removeClassFromAllLines = (klass) =>
         deprecate('Use decorations instead: http://blog.atom.io/2014/07/24/decorations.html')
@@ -97,6 +96,9 @@
         lines = @gutter.find("[data-buffer-row='#{bufferRow}']")
         lines.addClass(klass)
         lines.length > 0
+
+  find: ->
+    @root.find.apply(@root, arguments)

   # Public: Get the underlying editor model for this view.
   #


added a couple of TODO comments

added a couple of TODO comments

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/UCSFMemoryAndAging/tabcat.git/'


reorganize grunt

reorganize grunt

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -1,22 +1,13 @@
 module.exports = (grunt) ->
   grunt.initConfig
-    connect:
-      test:
-        options:
-          port: 3000
-          hostname: '0.0.0.0'
-      coverage:
-        options:
-          port: 4000
-          hostname: '0.0.0.0'
-          middleware: [
-            (req, res, next) ->
-              fs = require('fs')
-              try fs.mkdirSync('coverage')
-              try fs.mkdirSync("coverage/#{req.url[1..]}")
-              req.on 'data', (json) ->
-                fs.writeFileSync("coverage/#{req.url[1..]}/coverage.json", json.toString())
-          ]
+
+    # ----------
+    # Processing
+    # ----------
+    less:
+      default:
+        files: 'lib/codecov.css': 'src/less/*.less'
+        options: compress: yes, cleancss: yes

     coffeecov:
       options:
@@ -40,6 +31,10 @@
       all:
         files: ['src/**/*']
         tasks: 'build'
+
+    # --------
+    # Building
+    # --------

     clean: ['tmp', 'test/**/*.html']

@@ -90,10 +85,26 @@
       opera:
         command: 'cp dist/chrome.crx dist/opera.nex'

-    less:
-      default:
-        files: 'lib/codecov.css': 'src/less/*.less'
-        options: compress: yes, cleancss: yes
+    # -------
+    # Testing
+    # -------
+    connect:
+      test:
+        options:
+          port: 3000
+          hostname: '0.0.0.0'
+      coverage:
+        options:
+          port: 4000
+          hostname: '0.0.0.0'
+          middleware: [
+            (req, res, next) ->
+              fs = require('fs')
+              try fs.mkdirSync('coverage')
+              try fs.mkdirSync("coverage/#{req.url[1..]}")
+              req.on 'data', (json) ->
+                fs.writeFileSync("coverage/#{req.url[1..]}/coverage.json", json.toString())
+          ]

     curl:
       # Github
@@ -160,6 +171,9 @@
           reporter: 'mocha-phantom-coverage-reporter'
           timeout: 10000

+  # -------------
+  # Load Packages
+  # -------------
   grunt.loadNpmTasks 'grunt-curl'
   grunt.loadNpmTasks 'grunt-shell'
   grunt.loadNpmTasks 'grunt-mocha'
@@ -177,5 +191,3 @@
   grunt.registerTask 'build',    ['default', 'clean', 'copy', 'concat', 'shell']
   grunt.registerTask 'test',     ['curl', 'dom_munger', 'runTests']
   grunt.registerTask 'runTests', ['coffeecov', 'connect', 'mocha']
-
-  grunt.registerTask 'firefox', ['default', 'copy:firefox', 'concat:firefox', 'shell:firefox']


chore: update contributors commit msg

chore: update contributors commit msg

Since grunt-npm does not currently follow the `commit: false` flag

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -13,7 +13,9 @@

     bump: options: commitFiles: [ 'package.json', 'CHANGELOG.md' ]
     'auto-release': options: checkTravisBuild: false
-    'npm-contributors': options: commit: false
+    'npm-contributors': options:
+      commit: false
+      commitMessage: 'chore: update contributors'

   # Actually load this plugin's task. Mainly for testing
   grunt.loadTasks('tasks')


create gruntfile

create gruntfile

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/o2dly/grunt-json-sitemap.git/'


Create path to install directory

Create path to install directory

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -139,6 +139,7 @@

   grunt.registerTask 'install', 'Install the built application', ->
     rm INSTALL_DIR
+    mkdir path.dirname(INSTALL_DIR)
     cp path.join(BUILD_DIR, APP_NAME), INSTALL_DIR

   grunt.registerTask('compile', ['coffee', 'less', 'cson'])


added gruntfile for build tasks like stylechecks

error: pathspec 'Gruntfile.coffee' did not match any file(s) known to git

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -0,0 +1,28 @@
+module.exports = (grunt) ->
+
+  # Project configuration.
+  grunt.initConfig
+    pkg: grunt.file.readJSON "package.json"
+    coffeelint:
+      app: [
+        "*.coffee"
+        "devices/**/*.coffee"
+        "actions/**/*.coffee"
+      ]
+      options:
+        no_trailing_whitespace:
+          level: "ignore"
+        max_line_length:
+          value: 100
+        indentation:
+          value: 2
+          level: "error"
+        no_unnecessary_fat_arrows:
+          level: 'ignore'
+
+  grunt.loadNpmTasks "grunt-coffeelint"
+
+
+  # Default task(s).
+  grunt.registerTask "default", ["coffeelint"]
+  grunt.registerTask "test", ["coffeelint"]


Don't copy git-utils/oniguruma deps/ during build

These contain source files used to build the native modules
and are not needed in the binary distribution.

--- Gruntfile.coffee
+++ Gruntfile.coffee
@@ -149,7 +149,14 @@
       catch e
         directories.push(directory)

-    cp directory, path.join(APP_DIR, directory) for directory in directories
+    ignoredPaths = [
+      path.join('git-utils', 'deps')
+      path.join('oniguruma', 'deps')
+    ]
+    ignoredPaths = ignoredPaths.map (ignoredPath) -> "(#{ignoredPath})"
+    nodeModulesFilter = new RegExp(ignoredPaths.join('|'))
+    for directory in directories
+      cp directory, path.join(APP_DIR, directory), filter: nodeModulesFilter

     cp 'src', path.join(APP_DIR, 'src'), filter: /.+\.(cson|coffee|less)$/
     cp 'static', path.join(APP_DIR, 'static'), filter: /.+\.less$/


removed that ugly stub concept

removed that ugly stub concept

--- gem/vendor/assets/javascripts/angular-table.js.coffee
+++ gem/vendor/assets/javascripts/angular-table.js.coffee
@@ -132,14 +132,14 @@
     template: "

-          <li ng-class='{disabled: stub.currentPage <= 0}'>
-            «
+          <li ng-class='{disabled: currentPage <= 0}'>
+            «

-

+

             {{page + 1}}

-
= stub.numberOfPages - 1}'>
-            »
+
= numberOfPages - 1}'>
+            »

"
@@ -150,33 +150,30 @@
     }
     link: ($scope, $element, $attributes) ->

-      $scope.stub = {}
-      $scope.instance = $scope.stub
-      $scope.stub.list = $scope.list
-      $scope.stub.itemsPerPage = $scope.itemsPerPage
-      $scope.stub.currentPage = 0
+      $scope.instance = $scope
+      $scope.currentPage = 0

       $scope.update = () ->
-        $scope.stub.currentPage = 0
+        $scope.currentPage = 0
         if $scope.list
           if $scope.list.length > 0
-            $scope.stub.numberOfPages = Math.ceil($scope.list.length / $scope.stub.itemsPerPage)
-            $scope.pages = for x in [0..($scope.stub.numberOfPages - 1)]
+            $scope.numberOfPages = Math.ceil($scope.list.length / $scope.itemsPerPage)
+            $scope.pages = for x in [0..($scope.numberOfPages - 1)]
               x
           else
-            $scope.stub.numberOfPages = 0
+            $scope.numberOfPages = 0
             $scope.pages = []
-          $scope.stub.list = $scope.list
+          $scope.list = $scope.list

-      $scope.stub.fromPage = () ->
+      $scope.fromPage = () ->
         if $scope.list
-          $scope.stub.itemsPerPage * $scope.stub.currentPage - $scope.list.length
+          $scope.itemsPerPage * $scope.currentPage - $scope.list.length

-      $scope.stub.getFillerArray = () ->
-        if $scope.stub.currentPage == $scope.stub.numberOfPages - 1
-          itemCountOnLastPage = $scope.list.length % $scope.stub.itemsPerPage
+      $scope.getFillerArray = () ->
+        if $scope.currentPage == $scope.numberOfPages - 1
+          itemCountOnLastPage = $scope.list.length % $scope.itemsPerPage
           if itemCountOnLastPage != 0
-            fillerLength = $scope.stub.itemsPerPage - itemCountOnLastPage - 1
+            fillerLength = $scope.itemsPerPage - itemCountOnLastPage - 1
             x for x in [($scope.list.length)..($scope.list.length + fillerLength)]
           else
             []
@@ -184,9 +181,9 @@

       $scope.goToPage = (page) ->
         page = Math.max(0, page)
-        page = Math.min($scope.stub.numberOfPages - 1, page)
+        page = Math.min($scope.numberOfPages - 1, page)

-        $scope.stub.currentPage = page
+        $scope.currentPage = page

       $scope.update()


download image to local [deploy: production]

download image to local and server from local if possible. aim to
increase speed.

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/domjtalbot/Portfolio.git/'


autoselect webcollab room name on input focus

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/VoiSmart/Rocket.Chat.git/'


:angry::page_facing_up: Updated in browser at strd6.github.io/editor

error: pathspec 'pixie.cson' did not match any file(s) known to git


Do not show fields in i18nEditModelView when english is empty

Fixes #3528

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/VilkkuV/codecombat.git/'


Add assert for multi-segment prefix

Add assert for multi-segment prefix

--- spec/app/text-mate-scope-selector-spec.coffee
+++ spec/app/text-mate-scope-selector-spec.coffee
@@ -11,6 +11,7 @@
   it "matches prefixes", ->
     expect(new TextMateScopeSelector('a').matches(['a'])).toBeTruthy()
     expect(new TextMateScopeSelector('a').matches(['a.b'])).toBeTruthy()
+    expect(new TextMateScopeSelector('a.b').matches(['a.b.c'])).toBeTruthy()
     expect(new TextMateScopeSelector('a').matches(['abc'])).toBeFalsy()

   it "matches disjunction", ->


parse method was implemented

parse method was implemented

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/NightingaleStudio/exam.js.git/'


add BankWharf strategy

add BankWharf strategy

error: pathspec 'strategies/BankWharf.coffee' did not match any file(s) known to git

--- strategies/BankWharf.coffee
+++ strategies/BankWharf.coffee
@@ -0,0 +1,17 @@
+# Play Big Money including Banks, except buy Wharf with every $5 buy.
+{
+  name: 'BankWharf'
+  gainPriority: (state) -> [
+    "Colony" if state.current.countInDeck("Platinum") > 0
+    "Province" if state.countInSupply("Colony") <= 6
+    "Duchy" if 0 < state.gainsToEndGame() <= 5
+    "Estate" if 0 < state.gainsToEndGame() <= 2
+    "Platinum"
+    "Bank"
+    "Gold"
+    "Wharf"
+    "Silver"
+    "Copper" if state.gainsToEndGame() <= 3
+    null
+  ]
+}


provide an explicit error if the token provides an access denied

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/sharabash/hubot.git/'


changing tests according to the updated code

remote: Support for password authentication was removed on August 13, 2021.
remote: Please see https://docs.github.com/en/get-started/getting-started-with-git/about-remote-repositories#cloning-with-https-urls for information on currently recommended modes of authentication.
fatal: Authentication failed for 'https://github.com/virtdb/virtdb-googlesheet-provider.git/'