mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-09-09 06:04:33 +08:00
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:
parent
7bd3f7ac78
commit
b194d7fb37
15 changed files with 107 additions and 56 deletions
|
@ -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;
|
||||
|
|
|
@ -136,6 +136,7 @@
|
|||
|
||||
.collapsed-timestamp {
|
||||
margin-left: 0.5em;
|
||||
color: @text-color-very-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
27
spec-nylas/stores/namespace-store-spec.coffee
Normal file
27
spec-nylas/stores/namespace-store-spec.coffee
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
#
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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}")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -42,6 +42,9 @@
|
|||
text-align: center;
|
||||
color:@text-color-subtle;
|
||||
padding-top: 15px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue