fix(*) Small visual tweaks and fixes - see summary

Summary:
ThreadStore should be done loading as soon as threads are available

SearchSuggestionStore should use ContactsStore for contact results

Contact Store should not "filter all, take 10" it should only filter until it has 10. It should also check against "Ben Gotow" as well as "Ben" and "Gotow", so I can type "Ben Go"

Sometimes participants are "Ben Gotow <ben@g.com>", "ben@g.com". If we get zero contacts after removing Me, put "Me" back in...

Fix "Update Available" notification, broken reference to `atom.views.getView(atom.workspace)`

A bit more debugging around cursors. Need to handle this case soon.

Only use atomWorkspace if it exists.

Fix for dragging next to / around toolbar window controls

Consolidate the display of Contacts in menus into a single MenuItem subclass

Update Template Popover styling

fetchFromCache should only remove thread loading indicator *IF* it found results in the cache. Doh...

Give the thread list "Name" column a fixed width (mg)

Better styling of message list collapsed mode, rage against user selection and cursor: pointer

Occasionally admin.inboxapp.com returns bogus data

Sebaastian feedback on thread list

Test Plan: Run tests

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1350
This commit is contained in:
Ben Gotow 2015-03-25 18:22:52 -07:00
parent c04d8a6a93
commit dff5465931
27 changed files with 242 additions and 126 deletions

View file

@ -49,7 +49,7 @@
&:hover {
background: darken(@source-list-bg, 5%);
cursor: pointer;
cursor: default;
}
}
}

View file

@ -3,7 +3,7 @@ _ = require 'underscore-plus'
{Contact,
ContactStore} = require 'inbox-exports'
{TokenizingTextField} = require 'ui-components'
{TokenizingTextField, Menu} = require 'ui-components'
module.exports =
ParticipantsTextField = React.createClass
@ -58,15 +58,7 @@ ParticipantsTextField = React.createClass
focus: -> @refs.textField.focus()
_completionContent: (p) ->
if p.name?.length > 0 and p.name isnt p.email
<div className="completion-participant">
<span className="participant-name">{p.name}</span>
<span className="participant-email">({p.email})</span>
</div>
else
<div className="completion-participant">
<span className="participant-name">{p.email}</span>
</div>
<Menu.NameEmailItem name={p.name} email={p.email} />
_componentForParticipant: (p) ->
if p.name?.length > 0 and p.name isnt p.email

View file

@ -280,24 +280,6 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
z-index: 2;
padding: 5px @spacing-standard 0 @spacing-standard;
.completion-participant {
.participant-name, .participant-email {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.participant-name {
display: block;
float: left;
}
.participant-email {
display: block;
float: right;
}
}
.participant {
white-space: nowrap;
text-overflow: ellipsis;

View file

@ -2,6 +2,7 @@
.activity-bar {
-webkit-font-smoothing: auto;
-webkit-user-select:none;
background-color: rgba(80,80,80,1);
border-top:1px solid rgba(0,0,0,0.7);
color:white;
@ -116,6 +117,7 @@
overflow-y: scroll;
background-color: rgba(0,0,0,0.5);
font-family: monospace;
-webkit-user-select:auto;
&.queue {
padding: 0;

View file

@ -174,8 +174,13 @@ MessageList = React.createClass
collapsed={collapsed}
thread_participants={@_threadParticipants()} />
unless idx is @state.messages.length - 1
components.push <hr className="message-item-divider" />
if idx < @state.messages.length - 1
next = @state.messages[idx + 1]
nextCollapsed = next and !@state.messagesExpandedState[next.id]
if collapsed and nextCollapsed
components.push <hr className="message-item-divider collapsed" />
else
components.push <hr className="message-item-divider" />
components

View file

@ -78,8 +78,7 @@
.messages-wrap {
flex: 4;
overflow-y: auto;
// position: absolute;
// top:0; left:0; right:0; bottom:0;
-webkit-user-select:none;
opacity:0;
transition: opacity 0s;
&.has-reply-area {
@ -97,15 +96,32 @@
margin: 0 auto;
padding: @spacing-standard 0 @spacing-double 0;
-webkit-user-select:none;
&:first-child { padding-top: 0; }
&.collapsed {
padding: 0;
background-color: @background-secondary;
padding-bottom: 0;
background-color: darken(@background-primary, 3%);
-webkit-user-select:none;
.message-header {
padding-top: 0;
padding-bottom: 5px;
-webkit-user-select:none;
}
.snippet {
cursor:default;
cursor: default;
padding-top: @spacing-half;
color: @text-color-subtle;
color: @text-color-very-subtle;
}
.collapse-control,
.participant-label.to-label,
.participant-label.cc-label,
.participant-label.bcc-label,
.participant-name.to-contact,
.participant-name.cc-contact,
.participant-name.bcc-contact {
opacity: 0;
}
}
@ -113,12 +129,17 @@
}
.message-item-divider {
border:0; // remove default hr border left, right
border-top: 2px solid @border-secondary-bg;
height: 3px;
background: @background-secondary;
border-bottom: 1px solid @border-primary-bg;
margin: 0;
margin-right: 2px;
&.collapsed {
height:0px;
border-bottom: 0;
}
}
.message-header {
@ -127,6 +148,7 @@
border-bottom: 1px solid @border-color-divider;
padding-bottom: @spacing-standard;
padding-top: 5px;
-webkit-user-select:auto;
.message-actions-wrap {
width: 100%;
@ -139,6 +161,7 @@
height: 22px;
border: 1px solid @border-color-divider;
border-radius: 11px;
-webkit-user-select:none;
z-index: 4;
margin-top: 0.35em;
@ -208,8 +231,6 @@
.collapse-control.inactive { display: block; }
}
.collapse-control:hover {cursor: pointer;}
.footer-reply-area-wrap {
position: absolute;
bottom: 0;
@ -218,6 +239,7 @@
color: @text-color-very-subtle;
border-top: 1px solid @border-color-divider;
background: @background-primary;
-webkit-user-select:none;
z-index: 10;
&:hover {
@ -249,7 +271,7 @@
///////////////////////////////
.message-participants {
&.collapsed:hover {cursor: pointer;}
&.collapsed:hover {cursor: default;}
.from-contact {
font-weight: @headings-font-weight;
@ -316,6 +338,7 @@
padding: @spacing-standard;
order: 2;
flex-shrink: 0;
cursor: default;
.other-contact {
color: @text-color-subtle;
@ -323,7 +346,6 @@
&.selected {
font-weight: @font-weight-semi-bold;
}
&:hover {cursor: pointer;}
}
// TODO: DRY

View file

@ -12,15 +12,13 @@ module.exports =
if updater.getState() is 'update-available'
@displayNotification(updater.releaseVersion)
# Watch for state changes via a command the auto-update manager fires.
# This is necessary because binding callbacks through `remote` is dangerous
@_command = atom.commands.add 'atom-workspace', 'window:update-available', (event, version, releaseNotes) =>
@displayNotification(version)
atom.onUpdateAvailable ({releaseVersion, releaseNotes} = {}) =>
@displayNotification(releaseVersion)
displayNotification: (version) ->
version = if version then "(#{version})" else ''
Actions.postNotification
type: 'success',
type: 'info',
sticky: true
message: "An update to Edgehill is available #{version} - Restart now to update!",
icon: 'fa-flag',
@ -30,7 +28,6 @@ module.exports =
}]
deactivate: ->
@_command.dispose()
@_unlisten()
_onNotificationActionTaken: ({notification, action}) ->

View file

@ -41,12 +41,9 @@ describe "NotificationUpdateAvailable", ->
expect(@package.displayNotification).not.toHaveBeenCalled()
it "should listen for `window:update-available`", ->
spyOn(atom.commands, 'add').andCallThrough()
spyOn(atom, 'onUpdateAvailable').andCallThrough()
@package.activate()
args = atom.commands.add.mostRecentCall.args
expect(args[0]).toEqual('atom-workspace')
expect(args[1]).toEqual('window:update-available')
expect(atom.onUpdateAvailable).toHaveBeenCalled()
describe "displayNotification", ->
beforeEach ->

View file

@ -56,6 +56,8 @@ SearchBar = React.createClass
itemContentFunc = (item) ->
if item.divider
<Menu.Item divider={item.divider} />
else if item.contact
<Menu.NameEmailItem name={item.contact.name} email={item.contact.email} />
else
item.label

View file

@ -1,5 +1,7 @@
Reflux = require 'reflux'
{DatabaseStore, Actions, Contact} = require 'inbox-exports'
{Actions,
Contact,
ContactStore} = require 'inbox-exports'
_ = require 'underscore-plus'
# Stores should closely match the needs of a particular part of the front end.
@ -9,7 +11,6 @@ _ = require 'underscore-plus'
SearchSuggestionStore = Reflux.createStore
init: ->
@_all = []
@_suggestions = []
@_searchConstants = {"from": 4, "subject": 2}
@_query = ""
@ -19,14 +20,6 @@ SearchSuggestionStore = Reflux.createStore
@listenTo Actions.searchQueryChanged, @onSearchQueryChanged
@listenTo Actions.searchQueryCommitted, @onSearchQueryCommitted
@listenTo Actions.searchBlurred, @onSearchBlurred
@listenTo DatabaseStore, @onDataChanged
@onDataChanged()
onDataChanged: (change) ->
return if change && change.objectClass != Contact.name
DatabaseStore.findAll(Contact).then (contacts) =>
@_all = contacts
@repopulate()
onSearchQueryChanged: (query) ->
@_query = query
@ -56,12 +49,7 @@ SearchSuggestionStore = Reflux.createStore
val = term[key]?.toLowerCase()
return @trigger(@) unless val
contactResults = []
for contact in @_all
if contact.name?.toLowerCase().indexOf(val) == 0 or contact.email?.toLowerCase().indexOf(val) == 0
contactResults.push(contact)
if contactResults.length is 10
break
contactResults = ContactStore.searchContacts(val, limit:10)
@_suggestions.push
label: "Message Contains: #{val}"
@ -72,13 +60,8 @@ SearchSuggestionStore = Reflux.createStore
divider: 'People'
_.each contactResults, (contact) =>
if contact.name
label = "#{contact.name} <#{contact.email}>"
else
label = contact.email
@_suggestions.push
label: label
contact: contact
value: [{"participants": contact.email}]
@trigger(@)

View file

@ -59,7 +59,11 @@ FullContactStore = Reflux.createStore
@_error = err
else
@_error = null
@_accountCache = JSON.parse(data)
try
@_accountCache = JSON.parse(data)
catch err
@_error = err
@_accountCache = null
@trigger(@)
# Swap the url's to see real data
@ -68,5 +72,9 @@ FullContactStore = Reflux.createStore
@_error = err
else
@_error = null
@_applicationCache = JSON.parse(data)
try
@_applicationCache = JSON.parse(data)
catch err
@_error = err
@_applicationCache = null
@trigger(@)

View file

@ -54,8 +54,13 @@ ThreadListParticipants = React.createClass
list = @props.thread.participants
return [] unless list and list instanceof Array
me = NamespaceStore.current().emailAddress
if list.length > 1
list = _.reject list, (p) -> p.email is me
list = _.reject list, (p) -> p.email is me
# Removing "Me" may remove "Me" several times due to the way
# participants is created. If we're left with an empty array,
# put one a "Me" back in.
if list.length is 0
list.push(@props.thread.participants[0])
# We only ever want to show three. Ben...Kevin... Marty
# But we want the *right* three.

View file

@ -79,7 +79,7 @@ ThreadList = React.createClass
c2 = new ListTabular.Column
name: "Name"
flex: 1
width: 200
resolver: (thread) ->
<ThreadListParticipants thread={thread} />

View file

@ -10,10 +10,14 @@
flex: 1;
overflow: auto;
-webkit-user-select: none;
-webkit-font-smoothing: subpixel-antialiased;
position: relative;
.list-item {
background-color: @background-secondary;
background-color: darken(@background-primary, 3%);
}
.list-column {
border-bottom: 1px solid fade(@list-border, 60%);
}
.message-count {
@ -71,13 +75,25 @@
}
.unread:not(.selected) {
background-color: white;
background-color: @background-primary;
&:hover {
background: darken(@background-primary, 2%);
}
.list-column {
border-bottom: 1px solid @list-border;
}
.subject {
font-weight: @font-weight-semi-bold;
}
.snippet {
color: @text-color-subtle;
}
}
.selected {
// subpixel antialiasing looks awful against dark background colors
-webkit-font-smoothing: antialiased;
.participants {
color: @text-color-inverse;
.unread-true {

View file

@ -18,7 +18,7 @@
border-radius: @border-radius-base;
color: @text-color;
font-weight: @font-weight-medium;
font-weight: @font-weight-normal;
text-align: center;
text-transform: capitalize;

View file

@ -2,7 +2,7 @@ _ = require 'underscore-plus'
React = require 'react'
class ListColumn
constructor: ({@name, @resolver, @flex}) ->
constructor: ({@name, @resolver, @flex, @width}) ->
ListTabularItem = React.createClass
displayName: 'ListTabularItem'
@ -29,7 +29,7 @@ ListTabularItem = React.createClass
for column in (@props.columns ? [])
<div key={column.name}
displayName={column.name}
style={flex: column.flex}
style={_.pick(column, ['flex', 'width'])}
className="list-column">
{column.resolver(@props.item, @)}
</div>

View file

@ -63,6 +63,7 @@ Events
MenuItem = React.createClass
displayName: 'MenuItem'
render: ->
if @props.divider
<div className="divider">{@props.divider}</div>
@ -71,6 +72,22 @@ MenuItem = React.createClass
className += " selected" if @props.selected
<div className={className} key={@props.key} onMouseDown={@props.onMouseDown}>{@props.content}</div>
MenuNameEmailItem = React.createClass
displayName: 'MenuNameEmailItem'
propTypes:
name: React.PropTypes.string
email: React.PropTypes.string
render: ->
if @props.name?.length > 0 and @props.name isnt @props.email
<span>
<span className="primary">{@props.name}</span>
<span className="secondary">{"(#{@props.email})"}</span>
</span>
else
<span className="primary">{@props.email}</span>
Menu = React.createClass
@ -175,5 +192,6 @@ Menu = React.createClass
Menu.Item = MenuItem
Menu.NameEmailItem = MenuNameEmailItem
module.exports = Menu

View file

@ -78,7 +78,10 @@ Popover = React.createClass
popoverComponent = []
if @state.showing
popoverComponent = <div ref="popover" className="popover">{@props.children}</div>
popoverComponent = <div ref="popover" className="popover">
{@props.children}
<div className="popover-pointer"></div>
</div>
<div className={"popover-container "+@props.className} onBlur={@_onBlur} ref="container">
{wrappedButtonComponent}

View file

@ -47,6 +47,8 @@ class InboxLongConnection
success: (json) =>
@setCursor(json['cursor'])
callback(json['cursor'])
console.log("Retrieved cursor #{json['cursor']} from \
`generate_cursor` with timestamp: #{stamp}")
setCursor: (cursor) ->
atom.config.set(@_cursorKey, cursor)
@ -104,7 +106,15 @@ class InboxLongConnection
lib = require 'https'
req = lib.request options, (res) =>
return @retry() unless res.statusCode == 200
if res.statusCode isnt 200
res.on 'data', (chunk) =>
if chunk.toString().indexOf('Invalid cursor') > 0
console.log('Long Polling Connection: Cursor is invalid. Need to blow away local cache.')
# TODO THIS!
else
@retry()
return
@_buffer = ''
res.setEncoding('utf8')
processBufferThrottled = _.throttle(@onProcessBuffer, 400, {leading: false})

View file

@ -18,17 +18,32 @@ module.exports = ContactStore = Reflux.createStore
@_refreshCacheFromDB()
searchContacts: (search, {limit}={}) ->
return [] if not search or search.length is 0
limit ?= 5
limit = Math.max(limit, 0)
return [] if not search or search.length is 0
search = search.toLowerCase()
matches = _.filter @_contactCache, (contact) ->
matchFunction = (contact) ->
# For a given contact, check:
# - email (bengotow@gmail.com)
# - name parts (Ben, Go)
# - name full (Ben Gotow)
# (necessary so user can type more than first name ie: "Ben Go")
return true if contact.email?.toLowerCase().indexOf(search) is 0
return true if contact.name?.toLowerCase().indexOf(search) is 0
name = contact.name?.toLowerCase() ? ""
for namePart in name.split(/\s/)
return true if namePart.indexOf(search) is 0
false
matches = matches[0..limit-1] if matches.length > limit
matches = []
for contact in @_contactCache
if matchFunction(contact)
matches.push(contact)
if matches.length is limit
break
matches
_refreshCacheFromDB: ->

View file

@ -80,25 +80,31 @@ ThreadStore = Reflux.createStore
Actions.selectThreadId(newSelectedId)
console.log("Fetching data for thread list took #{Date.now() - start} msec")
# If we've loaded threads, remove the loading indicator.
# If there are no results, wait for the API query to finish
if @_items.length > 0
@_itemsLoading = false
@trigger()
fetchFromAPI: ->
return unless @_namespaceId
@_itemsLoading = true
@trigger()
doneLoading = =>
return unless @_itemsLoading
@_itemsLoading = false
@trigger()
if @_searchQuery
atom.inbox.getThreadsForSearch @_namespaceId, @_searchQuery, (items) =>
@_items = items
@_itemsLoading = false
@trigger()
doneLoading()
else
success = =>
@_itemsLoading = false
@trigger()
error = =>
@_itemsLoading = false
@trigger()
atom.inbox.getThreads(@_namespaceId, {tag: @_tagId}, {success: success, error: error})
@trigger()
atom.inbox.getThreads(@_namespaceId, {tag: @_tagId}, {success: doneLoading, error: doneLoading})
# Inbound Events

View file

@ -31,16 +31,10 @@ class WindowEventHandler
when 'update-available'
atom.updateAvailable(detail)
# FIXME: Remove this when deprecations are removed
{releaseVersion, releaseNotes} = detail
detail = [releaseVersion, releaseNotes]
if workspaceElement = atom.views.getView(atom.workspace)
atom.commands.dispatch workspaceElement, "window:update-available", detail
@subscribe ipc, 'command', (command, args...) ->
activeElement = document.activeElement
# Use the workspace element view if body has focus
if activeElement is document.body and workspaceElement = atom.views.getView(atom.workspace)
if activeElement is document.body and workspaceElement = document.getElementById("atom-workspace")
activeElement = workspaceElement
atom.commands.dispatch(activeElement, command, args[0])

View file

@ -5,12 +5,12 @@
.list-item {
font-size: @font-size-base;
line-height: @line-height-computed;
line-height: @line-height-computed * 1.15;
color: @text-color;
background: @list-bg;
&:hover {
background: @list-hover-bg;
background: darken(@list-bg, 5%);
}
&.selected {

View file

@ -8,7 +8,7 @@
position: relative;
input.search {
border:1px solid darken(@background-color-secondary, 10%);
background-color:white;
background-color: white;
border-radius:12px;
padding-left: 10px;
}
@ -27,17 +27,39 @@
.item {
display: block;
padding: 0.5em;
padding-left: @padding-base-horizontal;
padding-top: @padding-base-vertical;
padding-bottom: @padding-base-vertical;
cursor: pointer;
color: @text-color;
background: @background-primary;
width: 100%;
overflow: hidden;
.primary {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.secondary {
max-width: 100%;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
padding-left:5px;
color:@text-color-very-subtle;
}
}
.item.selected, .item:hover {
text-decoration: none;
background: @accent-primary;
color: @text-color-inverse;
.primary {
color: @text-color-inverse;
}
.secondary {
color: @text-color-inverse-very-subtle;
}
}
}

View file

@ -1,18 +1,54 @@
@import "ui-variables";
.popover-container {
display:inline-block;
position:relative;
display:inline-block;
position:relative;
}
.popover {
position: absolute;
top: -10px;
left: 50%;
width: 250px;
max-height:400px;
background-color: @background-color;
transform: translate(-50%,-100%);
box-shadow: 0px 4px 30px rgba(0,0,0,0.19), inset 0px 0px 1px rgba(0,0,0,0.5);
border-radius: @border-radius-base;
z-index: 40;
.menu {
z-index:1;
position: relative;
.content-container {
background: none;
}
.header-container {
border-top-left-radius: @border-radius-base;
border-top-right-radius: @border-radius-base;
background: none;
}
.footer-container {
border-bottom-left-radius: @border-radius-base;
border-bottom-right-radius: @border-radius-base;
background: none;
.item:last-child:hover {
border-bottom-left-radius: @border-radius-base;
border-bottom-right-radius: @border-radius-base;
}
}
}
.popover-pointer {
position: absolute;
top: 0;
left: 0;
width: 250px;
max-height:400px;
background-color: @background-color;
transform: translateY(-100%);
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
z-index: 40;
width: 22.5px;
height: 11px;
background: transparent url('images/tooltip/tooltip-bg-pointer@2x.png') no-repeat;
background-size: 22.5px 10.5px;
margin-left: 50%;
transform: translateX(-50%);
z-index:0;
bottom: -10px;
}
}

View file

@ -12,6 +12,7 @@
border: 1px solid @border-secondary-bg;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
border-radius: @border-radius-small;
background-color: @background-color;
position: absolute;
}

View file

@ -58,11 +58,11 @@ atom-workspace {
}
.toolbar-window-controls {
padding-top:14px;
padding-left:@spacing-half;
margin-top:14px;
margin-left:@spacing-half;
order: -1000;
min-width: 102px;
width: 102px;
min-width: 72px;
width: 72px;
flex-grow: 0;
flex-shrink: 0;