fix(mailto): Handle mailto on application launch, populate NamespaceStore synchronously

Summary:
atom-window `sendMessage` was not the same as `browserWindow.webContents.send`. WTF.

Save current namespace to config.cson so that it is never null when window opens

Don't re-create thread view on namespace change unless the namespace has changed

Tests for NamespaceStore state

Push worker immediately in workerForNamcespace to avoid creating two connections per namespace

Allow \n to be put into sreaming buffer, but only one

Clear streaming buffer when we're reconnecting to avoid processing same deltas twice (because of 400msec throttle)

Make `onProcessBuffer` more elegant—No functional changes

Test Plan: Run tests!

Reviewers: evan

Reviewed By: evan

Differential Revision: https://phab.nylas.com/D1551
This commit is contained in:
Ben Gotow 2015-05-21 18:08:29 -07:00
parent 7bd3f7ac78
commit b194d7fb37
15 changed files with 107 additions and 56 deletions

View file

@ -5,7 +5,7 @@
order: 2;
height: 100%;
overflow-y: auto;
overflow-x: hidden;
background-color: @source-list-bg;
section {
@ -26,6 +26,9 @@
font-weight: 400;
padding: 0 @spacing-standard;
line-height: @line-height-large * 1.1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
.unread {
font-weight: @font-weight-medium;

View file

@ -136,6 +136,7 @@
.collapsed-timestamp {
margin-left: 0.5em;
color: @text-color-very-subtle;
}
}

View file

@ -15,6 +15,7 @@
.item {
border-bottom:1px solid @border-color-divider;
.inner {
padding: @padding-large-vertical @padding-base-horizontal @padding-large-vertical @padding-base-horizontal;
margin-top:3px;
@ -43,6 +44,7 @@
// TODO: Necessary for Chromium 42 to render `activity-item-leave` animation
// properly. Removing position relative causes the div to remain visible
position:relative;
opacity: 1;
}
transition: height 0.4s;

View file

@ -35,6 +35,8 @@ ThreadListStore = Reflux.createStore
@listenTo FocusedTagStore, @_onTagChanged
@listenTo NamespaceStore, @_onNamespaceChanged
@createView()
_resetInstanceVars: ->
@_lastQuery = null
@_searchQuery = null
@ -74,7 +76,13 @@ ThreadListStore = Reflux.createStore
# Inbound Events
_onTagChanged: -> @createView()
_onNamespaceChanged: -> @createView()
_onNamespaceChanged: ->
namespaceId = NamespaceStore.current()?.id
namespaceMatcher = (m) ->
m.attribute() is Thread.attributes.namespaceId and m.value() is namespaceId
return if @view and _.find(@view.matchers, namespaceMatcher)
@createView()
_onSearchCommitted: (query) ->
return if @_searchQuery is query

View file

@ -0,0 +1,27 @@
_ = require 'underscore'
NamespaceStore = require '../../src/flux/stores/namespace-store'
describe "NamespaceStore", ->
beforeEach ->
@constructor = NamespaceStore.constructor
it "should initialize current() using data saved in config", ->
state =
"id": "123",
"email_address":"bengotow@gmail.com",
"object":"namespace"
spyOn(atom.config, 'get').andCallFake -> state
instance = new @constructor
expect(instance.current().id).toEqual(state['id'])
expect(instance.current().emailAddress).toEqual(state['email_address'])
it "should initialize current() to null if data is not present", ->
spyOn(atom.config, 'get').andCallFake -> null
instance = new @constructor
expect(instance.current()).toEqual(null)
it "should initialize current() to null if data is invalid", ->
spyOn(atom.config, 'get').andCallFake -> "this isn't an object"
instance = new @constructor
expect(instance.current()).toEqual(null)

View file

@ -265,23 +265,23 @@ describe "Window", ->
describe "when the opened path exists", ->
it "sets the project path to the opened path", ->
atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __filename
atom.getCurrentWindow().send 'open-path', pathToOpen: __filename
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path does not exist but its parent directory does", ->
it "sets the project path to the opened path's parent directory", ->
pathToOpen = path.join(__dirname, 'this-path-does-not-exist.txt')
atom.getCurrentWindow().send 'message', 'open-path', {pathToOpen}
atom.getCurrentWindow().send 'open-path', {pathToOpen}
expect(atom.project.getPaths()[0]).toBe __dirname
describe "when the opened path is a file", ->
it "opens it in the workspace", ->
atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __filename
atom.getCurrentWindow().send 'open-path', pathToOpen: __filename
expect(atom.workspace.open.mostRecentCall.args[0]).toBe __filename
describe "when the opened path is a directory", ->
it "does not open it in the workspace", ->
atom.getCurrentWindow().send 'message', 'open-path', pathToOpen: __dirname
atom.getCurrentWindow().send 'open-path', pathToOpen: __dirname
expect(atom.workspace.open.callCount).toBe 0

View file

@ -94,7 +94,7 @@ class Application
else
@windowManager.ensurePrimaryWindowOnscreen()
for urlToOpen in (urlsToOpen || [])
@openUrl({urlToOpen})
@openUrl(urlToOpen)
prepareDatabaseInterface: ->
return @dblitePromise if @dblitePromise
@ -268,7 +268,7 @@ class Application
event.preventDefault()
app.on 'open-url', (event, urlToOpen) =>
@openUrl({urlToOpen})
@openUrl(urlToOpen)
event.preventDefault()
ipc.on 'new-window', (event, options) =>
@ -355,21 +355,14 @@ class Application
else return false
true
# Open an atom:// or mailto:// url.
# Open a mailto:// url.
#
# options -
# :urlToOpen - The atom:// or mailto:// url to open.
# :devMode - Boolean to control the opened window's dev mode.
# :safeMode - Boolean to control the opened window's safe mode.
openUrl: ({urlToOpen, devMode, safeMode}) ->
parts = url.parse(urlToOpen)
# Attempt to parse the mailto link into Message object JSON
# and then open a composer window
if parts.protocol is 'mailto:'
openUrl: (urlToOpen) ->
{protocol} = url.parse(urlToOpen)
if protocol is 'mailto:'
@windowManager.sendToMainWindow('mailto', urlToOpen)
else
console.log "Opening unknown url: #{urlToOpen}"
console.log "Ignoring unknown URL type: #{urlToOpen}"
# Opens up a new {AtomWindow} to run specs within.
#

View file

@ -192,10 +192,10 @@ class AtomWindow
sendMessage: (message, detail) ->
if @loaded
@browserWindow.webContents.send 'message', message, detail
@browserWindow.webContents.send(message, detail)
else
@once 'window:loaded', =>
@browserWindow.webContents.send 'message', message, detail
@browserWindow.once 'window:loaded', =>
@browserWindow.webContents.send(message, detail)
sendCommand: (command, args...) ->
if @isSpecWindow()

View file

@ -38,6 +38,12 @@ class Matcher
@muid = Matcher.muid
Matcher.muid = (Matcher.muid + 1) % 50
@
attribute: ->
@attr
value: ->
@val
evaluate: (model) ->
value = model[@attr.modelKey]

View file

@ -49,7 +49,7 @@ class NylasAPI
_onNamespacesChanged: ->
return if atom.inSpecMode()
return unless atom.isMainWindow()
return if not atom.isMainWindow()
namespaces = NamespaceStore.items()
workers = _.map(namespaces, @workerForNamespace)
@ -66,8 +66,7 @@ class NylasAPI
@_workers
workerForNamespace: (namespace) =>
worker = _.find @_workers, (c) ->
c.namespaceId() is namespace.id
worker = _.find @_workers, (c) -> c.namespaceId() is namespace.id
return worker if worker
worker = new NylasSyncWorker(@, namespace.id)
@ -86,6 +85,7 @@ class NylasAPI
PriorityUICoordinator.settle.then =>
@_handleDeltas(deltas)
@_workers.push(worker)
worker.start()
worker

View file

@ -69,14 +69,19 @@ class NylasLongConnection
onProcessBuffer: =>
bufferJSONs = @_buffer.split('\n')
bufferCursor = null
return if bufferJSONs.length == 1
for i in [0..bufferJSONs.length - 2]
# We can't parse the last block - we don't know whether we've
# received the entire delta or only part of it. Wait
# until we have more.
@_buffer = bufferJSONs.pop()
for deltaJSON in bufferJSONs
continue if deltaJSON.length is 0
delta = null
try
delta = JSON.parse(bufferJSONs[i])
delta = JSON.parse(deltaJSON)
catch e
console.log("#{bufferJSONs[i]} could not be parsed as JSON.", e)
console.log("#{deltaJSON} could not be parsed as JSON.", e)
if delta
throw (new Error 'Received delta with no cursor!') unless delta.cursor
@_deltas.push(delta)
@ -84,15 +89,14 @@ class NylasLongConnection
bufferCursor = delta.cursor
# Note: setCursor is slow and saves to disk, so we do it once at the end
@setCursor(bufferCursor)
@_buffer = bufferJSONs[bufferJSONs.length - 1]
if bufferCursor
@setCursor(bufferCursor)
start: ->
return if not @_api.APIToken?
return if @_state is NylasLongConnection.State.Ended
return if @_req
console.log("Long Polling Connection: Starting....")
@withCursor (cursor) =>
return if @state is NylasLongConnection.State.Ended
console.log("Long Polling Connection: Starting for namespace #{@_namespaceId}, token #{@_api.APIToken}, with cursor #{cursor}")
@ -116,12 +120,13 @@ class NylasLongConnection
return
@_buffer = ''
res.setEncoding('utf8')
processBufferThrottled = _.throttle(@onProcessBuffer, 400, {leading: false})
res.setEncoding('utf8')
res.on 'close', => @retry()
res.on 'data', (chunk) =>
# Ignore characters sent as pings
return if chunk is '\n'
# Ignore redundant newlines sent as pings. Want to avoid
# calls to @onProcessBuffer that contain no actual updates
return if chunk is '\n' and (@_buffer.length is 0 or @_buffer[-1] is '\n')
@_buffer += chunk
processBufferThrottled()
@ -160,6 +165,7 @@ class NylasLongConnection
cleanup: ->
clearInterval(@_reqForceReconnectInterval) if @_reqForceReconnectInterval
@_reqForceReconnectInterval = null
@_buffer = ''
if @_req
@_req.end()
@_req.abort()

View file

@ -6,6 +6,8 @@ _ = require 'underscore'
{Listener, Publisher} = require '../modules/reflux-coffee'
CoffeeHelpers = require '../coffee-helpers'
saveStateKey = "nylas.current_namespace"
###
Public: The NamespaceStore listens to changes to the available namespaces in
the database and exposes the currently active Namespace via {::current}
@ -21,20 +23,26 @@ class NamespaceStore
constructor: ->
@_items = []
@_current = null
saveState = atom.config.get(saveStateKey)
if saveState and _.isObject(saveState)
@_current = (new Namespace).fromJSON(saveState)
@listenTo Actions.selectNamespaceId, @onSelectNamespaceId
@listenTo DatabaseStore, @onDataChanged
@populateItems()
populateItems: =>
DatabaseStore.findAll(Namespace).then (namespaces) =>
current = _.find namespaces, (n) -> n.id == @_current?.id
current = _.find namespaces, (n) -> n.id is @_current?.id
current = namespaces?[0] unless current
if current isnt @_current or not _.isEqual(namespaces, @_namespaces)
atom.config.set(saveStateKey, current)
@_current = current
@_namespaces = namespaces
@trigger(@)
.catch (err) =>
console.warn("Request for Namespaces failed. #{err}")

View file

@ -16,24 +16,20 @@ class WindowEventHandler
constructor: ->
@reloadRequested = false
@subscribe ipc, 'message', (message, detail) ->
switch message
when 'open-path'
pathToOpen = detail
@subscribe ipc, 'open-path', (pathToOpen) ->
unless atom.project?.getPaths().length
if fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen))
atom.project?.setPaths([pathToOpen])
unless atom.project?.getPaths().length
if fs.existsSync(pathToOpen) or fs.existsSync(path.dirname(pathToOpen))
atom.project?.setPaths([pathToOpen])
unless fs.isDirectorySync(pathToOpen)
atom.workspace?.open(pathToOpen, {})
unless fs.isDirectorySync(pathToOpen)
atom.workspace?.open(pathToOpen, {})
@subscribe ipc, 'update-available', (detail) ->
atom.updateAvailable(detail)
when 'update-available'
atom.updateAvailable(detail)
when 'send-feedback'
Actions = require './flux/actions'
Actions.sendFeedback()
@subscribe ipc, 'send-feedback', (detail) ->
Actions = require './flux/actions'
Actions.sendFeedback()
@subscribe ipc, 'command', (command, args...) ->
activeElement = document.activeElement
@ -135,10 +131,7 @@ class WindowEventHandler
location = target?.getAttribute('href') or currentTarget?.getAttribute('href')
if location?
schema = url.parse(location).protocol
# special handling for mailto
if schema? and schema in ['http:', 'https:', 'mailto:', 'tel:']
# should add test coverage to this, need to discuss which
# protocols are allowable, add map(s) eventually...
shell.openExternal(location)
false

View file

@ -42,6 +42,9 @@
text-align: center;
color:@text-color-subtle;
padding-top: 15px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
}
}

View file

@ -157,6 +157,7 @@ body.is-blurred {
margin-top: @spacing-half;
margin-left: @spacing-three-quarters;
margin-right: @spacing-three-quarters;
flex-shrink: 0;
height:32px;
}
}