diff --git a/.gitignore b/.gitignore index 45ca173b1..f9b54ef15 100644 --- a/.gitignore +++ b/.gitignore @@ -38,4 +38,5 @@ internal_packages/composer-scheduler internal_packages/link-tracking internal_packages/open-tracking internal_packages/send-later +internal_packages/send-reminders internal_packages/thread-sharing diff --git a/internal_packages/account-sidebar/lib/sidebar-section.coffee b/internal_packages/account-sidebar/lib/sidebar-section.coffee index 4a3884071..ceff3e96b 100644 --- a/internal_packages/account-sidebar/lib/sidebar-section.coffee +++ b/internal_packages/account-sidebar/lib/sidebar-section.coffee @@ -4,6 +4,7 @@ _ = require 'underscore' DestroyCategoryTask, CategoryStore, Category, + ExtensionRegistry, RegExpUtils} = require 'nylas-exports' SidebarItem = require './sidebar-item' SidebarActions = require './sidebar-actions' @@ -42,8 +43,15 @@ class SidebarSection draftsItem = SidebarItem.forDrafts([account.id]) snoozedItem = SidebarItem.forSnoozed([account.id]) + extensionItems = ExtensionRegistry.AccountSidebar.extensions() + .filter((ext) => ext.sidebarItem?) + .map((ext) => ext.sidebarItem([account.id])) + .map(({id, name, iconName, perspective}) => + SidebarItem.forPerspective(id, perspective, {name, iconName}) + ) + # Order correctly: Inbox, Unread, Starred, rest... , Drafts - items.splice(1, 0, unreadItem, starredItem, snoozedItem) + items.splice(1, 0, unreadItem, starredItem, snoozedItem, extensionItems...) items.push(draftsItem) return { @@ -96,8 +104,26 @@ class SidebarSection children: accounts.map (acc) -> SidebarItem.forSnoozed([acc.id], name: acc.label) ) + extensionItems = ExtensionRegistry.AccountSidebar.extensions() + .filter((ext) => ext.sidebarItem?) + .map((ext) => + {id, name, iconName, perspective} = ext.sidebarItem(accountIds) + return SidebarItem.forPerspective(id, perspective, { + name, + iconName, + children: accounts.map((acc) => + subItem = ext.sidebarItem([acc.id]) + return SidebarItem.forPerspective( + subItem.id + "-#{acc.id}", + subItem.perspective, + {name: acc.label, iconName: subItem.iconName} + ) + ) + }) + ) + # Order correctly: Inbox, Unread, Starred, rest... , Drafts - items.splice(1, 0, unreadItem, starredItem, snoozedItem) + items.splice(1, 0, unreadItem, starredItem, snoozedItem, extensionItems...) items.push(draftsItem) return { diff --git a/internal_packages/composer-markdown/lib/markdown-editor.cjsx b/internal_packages/composer-markdown/lib/markdown-editor.cjsx index cd9237510..daae60aee 100644 --- a/internal_packages/composer-markdown/lib/markdown-editor.cjsx +++ b/internal_packages/composer-markdown/lib/markdown-editor.cjsx @@ -111,6 +111,7 @@ class MarkdownEditor extends React.Component {".btn-templates { display:none; }"} {".btn-scheduler { display:none; }"} {".btn-translate { display:none; }"} + {".btn-send-reminder { display:none; }"}
diff --git a/internal_packages/message-list/lib/message-list.cjsx b/internal_packages/message-list/lib/message-list.cjsx index 91ecf2c35..d5692a1ea 100644 --- a/internal_packages/message-list/lib/message-list.cjsx +++ b/internal_packages/message-list/lib/message-list.cjsx @@ -195,6 +195,7 @@ class MessageList extends React.Component {subject} diff --git a/internal_packages/thread-list/lib/thread-list-columns.cjsx b/internal_packages/thread-list/lib/thread-list-columns.cjsx index 914ffe239..fda7a11fa 100644 --- a/internal_packages/thread-list/lib/thread-list-columns.cjsx +++ b/internal_packages/thread-list/lib/thread-list-columns.cjsx @@ -7,6 +7,7 @@ moment = require 'moment' RetinaImg, MailLabelSet, MailImportantIcon, + InjectedComponent, InjectedComponentSet} = require 'nylas-component-kit' {Thread, FocusedPerspectiveStore, Utils, DateUtils} = require 'nylas-exports' @@ -19,13 +20,13 @@ ThreadListStore = require './thread-list-store' ThreadListIcon = require './thread-list-icon' # Get and format either last sent or last received timestamp depending on thread-list being viewed -TimestampComponentForPerspective = (thread) -> +ThreadListTimestamp = ({thread}) -> if FocusedPerspectiveStore.current().isSent() rawTimestamp = thread.lastMessageSentTimestamp else rawTimestamp = thread.lastMessageReceivedTimestamp timestamp = DateUtils.shortTimeString(rawTimestamp) - {timestamp} + return {timestamp} subject = (subj) -> if (subj ? "").trim().length is 0 @@ -42,6 +43,13 @@ subject = (subj) -> else return subj +getSnippet = (thread) -> + messages = thread.__messages || [] + if (messages.length is 0) + return thread.snippet + + return messages[messages.length - 1].snippet + c1 = new ListTabular.Column name: "★" @@ -51,21 +59,23 @@ c1 = new ListTabular.Column + showIfAvailableForAnyAccount={true} + /> + exposedProps={thread: thread} + /> ] c2 = new ListTabular.Column name: "Participants" width: 200 resolver: (thread) => - hasDraft = _.find (thread.metadata ? []), (m) -> m.draft + hasDraft = (thread.__messages || []).find((m) => m.draft) if hasDraft
@@ -81,23 +91,23 @@ c3 = new ListTabular.Column flex: 4 resolver: (thread) => attachment = false - metadata = (thread.metadata ? []) + messages = (thread.__messages || []) - hasAttachments = thread.hasAttachments and metadata.find (m) -> Utils.showIconForAttachments(m.files) + hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files) if hasAttachments attachment =
{subject(thread.subject)} - {thread.snippet} + {getSnippet(thread)} {attachment} c4 = new ListTabular.Column name: "Date" resolver: (thread) => - TimestampComponentForPerspective(thread) + return c5 = new ListTabular.Column name: "HoverActions" @@ -114,7 +124,8 @@ c5 = new ListTabular.Column ]} matching={role: "ThreadListQuickAction"} className="thread-injected-quick-actions" - exposedProps={thread: thread}/> + exposedProps={thread: thread} + />
cNarrow = new ListTabular.Column @@ -123,13 +134,13 @@ cNarrow = new ListTabular.Column resolver: (thread) => pencil = false attachment = false - metadata = (thread.metadata ? []) + messages = (thread.__messages || []) - hasAttachments = thread.hasAttachments and metadata.find (m) -> Utils.showIconForAttachments(m.files) + hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files) if hasAttachments attachment =
- hasDraft = _.find metadata, (m) -> m.draft + hasDraft = messages.find((m) => m.draft) if hasDraft pencil = @@ -159,11 +170,19 @@ cNarrow = new ListTabular.Column {pencil} {attachment} - {TimestampComponentForPerspective(thread)} +
{subject(thread.subject)}
-
{thread.snippet} 
+
{getSnippet(thread)} 
diff --git a/internal_packages/thread-list/lib/thread-list-data-source.coffee b/internal_packages/thread-list/lib/thread-list-data-source.coffee index 19f994481..77eb61141 100644 --- a/internal_packages/thread-list/lib/thread-list-data-source.coffee +++ b/internal_packages/thread-list/lib/thread-list-data-source.coffee @@ -9,7 +9,7 @@ Rx = require 'rx-lite' _flatMapJoiningMessages = ($threadsResultSet) => # DatabaseView leverages `QuerySubscription` for threads /and/ for the - # messages on each thread, which are passed to out as `thread.metadata`. + # messages on each thread, which are passed to out as `thread.__messages`. $messagesResultSets = {} @@ -52,13 +52,13 @@ _flatMapJoiningMessages = ($threadsResultSet) => Rx.Observable.combineLatest(sets) .flatMapLatest ([threadsResultSet, messagesResultSets...]) => - threadsWithMetadata = {} + threadsWithMessages = {} threadsResultSet.models().map (thread, idx) -> thread = new thread.constructor(thread) - thread.metadata = messagesResultSets[idx]?.models() - threadsWithMetadata[thread.id] = thread + thread.__messages = messagesResultSets[idx]?.models().filter((m) => !m.isHidden()) + threadsWithMessages[thread.id] = thread - Rx.Observable.from([QueryResultSet.setByApplyingModels(threadsResultSet, threadsWithMetadata)]) + Rx.Observable.from([QueryResultSet.setByApplyingModels(threadsResultSet, threadsWithMessages)]) _observableForThreadMessages = (id, initialModels) -> subscription = new QuerySubscription(DatabaseStore.findAll(Message, threadId: id), { diff --git a/internal_packages/thread-list/lib/thread-list-icon.cjsx b/internal_packages/thread-list/lib/thread-list-icon.cjsx index 268ae2cea..b22d4b913 100644 --- a/internal_packages/thread-list/lib/thread-list-icon.cjsx +++ b/internal_packages/thread-list/lib/thread-list-icon.cjsx @@ -4,6 +4,7 @@ React = require 'react' Actions, Thread, ChangeStarredTask, + ExtensionRegistry, AccountStore} = require 'nylas-exports' class ThreadListIcon extends React.Component @@ -11,10 +12,20 @@ class ThreadListIcon extends React.Component @propTypes: thread: React.PropTypes.object - _iconType: => + _extensionsIconClassNames: => + return ExtensionRegistry.ThreadList.extensions() + .filter((ext) => ext.cssClassNamesForThreadListIcon?) + .reduce(((prev, ext) => prev + ' ' + ext.cssClassNamesForThreadListIcon(@props.thread)), '') + .trim() + + _iconClassNames: => if !@props.thread return 'thread-icon-star-on-hover' + extensionIconClassNames = @_extensionsIconClassNames() + if extensionIconClassNames.length > 0 + return extensionIconClassNames + if @props.thread.starred return 'thread-icon-star' @@ -33,7 +44,7 @@ class ThreadListIcon extends React.Component return 'thread-icon-none thread-icon-star-on-hover' _nonDraftMessages: => - msgs = @props.thread.metadata + msgs = @props.thread.__messages return [] unless msgs and msgs instanceof Array msgs = _.filter msgs, (m) -> m.serverId and not m.draft return msgs @@ -43,7 +54,7 @@ class ThreadListIcon extends React.Component true render: => -
diff --git a/internal_packages/thread-list/lib/thread-list-participants.cjsx b/internal_packages/thread-list/lib/thread-list-participants.cjsx index ac6713e8a..425286d92 100644 --- a/internal_packages/thread-list/lib/thread-list-participants.cjsx +++ b/internal_packages/thread-list/lib/thread-list-participants.cjsx @@ -53,15 +53,16 @@ class ThreadListParticipants extends React.Component short += ", " accumulate(short, unread) - if @props.thread.metadata and @props.thread.metadata.length > 1 - accumulate(" (#{@props.thread.metadata.length})") + messages = (@props.thread.__messages ? []) + if messages.length > 1 + accumulate(" (#{messages.length})") flush() return spans - getTokensFromMetadata: => - messages = @props.thread.metadata + getTokensFromMessages: => + messages = @props.thread.__messages tokens = [] field = 'from' @@ -94,8 +95,8 @@ class ThreadListParticipants extends React.Component contacts.map (contact) -> { contact: contact, unread: false } getTokens: => - if @props.thread.metadata instanceof Array - list = @getTokensFromMetadata() + if @props.thread.__messages instanceof Array + list = @getTokensFromMessages() else list = @getTokensFromParticipants() diff --git a/internal_packages/thread-list/lib/thread-list.cjsx b/internal_packages/thread-list/lib/thread-list.cjsx index 058b6f59f..cdd4bff83 100644 --- a/internal_packages/thread-list/lib/thread-list.cjsx +++ b/internal_packages/thread-list/lib/thread-list.cjsx @@ -1,7 +1,7 @@ _ = require 'underscore' React = require 'react' ReactDOM = require 'react-dom' -classNames = require 'classnames' +classnames = require 'classnames' {MultiselectList, FocusContainer, @@ -18,6 +18,7 @@ classNames = require 'classnames' WorkspaceStore, AccountStore, CategoryStore, + ExtensionRegistry, FocusedContentStore, FocusedPerspectiveStore} = require 'nylas-exports' @@ -109,9 +110,15 @@ class ThreadList extends React.Component _threadPropsProvider: (item) -> + classes = classnames({ + 'unread': item.unread + }) + classes += ExtensionRegistry.ThreadList.extensions() + .filter((ext) => ext.cssClassNamesForThreadListItem?) + .reduce(((prev, ext) => prev + ' ' + ext.cssClassNamesForThreadListItem(item)), ' ') + props = - className: classNames - 'unread': item.unread + className: classes props.shouldEnableSwipe = => perspective = FocusedPerspectiveStore.current() diff --git a/internal_packages/thread-list/spec/thread-list-participants-spec.cjsx b/internal_packages/thread-list/spec/thread-list-participants-spec.cjsx index 454f21398..bec3690b6 100644 --- a/internal_packages/thread-list/spec/thread-list-participants-spec.cjsx +++ b/internal_packages/thread-list/spec/thread-list-participants-spec.cjsx @@ -20,7 +20,7 @@ describe "ThreadListParticipants", -> ben = new Contact(email: 'ben@nylas.com', name: 'ben') ben.unread = true thread = new Thread() - thread.metadata = [new Message(from: [ben], unread:true)] + thread.__messages = [new Message(from: [ben], unread:true)] @participants = ReactTestUtils.renderIntoDocument( @@ -171,7 +171,7 @@ describe "ThreadListParticipants", -> for scenario in scenarios thread = new Thread() - thread.metadata = scenario.in + thread.__messages = scenario.in participants = ReactTestUtils.renderIntoDocument( ) @@ -191,9 +191,9 @@ describe "ThreadListParticipants", -> @michael = new Contact(email: 'michael@nylas.com', name: 'michael') @kavya = new Contact(email: 'kavya@nylas.com', name: 'kavya') - getTokens = (threadMetadata) -> + getTokens = (threadMessages) -> thread = new Thread() - thread.metadata = threadMetadata + thread.__messages = threadMessages participants = ReactTestUtils.renderIntoDocument( ) diff --git a/internal_packages/thread-search/lib/search-mailbox-perspective.es6 b/internal_packages/thread-search/lib/search-mailbox-perspective.es6 index 9ac6e56e4..0de3b40e1 100644 --- a/internal_packages/thread-search/lib/search-mailbox-perspective.es6 +++ b/internal_packages/thread-search/lib/search-mailbox-perspective.es6 @@ -13,7 +13,6 @@ class SearchMailboxPerspective extends MailboxPerspective { if (!_.isString(this.searchQuery)) { throw new Error("SearchMailboxPerspective: Expected a `string` search query") } - return this } emptyMessage() { diff --git a/internal_packages/thread-snooze/lib/snooze-buttons.jsx b/internal_packages/thread-snooze/lib/snooze-buttons.jsx index d9b0fab44..b8bf85fee 100644 --- a/internal_packages/thread-snooze/lib/snooze-buttons.jsx +++ b/internal_packages/thread-snooze/lib/snooze-buttons.jsx @@ -11,35 +11,30 @@ class SnoozeButton extends Component { className: PropTypes.string, threads: PropTypes.array, direction: PropTypes.string, - renderImage: PropTypes.bool, + shouldRenderIconImg: PropTypes.bool, getBoundingClientRect: PropTypes.func, }; static defaultProps = { className: 'btn btn-toolbar', direction: 'down', - renderImage: true, + shouldRenderIconImg: true, + getBoundingClientRect: (inst) => ReactDOM.findDOMNode(inst).getBoundingClientRect(), }; onClick = (event) => { event.stopPropagation() - const buttonRect = this.getBoundingClientRect() + const {threads, direction, getBoundingClientRect} = this.props + const buttonRect = getBoundingClientRect(this) Actions.openPopover( , - {originRect: buttonRect, direction: this.props.direction} + {originRect: buttonRect, direction: direction} ) }; - getBoundingClientRect = () => { - if (this.props.getBoundingClientRect) { - return this.props.getBoundingClientRect() - } - return ReactDOM.findDOMNode(this).getBoundingClientRect() - }; - render() { if (!FocusedPerspectiveStore.current().isInbox()) { return ; @@ -51,11 +46,12 @@ class SnoozeButton extends Component { className={`snooze-button ${this.props.className}`} onClick={this.onClick} > - {this.props.renderImage ? + {this.props.shouldRenderIconImg ? : null + /> : + null } ); @@ -89,9 +85,9 @@ export class QuickActionSnooze extends Component { return ( ); diff --git a/internal_packages/thread-snooze/lib/snooze-popover.jsx b/internal_packages/thread-snooze/lib/snooze-popover.jsx index 666a817ed..79cf25fa0 100644 --- a/internal_packages/thread-snooze/lib/snooze-popover.jsx +++ b/internal_packages/thread-snooze/lib/snooze-popover.jsx @@ -117,7 +117,7 @@ class SnoozePopover extends Component { ); diff --git a/internal_packages/thread-snooze/stylesheets/snooze-popover.less b/internal_packages/thread-snooze/stylesheets/snooze-popover.less index 4f1f810ba..633e9f5f4 100644 --- a/internal_packages/thread-snooze/stylesheets/snooze-popover.less +++ b/internal_packages/thread-snooze/stylesheets/snooze-popover.less @@ -7,7 +7,7 @@ } .snooze-button { - order: -104; + order: -103; } .snooze-popover { diff --git a/spec/components/date-input-spec.jsx b/spec/components/date-input-spec.jsx index c42b1ba83..952aa0249 100644 --- a/spec/components/date-input-spec.jsx +++ b/spec/components/date-input-spec.jsx @@ -22,8 +22,8 @@ const makeInput = (props = {}) => { describe('DateInput', function dateInput() { describe('onInputKeyDown', () => { it('should submit the input if Enter or Escape pressed', () => { - const onSubmitDate = jasmine.createSpy('onSubmitDate') - const component = makeInput({onSubmitDate: onSubmitDate}) + const onDateSubmitted = jasmine.createSpy('onDateSubmitted') + const component = makeInput({onDateSubmitted: onDateSubmitted}) const inputNode = ReactDOM.findDOMNode(component).querySelector('input') const stopPropagation = jasmine.createSpy('stopPropagation') const keys = ['Enter', 'Return'] @@ -34,10 +34,10 @@ describe('DateInput', function dateInput() { keys.forEach((key) => { Simulate.keyDown(inputNode, {key, stopPropagation}) expect(stopPropagation).toHaveBeenCalled() - expect(onSubmitDate).toHaveBeenCalledWith('someday', 'tomorrow') + expect(onDateSubmitted).toHaveBeenCalledWith('someday', 'tomorrow') expect(component.setState).toHaveBeenCalledWith({inputDate: null}) stopPropagation.reset() - onSubmitDate.reset() + onDateSubmitted.reset() component.setState.reset() }) }); diff --git a/spec/components/date-picker-popover-spec.jsx b/spec/components/date-picker-popover-spec.jsx new file mode 100644 index 000000000..e8ffadc86 --- /dev/null +++ b/spec/components/date-picker-popover-spec.jsx @@ -0,0 +1,81 @@ +import React from 'react'; +import {mount} from 'enzyme' +import {DateUtils} from 'nylas-exports' +import {DatePickerPopover} from 'nylas-component-kit' + + +const makePopover = (props = {}) => { + return mount( + my header} + onSelectDate={() => {}} + {...props} + /> + ); +}; + +describe('DatePickerPopover', function sendLaterPopover() { + beforeEach(() => { + spyOn(DateUtils, 'format').andReturn('formatted') + }); + + describe('selectDate', () => { + it('calls props.onSelectDate', () => { + const onSelectDate = jasmine.createSpy('onSelectDate') + const popover = makePopover({onSelectDate}) + popover.instance().selectDate({utc: () => 'utc'}, 'Custom') + + expect(onSelectDate).toHaveBeenCalledWith('formatted', 'Custom') + }); + }); + + describe('onSelectMenuOption', () => { + + }); + + describe('onSelectCustomOption', () => { + it('selects date', () => { + const popover = makePopover() + const instance = popover.instance() + spyOn(instance, 'selectDate') + instance.onSelectCustomOption('date', 'abc') + expect(instance.selectDate).toHaveBeenCalledWith('date', 'Custom') + }); + + it('throws error if date is invalid', () => { + spyOn(NylasEnv, 'showErrorDialog') + const popover = makePopover() + popover.instance().onSelectCustomOption(null, 'abc') + expect(NylasEnv.showErrorDialog).toHaveBeenCalled() + }); + }); + + describe('render', () => { + it('renders the provided dateOptions', () => { + const popover = makePopover({ + dateOptions: { + 'label 1-': () => {}, + 'label 2-': () => {}, + }, + }) + const items = popover.find('.item') + expect(items.at(0).text()).toEqual('label 1-formatted') + expect(items.at(1).text()).toEqual('label 2-formatted') + }); + + it('renders header components', () => { + const popover = makePopover() + expect(popover.find('.header').text()).toEqual('my header') + }) + + it('renders footer components', () => { + const popover = makePopover({ + footer: footer, + }) + expect(popover.find('.footer').text()).toEqual('footer') + expect(popover.find('.date-input-section').isEmpty()).toBe(false) + }); + }); +}); + diff --git a/src/components/date-input.jsx b/src/components/date-input.jsx index 111392673..deb176a4e 100644 --- a/src/components/date-input.jsx +++ b/src/components/date-input.jsx @@ -10,23 +10,30 @@ class DateInput extends Component { static propTypes = { className: PropTypes.string, dateFormat: PropTypes.string.isRequired, - onSubmitDate: PropTypes.func, + onDateInterpreted: PropTypes.func, + onDateSubmitted: PropTypes.func, }; static defaultProps = { - onSubmitDate: () => {}, + onDateInterpreted: () => {}, + onDateSubmitted: () => {}, }; constructor(props) { super(props) - this.unmounted = false + this._mounted = false this.state = { inputDate: null, + inputValue: '', } } + componentDidMount() { + this._mounted = true + } + componentWillUnmount() { - this.unmounted = true + this._mounted = false } onInputKeyDown = (event) => { @@ -35,39 +42,46 @@ class DateInput extends Component { // This prevents onInputChange from being fired event.stopPropagation(); const date = DateUtils.futureDateFromString(value); - this.props.onSubmitDate(date, value); - - // this.props.onSubmitDate may have unmounted this component - if (!this.unmounted) { - this.setState({inputDate: null}) - } + this.props.onDateSubmitted(date, value); } }; onInputChange = (event) => { - this.setState({inputDate: DateUtils.futureDateFromString(event.target.value)}); + const {target: {value}} = event + const nextDate = DateUtils.futureDateFromString(value) + if (nextDate) { + this.props.onDateInterpreted(nextDate.clone(), value) + } + this.setState({inputDate: nextDate, inputValue: value}); }; + clearInput() { + setImmediate(() => { + if (!this._mounted) { return } + this.setState({inputValue: '', inputDate: null}) + }) + } + render() { - let dateInterpretation; - if (this.state.inputDate) { - dateInterpretation = ( - - {DateUtils.format(this.state.inputDate, this.props.dateFormat)} - - ); - } const {className} = this.props + const {inputDate, inputValue} = this.state const classes = classnames({ "nylas-date-input": true, [className]: className != null, }) + const dateInterpretation = inputDate ? + + {DateUtils.format(this.state.inputDate, this.props.dateFormat)} + : + ; + return (
{ + const {dateOptions} = this.props + const date = dateOptions[optionKey](); + this.refs.dateInput.clearInput() + this.selectDate(date, optionKey); + }; + + onCustomDateInterpreted = (date) => { + const {shouldSelectDateWhenInterpreted} = this.props + if (date && shouldSelectDateWhenInterpreted) { + this.refs.menu.clearSelection() + this.selectDate(date, "Custom"); + } + } + + onCustomDateSelected = (date, inputValue) => { + if (date) { + this.refs.menu.clearSelection() + this.selectDate(date, "Custom"); + } else { + NylasEnv.showErrorDialog(`Sorry, we can't interpret ${inputValue} as a valid date.`); + } + }; + + selectDate = (date, dateLabel) => { + const formatted = DateUtils.format(date.utc()); + this.props.onSelectDate(formatted, dateLabel); + }; + + renderMenuOption = (optionKey) => { + const {dateOptions} = this.props + const date = dateOptions[optionKey](); + const formatted = DateUtils.format(date, DATE_FORMAT_SHORT); + return ( +
+ {optionKey} + {formatted} +
+ ); + } + + render() { + const {className, header, footer, dateOptions} = this.props + + let footerComponents = [ +
, + , + ] + if (footer) { + if (Array.isArray(footer)) { + footerComponents = footerComponents.concat(footer) + } else { + footerComponents = footerComponents.concat([footer]) + } + } + + return ( +
+ item} + itemContent={this.renderMenuOption} + defaultSelectedIndex={-1} + headerComponents={header} + footerComponents={footerComponents} + onEscape={this.onEscape} + onSelect={this.onSelectMenuOption} + /> +
+ ); + } +} + +export default DatePickerPopover diff --git a/src/components/decorators/listens-to-observable.jsx b/src/components/decorators/listens-to-observable.jsx index be2c173fd..1a8b20c7c 100644 --- a/src/components/decorators/listens-to-observable.jsx +++ b/src/components/decorators/listens-to-observable.jsx @@ -6,10 +6,10 @@ function ListensToObservable(ComposedComponent, {getObservable, getStateFromObse static containerRequired = ComposedComponent.containerRequired; - constructor() { - super() - this.state = getStateFromObservable() - this.observable = getObservable() + constructor(props) { + super(props) + this.state = getStateFromObservable(null, {props}) + this.observable = getObservable(props) } componentDidMount() { @@ -24,7 +24,7 @@ function ListensToObservable(ComposedComponent, {getObservable, getStateFromObse onObservableChanged = (data) => { if (this.unmounted) return; - this.setState(getStateFromObservable(data)) + this.setState(getStateFromObservable(data, {props: this.props})) }; render() { diff --git a/src/components/mail-label-set.jsx b/src/components/mail-label-set.jsx index 5b40b4395..830b7d26e 100644 --- a/src/components/mail-label-set.jsx +++ b/src/components/mail-label-set.jsx @@ -15,6 +15,7 @@ export default class MailLabelSet extends React.Component { static propTypes = { thread: React.PropTypes.object.isRequired, + messages: React.PropTypes.array, includeCurrentCategories: React.PropTypes.bool, removable: React.PropTypes.bool, }; @@ -28,7 +29,7 @@ export default class MailLabelSet extends React.Component { } render() { - const {thread, includeCurrentCategories} = this.props; + const {thread, messages, includeCurrentCategories} = this.props; const labels = []; if (AccountStore.accountForId(thread.accountId).usesLabels()) { @@ -73,7 +74,7 @@ export default class MailLabelSet extends React.Component { containersRequired={false} matching={{role: "Thread:MailLabel"}} className="thread-injected-mail-labels" - exposedProps={{thread: thread}} + exposedProps={{thread, messages}} > {labels} diff --git a/src/components/menu.cjsx b/src/components/menu.cjsx index 4a00edfff..59bcfe897 100644 --- a/src/components/menu.cjsx +++ b/src/components/menu.cjsx @@ -12,17 +12,20 @@ MenuItem's props allow you to display dividers as well as standard items. Section: Component Kit ### class MenuItem extends React.Component + @displayName = 'MenuItem' ### Public: React `props` supported by MenuItem: + - `index`: {Number} of the index of the current menu item - `divider` (optional) Pass a {Boolean} to render the menu item as a section divider. - `key` (optional) Pass a {String} to be the React key to optimize rendering lists of items. - `selected` (optional) Pass a {Boolean} to specify whether the item is selected. - `checked` (optional) Pass a {Boolean} to specify whether the item is checked. ### @propTypes: + index: React.PropTypes.number.isRequired divider: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool]) selected: React.PropTypes.bool checked: React.PropTypes.bool @@ -140,8 +143,8 @@ class Menu extends React.Component ### @propTypes: className: React.PropTypes.string, - footerComponents: React.PropTypes.arrayOf(React.PropTypes.element), - headerComponents: React.PropTypes.arrayOf(React.PropTypes.element), + footerComponents: React.PropTypes.node, + headerComponents: React.PropTypes.node, itemContext: React.PropTypes.object, itemContent: React.PropTypes.func.isRequired, itemKey: React.PropTypes.func.isRequired, @@ -159,6 +162,7 @@ class Menu extends React.Component onEscape: -> constructor: (@props) -> + @_mounted = false @state = selectedIndex: @props.defaultSelectedIndex ? 0 @@ -167,6 +171,18 @@ class Menu extends React.Component getSelectedItem: => @props.items[@state.selectedIndex] + # TODO this is a hack, refactor + clearSelection: => + setImmediate(=> + return if @_mounted is false + @setState({selectedIndex: -1}) + ) + + componentDidMount: => + @_mounted = true + + componentWillUnmount: => + @_mounted = false componentWillReceiveProps: (newProps) => # Attempt to preserve selection across props.items changes by @@ -194,12 +210,11 @@ class Menu extends React.Component container.scrollTop += adjustment render: => - hc = @props.headerComponents ? [] - if hc.length is 0 then hc = - fc = @props.footerComponents ? [] - if fc.length is 0 then fc = + hc = @props.headerComponents ? + fc = @props.footerComponents ? + className = if @props.className then @props.className else ''
{hc} @@ -236,7 +251,9 @@ class Menu extends React.Component onMouseDown = (event) => event.preventDefault() - @props.onSelect(item) if @props.onSelect + @setState({selectedIndex: i}, => + @props.onSelect(item) if @props.onSelect + ) key = @props.itemKey(item) if not key @@ -246,6 +263,7 @@ class Menu extends React.Component seenItemKeys[key] = item .*<\/signature>)|(?:<.+?>)|\s/gmi; return this.body.replace(re, "").length === 0; } + + isHidden() { + return ( + this.to.length === 1 && this.from.length === 1 && + this.to[0].email === this.from[0].email && + (this.snippet || "").startsWith('Nylas N1 Reminder:') + ) + } } diff --git a/src/flux/stores/message-store.coffee b/src/flux/stores/message-store.coffee index 05330a3f2..a71634042 100644 --- a/src/flux/stores/message-store.coffee +++ b/src/flux/stores/message-store.coffee @@ -125,14 +125,14 @@ class MessageStore extends NylasStore itemIndex = _.findIndex @_items, (msg) -> msg.id is item.id or msg.clientId is item.clientId if change.type is 'persist' and itemIndex is -1 - @_items = [].concat(@_items, [item]) + @_items = [].concat(@_items, [item]).filter((m) => !m.isHidden()) @_items = @_sortItemsForDisplay(@_items) @_expandItemsToDefault() @trigger() return if change.type is 'unpersist' and itemIndex isnt -1 - @_items = [].concat(@_items) + @_items = [].concat(@_items).filter((m) => !m.isHidden()) @_items.splice(itemIndex, 1) @_expandItemsToDefault() @trigger() @@ -250,7 +250,8 @@ class MessageStore extends NylasStore loaded = true - @_items = @_sortItemsForDisplay(items) + @_items = items.filter((m) => !m.isHidden()) + @_items = @_sortItemsForDisplay(@_items) # If no items were returned, attempt to load messages via the API. If items # are returned, this will trigger a refresh here. diff --git a/src/global/nylas-component-kit.coffee b/src/global/nylas-component-kit.coffee index f00133b36..d62f6b440 100644 --- a/src/global/nylas-component-kit.coffee +++ b/src/global/nylas-component-kit.coffee @@ -8,6 +8,15 @@ class NylasComponentKit get: -> NylasComponentKit.default(require "../components/#{path}") + # We load immediately when the component won't be loaded until the user + # performs an action. For example, opening a popover. In this case, the + # popover would take a long time to open the first time the user tries to open + # the popover + @loadImmediately = (prop, path) -> + exported = NylasComponentKit.default(require "../components/#{path}") + Object.defineProperty @prototype, prop, + get: -> exported + @loadFrom = (prop, path) -> Object.defineProperty @prototype, prop, get: -> @@ -28,6 +37,7 @@ class NylasComponentKit @load "Switch", 'switch' @loadDeprecated "Popover", 'popover', instead: 'Actions.openPopover' @load "FixedPopover", 'fixed-popover' + @loadImmediately "DatePickerPopover", 'date-picker-popover' @load "Modal", 'modal' @load "Flexbox", 'flexbox' @load "RetinaImg", 'retina-img' diff --git a/src/pro b/src/pro index ad9ff8f68..701ccb72d 160000 --- a/src/pro +++ b/src/pro @@ -1 +1 @@ -Subproject commit ad9ff8f680f72bdbe2f30a613215a95f515eb891 +Subproject commit 701ccb72d1bc9b7d9f0a31e829be2fc3fe52a964 diff --git a/static/components/date-input.less b/static/components/date-input.less index ffee83e1e..1eec6a182 100644 --- a/static/components/date-input.less +++ b/static/components/date-input.less @@ -1,6 +1,8 @@ @import "ui-variables"; .nylas-date-input { + text-align: center; + .date-interpretation { color: @text-color-subtle; font-size: @font-size-small; diff --git a/static/components/date-picker-popover.less b/static/components/date-picker-popover.less new file mode 100644 index 000000000..c93bde6c1 --- /dev/null +++ b/static/components/date-picker-popover.less @@ -0,0 +1,17 @@ +@import "ui-variables"; + +.date-picker-popover { + .menu .item { + .time { + display: none; + float: right; + padding-right: @padding-base-horizontal; + } + &.selected, + &:hover { + .time { + display: inline-block; + } + } + } +} diff --git a/static/components/fixed-popover.less b/static/components/fixed-popover.less index 836d12d14..5c961877d 100644 --- a/static/components/fixed-popover.less +++ b/static/components/fixed-popover.less @@ -25,6 +25,7 @@ .menu { z-index:1; position: relative; + width: 250px; .content-container { background: none; } @@ -50,6 +51,14 @@ } } + .section { + padding: @padding-base-vertical * 1.5 @padding-base-horizontal; + } + + .divider { + border-top: 1px solid @border-color-divider; + } + input[type=text] { border: 1px solid darken(@background-secondary, 10%); border-radius: 3px; diff --git a/static/images/composer/icon-composer-reminders@1x.png b/static/images/composer/icon-composer-reminders@1x.png new file mode 100644 index 000000000..1e22291c2 Binary files /dev/null and b/static/images/composer/icon-composer-reminders@1x.png differ diff --git a/static/images/composer/icon-composer-reminders@2x.png b/static/images/composer/icon-composer-reminders@2x.png new file mode 100644 index 000000000..e180a7add Binary files /dev/null and b/static/images/composer/icon-composer-reminders@2x.png differ diff --git a/static/images/empty-state/ic-emptystate-reminders@1x.png b/static/images/empty-state/ic-emptystate-reminders@1x.png new file mode 100644 index 000000000..861811824 Binary files /dev/null and b/static/images/empty-state/ic-emptystate-reminders@1x.png differ diff --git a/static/images/empty-state/ic-emptystate-reminders@2x.png b/static/images/empty-state/ic-emptystate-reminders@2x.png new file mode 100644 index 000000000..0a411f0ed Binary files /dev/null and b/static/images/empty-state/ic-emptystate-reminders@2x.png differ diff --git a/static/images/source-list/reminders@1x.png b/static/images/source-list/reminders@1x.png new file mode 100644 index 000000000..776fdddd8 Binary files /dev/null and b/static/images/source-list/reminders@1x.png differ diff --git a/static/images/source-list/reminders@2x.png b/static/images/source-list/reminders@2x.png new file mode 100644 index 000000000..2d418af7d Binary files /dev/null and b/static/images/source-list/reminders@2x.png differ diff --git a/static/images/thread-list/ic-timestamp-reminder@2x.png b/static/images/thread-list/ic-timestamp-reminder@2x.png new file mode 100644 index 000000000..7dd12fcb7 Binary files /dev/null and b/static/images/thread-list/ic-timestamp-reminder@2x.png differ diff --git a/static/images/thread-list/ic-timestamp-snooze@2x.png b/static/images/thread-list/ic-timestamp-snooze@2x.png new file mode 100644 index 000000000..c7dcaf6f7 Binary files /dev/null and b/static/images/thread-list/ic-timestamp-snooze@2x.png differ diff --git a/static/images/thread-list/icon-reminder@1x.png b/static/images/thread-list/icon-reminder@1x.png new file mode 100644 index 000000000..704903c9e Binary files /dev/null and b/static/images/thread-list/icon-reminder@1x.png differ diff --git a/static/images/thread-list/icon-reminder@2x.png b/static/images/thread-list/icon-reminder@2x.png new file mode 100644 index 000000000..2a820142e Binary files /dev/null and b/static/images/thread-list/icon-reminder@2x.png differ diff --git a/static/images/thread-list/in-label-bell@1x.png b/static/images/thread-list/in-label-bell@1x.png new file mode 100644 index 000000000..04d5505d5 Binary files /dev/null and b/static/images/thread-list/in-label-bell@1x.png differ diff --git a/static/images/thread-list/in-label-bell@2x.png b/static/images/thread-list/in-label-bell@2x.png new file mode 100644 index 000000000..93fbcf7b3 Binary files /dev/null and b/static/images/thread-list/in-label-bell@2x.png differ diff --git a/static/images/toolbar/ic-toolbar-native-reminder@1x.png b/static/images/toolbar/ic-toolbar-native-reminder@1x.png new file mode 100644 index 000000000..7794e167a Binary files /dev/null and b/static/images/toolbar/ic-toolbar-native-reminder@1x.png differ diff --git a/static/images/toolbar/ic-toolbar-native-reminder@2x.png b/static/images/toolbar/ic-toolbar-native-reminder@2x.png new file mode 100644 index 000000000..59a67bb9c Binary files /dev/null and b/static/images/toolbar/ic-toolbar-native-reminder@2x.png differ diff --git a/static/index.less b/static/index.less index ec54dc80a..6732a47b5 100644 --- a/static/index.less +++ b/static/index.less @@ -30,6 +30,7 @@ @import "components/editable-list"; @import "components/outline-view"; @import "components/fixed-popover"; +@import "components/date-picker-popover"; @import "components/modal"; @import "components/date-input"; @import "components/nylas-calendar";