fix(*): Message list container styles, logout exceptions, composer logic cleanup

Summary:
This diff makes a couple changes:

- New drafts are no longer created in the composer component. Draft creation is back in the draft store where it belongs!
  - This means that the Draft List doesn't *always* contain two extra drafts, which are the unopened hot windows.
- Windows never show until they're loaded, which means windows open slowly vs. opening empty. I think the former is preferable but it's easy to change.
- Login window is now sized *before* it's shown instead of afterwards
- The mailto: behavior is now handled by the DraftStore, which gets rid of the whole draftInitialJSON thing that never made any sense.

fix(login) Make login window show at the correct size

logout from edgehill completely

Fix draft delete issue

Don't show windows, hot or cold, until they've loaded

Move logic for responding to mailto links into the draft store

Always fire `windowPropsChanged` after packages load or the window is shown

Show more error codes in red in activity bar

Make sure DraftStoreProxy doesn't interrupt the rendering of the composer with a new draft

Fix account-sidebar scroll issue, maybe?

Test Plan: Run tests!

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1479
This commit is contained in:
Ben Gotow 2015-05-07 14:42:39 -07:00
parent 919fc1d70f
commit 21a32ac57f
12 changed files with 129 additions and 133 deletions

View file

@ -4,7 +4,7 @@
#account-sidebar {
order: 2;
height: 100%;
overflow: auto;
overflow-y: auto;
background-color: @source-list-bg;
@ -44,11 +44,11 @@
&.selected {
background: @source-list-active-bg;
color: @source-list-active-color;
img.colorfill {
img.colorfill {
background: @source-list-active-color;
}
}
&:hover {
background: darken(@source-list-bg, 5%);
cursor: default;

View file

@ -10,10 +10,8 @@ NewComposeButton = require('./new-compose-button')
ComposerView = require('./composer-view')
module.exports =
item: null # The DOM item the main React component renders into
activate: (@state={}) ->
atom.registerHotWindow
windowType: "composer"
replenishNum: 2
@ -26,66 +24,33 @@ module.exports =
@_activateComposeButton()
else
@_setupContainer()
windowProps = atom.getLoadSettings().windowProps ? {}
@windowPropsChanged(windowProps)
windowPropsChanged: (newWindowProps) ->
@_prepareDraft(newWindowProps).then (draftLocalId) =>
React.render(
<ComposerView mode="fullwindow" localId={draftLocalId} />, @item
)
if newWindowProps.errorMessage
@_showInitialErrorDialog(newWindowProps.errorMessage)
.catch (error) ->
console.error(error.stack)
windowPropsReceived: ({draftLocalId, errorMessage}) ->
return unless @_container
React.render(
<ComposerView mode="fullwindow" localId={draftLocalId} />, @_container
)
if errorMessage
@_showInitialErrorDialog(errorMessage)
deactivate: ->
if atom.isMainWindow()
React.unmountComponentAtNode(@new_compose_button)
@new_compose_button.remove()
@new_compose_button = null
React.unmountComponentAtNode(@_composeButton)
@_composeButton.remove()
@_composeButton = null
else
React.unmountComponentAtNode(@item)
@item.remove()
@item = null
React.unmountComponentAtNode(@_container)
@_container.remove()
@_container = null
serialize: -> @state
_setupContainer: ->
if @item? then return # Activate once
@item = document.createElement("div")
@item.setAttribute("id", "composer-full-window")
@item.setAttribute("class", "composer-full-window")
document.body.appendChild(@item)
# This logic used to be in the DraftStore (which is where it should be). It
# got moved here becaues of an obscure atom-shell/Chrome bug whereby database
# requests firing right before the new-window loaded would cause the
# new-window to load with about:blank instead of its contents. By moving the
# DB logic here, we can get around this.
_prepareDraft: ({draftLocalId, draftInitialJSON}={}) ->
# The NamespaceStore isn't set yet in the new window, populate it first.
NamespaceStore.populateItems().then ->
new Promise (resolve, reject) ->
if draftLocalId?
resolve(draftLocalId)
else
# Create a new draft
draft = new Message
body: ""
from: [NamespaceStore.current().me()]
date: (new Date)
draft: true
pristine: true
namespaceId: NamespaceStore.current().id
# If initial JSON was provided, apply it to the new model.
# This is used to apply the values in mailto: links to new drafts
if draftInitialJSON
draft.fromJSON(draftInitialJSON)
DatabaseStore.persistModel(draft).then ->
DatabaseStore.localIdForModel(draft).then(resolve).catch(reject)
.catch(reject)
if @_container? then return # Activate once
@_container = document.createElement("div")
@_container.setAttribute("id", "composer-full-window")
@_container.setAttribute("class", "composer-full-window")
document.body.appendChild(@_container)
_activateComposeButton: ->
ComponentRegistry.register NewComposeButton,

View file

@ -121,6 +121,10 @@
padding-bottom:3px;
}
.item.status-code-500,
.item.status-code-501,
.item.status-code-502,
.item.status-code-503,
.item.status-code-504,
.item.status-code-400,
.item.status-code-404,
.item.status-code-409 {

View file

@ -12,6 +12,9 @@ MessageItem = require "./message-item"
class MessageList extends React.Component
@displayName: 'MessageList'
@containerRequired: false
@containerStyles:
minWidth: 500
maxWidth: 900
constructor: (@props) ->
@state = @_getStateFromStores()
@ -253,7 +256,5 @@ class MessageList extends React.Component
_wasAtBottom: =>
(@_lastScrollTop + @_lastHeight) >= @_lastScrollHeight
MessageList.minWidth = 500
MessageList.maxWidth = 900
module.exports = MessageList

View file

@ -69,11 +69,10 @@ class DraftList extends React.Component
# Additional Commands
_onDelete: ({focusedId}) =>
item = @state.dataView.getById(focusedId)
item = DraftListStore.view().getById(focusedId)
return unless item
DatabaseStore.localIdForModel(item).then (localId) ->
Actions.destroyDraft(localId)
@_onShiftSelectedIndex(-1)
module.exports = DraftList

View file

@ -84,10 +84,8 @@ class Atom extends Model
# Returns the load settings hash associated with the current window.
@getLoadSettings: ->
# We pull from the window object instead of the url so the
# loadSettings can change post bootup. This is very useful for
# starting hot windows.
@loadSettings ?= @getCurrentWindow().loadSettings
@loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))
cloned = _.deepClone(@loadSettings)
# The loadSettings.windowState could be large, request it only when needed.
cloned.__defineGetter__ 'windowState', =>
@ -228,8 +226,6 @@ class Atom extends Model
@subscribe @packages.onDidActivateInitialPackages => @watchThemes()
@windowEventHandler = new WindowEventHandler
ipc.on("window-props-changed", => @windowPropsChanged())
# Start our error reporting to the backend and attach error handlers
# to the window and the Bluebird Promise library, converting things
# back through the sourcemap as necessary.
@ -452,19 +448,16 @@ class Atom extends Model
reload: ->
ipc.send('call-window-method', 'restart')
# Calls the `windowPropsChanged` method of all packages that are
# Calls the `windowPropsReceived` method of all packages that are
# currently loaded
windowPropsChanged: ->
# This will cause it to get refreshed the next time they're queried
@constructor.loadSettings = null
loadSettingsChanged: (loadSettings) =>
@loadSettings = loadSettings
{width, height, windowProps} = loadSettings
{width,
height,
windowProps} = @getLoadSettings()
@packages.windowPropsReceived(windowProps ? {})
@packages.windowPropsChanged(windowProps)
@setWindowDimensions({width, height}) if width and height
if width and height
@setWindowDimensions({width, height})
# Extended: Returns a {Boolean} true when the current window is maximized.
isMaximixed: ->
@ -620,6 +613,7 @@ class Atom extends Model
{width,
height,
windowType,
windowProps,
windowPackages} = @getLoadSettings()
@loadConfig()
@ -633,6 +627,9 @@ class Atom extends Model
@packages.loadPackage(pack) for pack in (windowPackages ? [])
@packages.activate()
ipc.on("load-settings-changed", @loadSettingsChanged)
@packages.windowPropsReceived(windowProps ? {})
@keymaps.loadUserKeymap()
@setWindowDimensions({width, height}) if width and height
@ -645,6 +642,7 @@ class Atom extends Model
logout: ->
if @isLoggedIn()
@config.set('inbox', null)
@config.set('edgehill', null)
Actions = require './flux/actions'
Actions.logout()
@hide()

View file

@ -20,6 +20,8 @@ class AtomWindow
constructor: (settings={}) ->
{frame,
title,
width,
height,
resizable,
pathToOpen,
hideMenuBar,
@ -38,6 +40,8 @@ class AtomWindow
show: false
title: title ? 'Nilas'
frame: frame ? true
width: width
height: height
resizable: resizable ? true
icon: @constructor.iconPath
'auto-hide-menu-bar': hideMenuBar
@ -78,6 +82,8 @@ class AtomWindow
@browserWindow.once 'window:loaded', =>
@emit 'window:loaded'
@loaded = true
if @browserWindow.loadSettingsChangedSinceGetURL
@browserWindow.webContents.send('load-settings-changed', @browserWindow.loadSettings)
@browserWindow.loadUrl @getUrl(loadSettings)
@browserWindow.focusOnWebView() if @isSpec
@ -88,6 +94,8 @@ class AtomWindow
setLoadSettings: (loadSettings) ->
@browserWindow.loadSettings = loadSettings
@browserWindow.loadSettingsChangedSinceGetURL = true
@browserWindow.webContents.send('load-settings-changed', loadSettings) if @loaded
getUrl: (loadSettingsObj) ->
# Ignore the windowState when passing loadSettings via URL, since it could
@ -95,6 +103,8 @@ class AtomWindow
loadSettings = _.clone(loadSettingsObj)
delete loadSettings['windowState']
@browserWindow.loadSettingsChangedSinceGetURL = false
url.format
protocol: 'file'
pathname: "#{@resourcePath}/static/index.html"
@ -212,6 +222,15 @@ class AtomWindow
show: -> @browserWindow.show()
showWhenLoaded: ->
if @loaded
@show()
@focus()
else
@once 'window:loaded', =>
@show()
@focus()
focus: -> @browserWindow.focus()
minimize: -> @browserWindow.minimize()

View file

@ -11,7 +11,6 @@ path = require 'path'
os = require 'os'
net = require 'net'
url = require 'url'
qs = require 'querystring'
exec = require('child_process').exec
querystring = require 'querystring'
{EventEmitter} = require 'events'
@ -121,7 +120,7 @@ class AtomApplication
#
# This means that when `newWindow` is called, instead of going through
# the bootup process, it simply replaces key parameters and does a soft
# reload via `windowPropsChanged`.
# reload via `windowPropsReceived`.
#
# Since the window is already loaded, there are only some options that
# can be soft-reloaded. If you attempt to pass options that a soft
@ -213,9 +212,8 @@ class AtomApplication
newColdWindow: (options={}) ->
options = _.extend(@defaultWindowOptions(), options)
w = new AtomWindow options
w.show()
w.focus()
w = new AtomWindow(options)
w.showWhenLoaded()
# Tries to create a new hot window. Since we're updating an existing
# window instead of creatinga new one, there are limitations in the
@ -232,19 +230,10 @@ class AtomApplication
options.windowPackages = hotWindowParams.windowPackages
@newColdWindow(options)
else
win = hotWindowParams.loadedWindows.pop()
[win] = hotWindowParams.loadedWindows.splice(0,1)
newLoadSettings = _.extend(win.loadSettings(), options)
# This will update the internal instance variable that the window will
# query for its load settings.
win.setLoadSettings(newLoadSettings)
# This is expected to be caught by the main application to re-fetch
# the loadSettings and re-render itself accordingly.
win.browserWindow.webContents.send('window-props-changed')
win.show()
win.focus()
win.showWhenLoaded()
@_replenishHotWindows()
@ -283,6 +272,7 @@ class AtomApplication
@_replenishQueue.push(optionsArray.shift())
@_processReplenishQueue()
_replenishHotWindows: _.debounce(AtomApplication::__replenishHotWindows, 100)
_processReplenishQueue: ->
@ -619,19 +609,7 @@ class AtomApplication
# Attempt to parse the mailto link into Message object JSON
# and then open a composer window
if parts.protocol is 'mailto:'
query = qs.parse(parts.query)
query.to = "#{parts.auth}@#{parts.host}"
json = {
subject: query.subject || '',
body: query.body || '',
}
emailToObj = (email) -> {email: email, object: 'Contact'}
for attr in ['to', 'cc', 'bcc']
json[attr] = query[attr]?.split(',').map(emailToObj) || []
@mainWindow.browserWindow.webContents.send('mailto', json)
@mainWindow.browserWindow.webContents.send('mailto', urlToOpen)
# The host of the URL being opened is assumed to be the package name
# responsible for opening the URL. A new window will be created with

View file

@ -45,15 +45,15 @@ class Contact extends Model
# - `name` if the contact has a populated name value
# - `email` in all other cases.
displayName: ->
return "You" if @email == NamespaceStore.current().emailAddress
return "You" if @email is NamespaceStore.current()?.emailAddress
@_nameParts().join(' ')
displayFirstName: ->
return "You" if @email == NamespaceStore.current().emailAddress
return "You" if @email is NamespaceStore.current()?.emailAddress
@firstName()
displayLastName: ->
return "" if @email == NamespaceStore.current().emailAddress
return "" if @email is NamespaceStore.current()?.emailAddress
@lastName()
firstName: ->

View file

@ -5,11 +5,13 @@ ipc = require 'ipc'
DraftStoreProxy = require './draft-store-proxy'
DatabaseStore = require './database-store'
NamespaceStore = require './namespace-store'
ContactStore = require './contact-store'
SendDraftTask = require '../tasks/send-draft'
DestroyDraftTask = require '../tasks/destroy-draft'
Thread = require '../models/thread'
Contact = require '../models/contact'
Message = require '../models/message'
MessageUtils = require '../models/message-utils'
Actions = require '../actions'
@ -40,11 +42,11 @@ class DraftStore
@listenTo Actions.composeReply, @_onComposeReply
@listenTo Actions.composeForward, @_onComposeForward
@listenTo Actions.composeReplyAll, @_onComposeReplyAll
@listenTo Actions.composePopoutDraft, @_onComposePopoutDraft
@listenTo Actions.composeNewBlankDraft, @_onComposeNewBlankDraft
@listenTo Actions.composePopoutDraft, @_onPopoutDraftLocalId
@listenTo Actions.composeNewBlankDraft, @_onPopoutBlankDraft
atom.commands.add 'body',
'application:new-message': => @_onComposeNewBlankDraft()
'application:new-message': => @_onPopoutBlankDraft()
@listenTo Actions.sendDraft, @_onSendDraft
@listenTo Actions.destroyDraft, @_onDestroyDraft
@ -59,9 +61,7 @@ class DraftStore
@_sendingState = {}
@_extensions = []
ipc.on 'mailto', (mailToJSON) =>
return unless atom.isMainWindow()
atom.newWindow @_composerWindowProps(draftInitialJSON: mailToJSON)
ipc.on 'mailto', @_onHandleMailtoLink
# TODO: Doesn't work if we do window.addEventListener, but this is
# fragile. Pending an Atom fix perhaps?
@ -301,22 +301,54 @@ class DraftStore
re = new RegExp("<img.*#{cidRE}[\\s\\S]*?>", "igm")
body.replace(re, "")
# The logic to create a new Draft used to be in the DraftStore (which is
# where it should be). It got moved to composer/lib/main.cjsx becaues
# of an obscure atom-shell/Chrome bug whereby database requests firing right
# before the new-window loaded would cause the new-window to load with
# about:blank instead of its contents. By moving the DB logic there, we can
# get around this.
_onComposeNewBlankDraft: =>
atom.newWindow @_composerWindowProps()
_onPopoutBlankDraft: =>
draft = new Message
body: ""
from: [NamespaceStore.current().me()]
date: (new Date)
draft: true
pristine: true
namespaceId: NamespaceStore.current().id
DatabaseStore.persistModel(draft).then =>
DatabaseStore.localIdForModel(draft).then(@_onPopoutDraftLocalId)
_onComposePopoutDraft: (draftLocalId) =>
atom.newWindow @_composerWindowProps(draftLocalId: draftLocalId)
_onPopoutDraftLocalId: (draftLocalId, options = {}) =>
options.draftLocalId = draftLocalId
_composerWindowProps: (props={}) =>
title: "Message"
windowType: "composer"
windowProps: _.extend {}, props
atom.newWindow
title: "Message"
windowType: "composer"
windowProps: options
_onHandleMailtoLink: (urlString) =>
namespace = NamespaceStore.current()
return unless namespace
url = require 'url'
qs = require 'querystring'
parts = url.parse(urlString)
query = qs.parse(parts.query)
query.to = "#{parts.auth}@#{parts.host}"
draft = new Message
body: query.body || ''
subject: query.subject || '',
from: [namespace.me()]
date: (new Date)
draft: true
pristine: true
namespaceId: namespace.id
contactForEmail = (email) ->
match = ContactStore.searchContacts(email, 1)
return match[0] if match[0]
return new Contact({email})
for attr in ['to', 'cc', 'bcc']
draft[attr] = query[attr]?.split(',').map(contactForEmail) || []
DatabaseStore.persistModel(draft).then =>
DatabaseStore.localIdForModel(draft).then(@_onPopoutDraftLocalId)
_onDestroyDraft: (draftLocalId) =>
# Immediately reset any pending changes so no saves occur
@ -354,7 +386,7 @@ class DraftStore
_onSendDraftError: (draftLocalId, errorMessage) ->
@_sendingState[draftLocalId] = false
if atom.getWindowType() is "composer"
atom.newWindow @_composerWindowProps({errorMessage, draftLocalId})
@_onPopoutDraftLocalId(draftLocalId, {errorMessage})
@trigger()
_onSendDraftSuccess: (draftLocalId) =>

View file

@ -445,5 +445,5 @@ class PackageManager
delete @activePackages[pack.name]
@emitter.emit 'did-deactivate-package', pack
windowPropsChanged: (windowProps) ->
pack.windowPropsChanged?(windowProps) for pack in @getLoadedPackages()
windowPropsReceived: (windowProps) ->
pack.windowPropsReceived?(windowProps) for pack in @getLoadedPackages()

View file

@ -154,8 +154,8 @@ class Package
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
windowPropsChanged: (windowProps) ->
@mainModule?.windowPropsChanged?(windowProps)
windowPropsReceived: (windowProps) ->
@mainModule?.windowPropsReceived?(windowProps)
activateNow: ->
try