fix(friday): Bugs and initial pass at the Today view without content (see summary)

Summary:
Load unread counts from database again, not tags

fix(multiselect-list): Clear selection on esc

fix(onboarding): Make target=_blank links work in onboarding pages

fix(workspace): Items in header and footer regions are in a single column

fix(layout): Critical issue for things not 100% height

fix(activity-bar): Show in dev mode so you know you're in dev mode

fix(quoted-text): Support for #divRplyFwdMsg quoted text marker

Test Plan: Run specs

Reviewers: evan

Reviewed By: evan

Differential Revision: https://review.inboxapp.com/D1484
This commit is contained in:
Ben Gotow 2015-05-08 16:36:48 -07:00
parent a745681684
commit 0bd303865e
20 changed files with 350 additions and 69 deletions

View file

@ -1,4 +1,5 @@
React = require 'react'
_ = require 'underscore-plus'
classNames = require 'classnames'
{Actions, Utils, WorkspaceStore} = require 'inbox-exports'
{RetinaImg} = require 'ui-components'
@ -7,12 +8,22 @@ class AccountSidebarSheetItem extends React.Component
@displayName: 'AccountSidebarSheetItem'
render: =>
classSet = classNames
classSet = classNames
'item': true
'selected': @props.select
if @props.item.icon and @props.item.icon.displayName?
component = @props.item.icon
icon = <component selected={@props.select} />
else if _.isString(@props.item.icon)
icon = <RetinaImg name={@props.item.icon} fallback="folder.png" colorfill={@props.select} />
else
icon = <RetinaImg name={"folder.png"} colorfill={@props.select} />
<div className={classSet} onClick={@_onClick}>
<RetinaImg name={"folder.png"} colorfill={@props.select} />
{icon}
<span className="name"> {@props.item.name}</span>
</div>

View file

@ -11,9 +11,13 @@ _ = require 'underscore-plus'
AccountSidebarStore = Reflux.createStore
init: ->
@_inboxCount = null
@_tags = []
@_setStoreDefaults()
@_registerListeners()
@_populate()
@_populateInboxCount()
########### PUBLIC #####################################################
@ -42,55 +46,63 @@ AccountSidebarStore = Reflux.createStore
return unless namespace
DatabaseStore.findAll(Tag, namespaceId: namespace.id).then (tags) =>
# Collect the built-in tags we want to display, and the user tags
# (which can be identified by having non-hardcoded IDs)
# We ignore the server drafts so we can use our own localDrafts
tags = _.reject tags, (tag) -> tag.id is "drafts"
# We ignore the trash tag because you can't trash anything
tags = _.reject tags, (tag) -> tag.id is "trash"
mainTagIDs = ['inbox', 'drafts', 'sent', 'archive']
mainTags = _.filter tags, (tag) -> _.contains(mainTagIDs, tag.id)
userTags = _.reject tags, (tag) -> _.contains(mainTagIDs, tag.id)
# Sort the main tags so they always appear in a standard order
mainTags = _.sortBy mainTags, (tag) -> mainTagIDs.indexOf(tag.id)
mainTags.push new Tag(name: 'All Mail', id: '*')
# Sort user tags by name
userTags = _.sortBy(userTags, 'name')
# Find root views, add the Views section
rootSheets = _.filter WorkspaceStore.Sheet, (sheet) -> sheet.root and sheet.name
lastSections = @_sections
@_sections = [
{ label: 'Mailboxes', items: mainTags, type: 'tag' },
{ label: 'Views', items: rootSheets, type: 'sheet' },
{ label: 'Tags', items: userTags, type: 'tag' },
]
@trigger(@)
@_tags = tags
@_build()
_populateInboxCount: ->
namespace = NamespaceStore.current()
return unless namespace
# Make a web request for unread count
atom.inbox.makeRequest
method: 'GET'
path: "/n/#{namespace.id}/tags/inbox"
returnsModel: true
DatabaseStore.count(Thread, [
Thread.attributes.namespaceId.equal(namespace.id),
Thread.attributes.unread.equal(true),
Thread.attributes.tags.contains('inbox')
]).then (count) =>
if count isnt @_inboxCount
@_inboxCount = count
@_build()
_populateDraftCount: ->
namespace = NamespaceStore.current()
return unless namespace
_build: ->
tags = @_tags
DatabaseStore.count(Message, draft: true).then (count) =>
#TODO: Save Draft Count
@trigger(@)
# Collect the built-in tags we want to display, and the user tags
# (which can be identified by having non-hardcoded IDs)
# We ignore the server drafts so we can use our own localDrafts
tags = _.reject tags, (tag) -> tag.id is "drafts"
# We ignore the trash tag because you can't trash anything
tags = _.reject tags, (tag) -> tag.id is "trash"
mainTagIDs = ['inbox', 'drafts', 'sent', 'archive']
mainTags = _.filter tags, (tag) -> _.contains(mainTagIDs, tag.id)
userTags = _.reject tags, (tag) -> _.contains(mainTagIDs, tag.id)
# Sort the main tags so they always appear in a standard order
mainTags = _.sortBy mainTags, (tag) -> mainTagIDs.indexOf(tag.id)
mainTags.push new Tag(name: 'All Mail', id: '*')
inboxTag = _.find tags, (tag) -> tag.id is 'inbox'
inboxTag?.unreadCount = @_inboxCount
# Sort user tags by name
userTags = _.sortBy(userTags, 'name')
# Find root views, add the Views section
featureSheets = _.filter WorkspaceStore.Sheet, (sheet) ->
sheet.name in ['Today']
extraSheets = _.filter WorkspaceStore.Sheet, (sheet) ->
sheet.root and sheet.name and not (sheet in featureSheets)
lastSections = @_sections
@_sections = [
{ label: '', items: featureSheets, type: 'sheet' },
{ label: 'Mailboxes', items: mainTags, type: 'tag' },
{ label: 'Views', items: extraSheets, type: 'sheet' },
{ label: 'Tags', items: userTags, type: 'tag' },
]
@trigger(@)
_refetchFromAPI: ->
namespace = NamespaceStore.current()
@ -113,17 +125,11 @@ AccountSidebarStore = Reflux.createStore
_onDataChanged: (change) ->
@populateInboxCountDebounced ?= _.debounce =>
@_populateInboxCount()
, 1000
@populateDraftCountDebounced ?= _.debounce =>
@_populateDraftCount()
, 1000
, 5000
if change.objectClass is Tag.name
@_populate()
if change.objectClass is Thread.name
@populateInboxCountDebounced()
if change.objectClass is Message.name
return unless _.some change.objects, (msg) -> msg.draft
@populateDraftCountDebounced()
module.exports = AccountSidebarStore

View file

@ -24,7 +24,7 @@ ActivityBarStore = Reflux.createStore
@_curlHistory = []
@_longPollHistory = []
@_longPollState = 'Unknown'
@_visible = false
@_visible = atom.inDevMode()
_registerListeners: ->
@listenTo Actions.didMakeAPIRequest, @_onAPIRequest

View file

@ -93,12 +93,14 @@ EmailFixingStyles = """
.gmail_extra,
.gmail_quote,
#divRplyFwdMsg,
blockquote {
display:none;
}
.show-quoted-text .gmail_extra,
.show-quoted-text .gmail_quote,
.show-quoted-text #divRplyFwdMsg,
.show-quoted-text blockquote {
display:inherit;
}
@ -160,4 +162,4 @@ class EmailFrame extends React.Component
Utils.stripQuotedText(email)
module.exports = EmailFrame
module.exports = EmailFrame

View file

@ -31,6 +31,8 @@ class ContainerView extends React.Component
if webview
node = React.findDOMNode(webview)
if node.hasListeners is undefined
node.addEventListener 'new-window', (e) ->
require('shell').openExternal(e.url)
node.addEventListener 'did-start-loading', (e) ->
if node.hasMobileUserAgent is undefined
node.setUserAgent("Mozilla/5.0 (iPhone; CPU iPhone OS 7_1 like Mac OS X) AppleWebKit/537.51.2 (KHTML, like Gecko) Version/7.0 Mobile/11D167 Safari/9537.53")

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 MiB

View file

@ -0,0 +1,16 @@
TodayView = require "./today-view"
TodayIcon = require "./today-icon"
{ComponentRegistry,
WorkspaceStore} = require 'inbox-exports'
module.exports =
activate: (@state={}) ->
WorkspaceStore.defineSheet 'Today', {root: true, supportedModes: ['list'], name: 'Today', icon: TodayIcon},
list: ['RootSidebar', 'Today']
ComponentRegistry.register TodayView,
location: WorkspaceStore.Location.Today
deactivate: ->
ComponentRegistry.unregister(TodayView)

View file

@ -0,0 +1,32 @@
React = require 'react'
_ = require "underscore-plus"
moment = require 'moment'
classNames = require 'classnames'
class TodayIcon extends React.Component
@displayName: 'TodayIcon'
constructor: (@props) ->
@state =
moment: moment()
componentDidMount: =>
@_setTimeState()
componentWillUnmount: =>
clearInterval(@_timer)
render: =>
classes = classNames
'today-icon': true
'selected': @props.selected
<div className={classes}>{@state.moment.format('D')}</div>
_setTimeState: =>
timeTillNextSecond = (60 - (new Date).getSeconds()) * 1000
@_timer = setTimeout(@_setTimeState, timeTillNextSecond)
@setState(moment: moment())
module.exports = TodayIcon

View file

@ -0,0 +1,83 @@
React = require 'react'
_ = require "underscore-plus"
{Utils, Actions} = require 'inbox-exports'
{Spinner, EventedIFrame} = require 'ui-components'
moment = require 'moment'
class TodayViewDateTime extends React.Component
@displayName: 'TodayViewDateTime'
constructor: (@props) ->
@state =
moment: moment()
componentDidMount: =>
@_setTimeState()
componentWillUnmount: =>
clearInterval(@_timer)
render: =>
<div className="centered">
<div className="time">{@state.moment.format('h:mm')}</div>
<div className="date">{@state.moment.format('dddd, MMM Do')}</div>
</div>
_setTimeState: =>
timeTillNextSecond = (60 - (new Date).getSeconds()) * 1000
@_timer = setTimeout(@_setTimeState, timeTillNextSecond)
@setState(moment: moment())
class TodayViewBox extends React.Component
@displayName: 'TodayViewBox'
@propTypes:
name: React.PropTypes.string.isRequired
constructor: (@props) ->
render: =>
<div className="box">
<h2>{@props.name}</h2>
</div>
class TodayView extends React.Component
@displayName: 'TodayView'
constructor: (@props) ->
@state = @_getStateFromStores()
render: =>
<div className="today">
<div className="inner">
<TodayViewDateTime />
<div className="boxes">
<TodayViewBox name="Conversations">
</TodayViewBox>
<TodayViewBox name="Events">
</TodayViewBox>
<TodayViewBox name="Drafts">
</TodayViewBox>
</div>
<div className="to-the-inbox">
Inbox
</div>
</div>
</div>
componentDidMount: =>
@_unsubscribers = []
componentWillUnmount: =>
unsubscribe() for unsubscribe in @_unsubscribers
_getStateFromStores: =>
{}
_onChange: =>
@setState(@_getStateFromStores())
module.exports = TodayView

View file

@ -0,0 +1,14 @@
{
"name": "today",
"version": "0.1.0",
"main": "./lib/main",
"description": "Today View",
"license": "Proprietary",
"private": true,
"engines": {
"atom": "*"
},
"dependencies": {
"moment": "^2.8"
}
}

View file

@ -0,0 +1,86 @@
@import "ui-variables";
@import "ui-mixins";
@font-face {
font-family: 'Hurme';
font-style: normal;
src: url(nylas://today/assets/HurmeGeometricSans4Thin.otf);
}
.today-icon {
display:inline-block;
overflow:hidden;
width:16px;
height:16px;
color:@source-list-bg;
text-align:center;
font-weight:500;
font-size:11px;
line-height:19px;
position:relative;
top:5px;
background-color:@text-color-very-subtle;
&.selected {
background-color:@accent-primary;
}
}
.today {
background:url(nylas://today/assets/background.png) top center no-repeat;
background-size:100%;
overflow-y:scroll;
position:absolute;
width:100%;
height:100%;
.inner {
}
.to-the-inbox {
opacity:0.3;
position:absolute;
width:100%;
text-align:center;
bottom:10px;
font-weight:@font-weight-semi-bold;
}
.centered {
text-align:center;
opacity:0.6;
.time {
font-family: 'Hurme';
margin-top:70px;
font-size:100px;
line-height:96px;
}
.date {
font-family:@font-family-sans-serif;
font-weight:@font-weight-normal;
font-size:22px;
}
}
.boxes {
display: flex;
flex-direction:row;
padding:15px;
position:absolute;
bottom:20px;
width:100%;
.box {
margin:15px;
border-radius: @border-radius-large;
background-color:white;
box-shadow: 0 1px 2px rgba(0,0,0,0.3);
flex:1;
height:40vh;
h2 {
margin-top:4px;
padding:12px;
border-bottom:1px solid #ccc;
font-size:15px;
font-weight:@font-weight-semi-bold;
}
}
}
}

View file

@ -1,27 +1,40 @@
Reflux = require 'reflux'
_ = require 'underscore-plus'
{DatabaseStore, NamespaceStore, Actions, Tag} = require 'inbox-exports'
{DatabaseStore, NamespaceStore, Actions, Thread} = require 'inbox-exports'
remote = require 'remote'
app = remote.require 'app'
AppUnreadCount = null
module.exports =
AppUnreadBadgeStore = Reflux.createStore
init: ->
@listenTo NamespaceStore, @_onNamespaceChanged
@listenTo DatabaseStore, @_onDataChanged
@_fetchCount()
_onNamespaceChanged: ->
@_onDataChanged()
_onDataChanged: (change) ->
return if change && change.objectClass != Tag.name
return app.dock?.setBadge?("") unless NamespaceStore.current()
@_updateBadge()
_updateBadge: ->
DatabaseStore.find(Tag, 'inbox').then (inbox) ->
return unless inbox
count = inbox.unreadCount
if change && change.objectClass is Thread.name
@_fetchCountDebounced ?= _.debounce(@_fetchCount, 5000)
@_fetchCountDebounced()
_fetchCount: ->
namespace = NamespaceStore.current()
return unless namespace
DatabaseStore.count(Thread, [
Thread.attributes.namespaceId.equal(namespace.id),
Thread.attributes.unread.equal(true),
Thread.attributes.tags.contains('inbox')
]).then (count) ->
return if AppUnreadCount is count
AppUnreadCount = count
if count > 999
app.dock?.setBadge?("\u221E")
else if count > 0

View file

@ -68,6 +68,7 @@ class MultiselectList extends React.Component
'core:previous-item': => @_onShift(-1)
'core:select-down': => @_onShift(1, {select: true})
'core:select-up': => @_onShift(-1, {select: true})
'application:pop-sheet': => @_onDeselect()
Object.keys(props.commands).forEach (key) =>
commands[key] = =>
@ -156,6 +157,10 @@ class MultiselectList extends React.Component
return unless id
@state.dataView.selection.toggle(@state.dataView.getById(id))
_onDeselect: =>
return unless @_visible()
@state.dataView.selection.clear()
_onShift: (delta, options = {}) =>
if @state.showKeyboardCursor and @_visible()
id = @state.keyboardCursorId

View file

@ -35,7 +35,7 @@ class ResizableRegion extends React.Component
###
Public: React `props` supported by ResizableRegion:
- `handle` Provide a {ResizableHandle} to indicate which edge of the
region should be draggable.
- `onResize` A {Function} that will be called continuously as the region is resized.
@ -58,6 +58,8 @@ class ResizableRegion extends React.Component
minHeight: React.PropTypes.number
maxHeight: React.PropTypes.number
style: React.PropTypes.object
constructor: (@props = {}) ->
@props.handle ?= ResizableHandle.Right
@state =
@ -65,7 +67,7 @@ class ResizableRegion extends React.Component
render: =>
if @props.handle.axis is 'horizontal'
containerStyle =
containerStyle = _.extend {}, @props.style,
'minWidth': @props.minWidth
'maxWidth': @props.maxWidth
'position': 'relative'
@ -76,7 +78,7 @@ class ResizableRegion extends React.Component
containerStyle.flex = 1
else
containerStyle =
containerStyle = _.extend {}, @props.style,
'minHeight': @props.minHeight
'maxHeight': @props.maxHeight
'position': 'relative'
@ -90,7 +92,7 @@ class ResizableRegion extends React.Component
containerStyle.flex = 1
otherProps = _.omit(@props, _.keys(@constructor.propTypes))
<div style={containerStyle} {...otherProps}>
{@props.children}
<div className={@props.handle.className}
@ -111,7 +113,7 @@ class ResizableRegion extends React.Component
@setState(height: nextProps.initialHeight)
if nextProps.handle.axis is 'horizontal' and nextProps.initialWidth != @props.initialWidth
@setState(width: nextProps.initialWidth)
componentWillUnmount: =>
PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId
@_taskId = null

View file

@ -15,8 +15,12 @@ about Tags on the Nylas Platform, read the
API documentation for more information about what tags are read-only.
`unreadCount`: {AttributeNumber} The number of unread threads with the tag.
Note: This attribute is only available when a single tag is fetched directly
from the Nylas API, not when all tags are listed.
`threadCount`: {AttributeNumber} The number of threads with the tag.
Note: This attribute is only available when a single tag is fetched directly
from the Nylas API, not when all tags are listed.
###
class Tag extends Model
@ -34,4 +38,4 @@ class Tag extends Model
modelKey: 'threadCount'
jsonKey: 'thread_count'
module.exports = Tag
module.exports = Tag

View file

@ -134,7 +134,7 @@ Utils =
tableNameForJoin: (primaryKlass, secondaryKlass) ->
"#{primaryKlass.name}-#{secondaryKlass.name}"
imageNamed: (resourcePath, fullname) ->
[name, ext] = fullname.split('.')
@ -171,6 +171,7 @@ Utils =
/<[br|p][ ]*>[\n]?[ ]*&gt;/i, # HTML lines beginning with >
/[\n|>]On .* wrote:[\n|<]/, #On ... wrote: on it's own line
/.gmail_quote/ # gmail quote class class
/divRplyFwdMsg/ # outlook?
]
for regex in regexs

View file

@ -106,7 +106,7 @@ WorkspaceStore = Reflux.createStore
###
Managing Sheets
###
# * `id` {String} The ID of the Sheet being defined.
# * `options` {Object} If the sheet should be listed in the left sidebar,
# pass `{root: true, name: 'Label'}`.
@ -126,6 +126,7 @@ WorkspaceStore = Reflux.createStore
columns: columns
supportedModes: Object.keys(columns)
icon: options.icon
name: options.name
root: options.root

View file

@ -192,6 +192,7 @@ class SheetContainer extends React.Component
<div name="Header" style={order:1, zIndex: 3}>
<InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}
direction="column"
id={topSheet.id}/>
</div>
@ -206,6 +207,7 @@ class SheetContainer extends React.Component
<div name="Footer" style={order:3, zIndex: 4}>
<InjectedComponentSet matching={locations: [topSheet.Footer, WorkspaceStore.Sheet.Global.Footer]}
direction="column"
id={topSheet.id}/>
</div>
</Flexbox>

View file

@ -63,6 +63,7 @@ class Sheet extends React.Component
<ResizableRegion key={"#{@props.data.id}:#{idx}"}
name={"#{@props.data.id}:#{idx}"}
className={"column-#{location.id}"}
style={height:'100%'}
data-column={idx}
onResize={ => @props.onColumnSizeChanged(@) }
minWidth={minWidth}
@ -76,7 +77,7 @@ class Sheet extends React.Component
name={"#{@props.data.id}:#{idx}"}
className={"column-#{location.id}"}
data-column={idx}
style={flex: 1}
style={flex: 1, height:'100%'}
matching={location: location, mode: @state.mode}/>
_getStateFromStores: =>